Skip to content

Commit

Permalink
ACP-77: Add caching to SoV DB helpers (#3516)
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenButtolph authored Nov 4, 2024
1 parent 9e9d658 commit 45b4507
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 68 deletions.
55 changes: 46 additions & 9 deletions vms/platformvm/state/subnet_only_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (

"github.com/google/btree"

"github.com/ava-labs/avalanchego/cache"
"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils"
"github.com/ava-labs/avalanchego/utils/maybe"
"github.com/ava-labs/avalanchego/vms/platformvm/block"
)

Expand Down Expand Up @@ -103,29 +105,64 @@ func (v SubnetOnlyValidator) immutableFieldsAreUnmodified(o SubnetOnlyValidator)
v.StartTime == o.StartTime
}

func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (SubnetOnlyValidator, error) {
func getSubnetOnlyValidator(
cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]],
db database.KeyValueReader,
validationID ids.ID,
) (SubnetOnlyValidator, error) {
if maybeSOV, ok := cache.Get(validationID); ok {
if maybeSOV.IsNothing() {
return SubnetOnlyValidator{}, database.ErrNotFound
}
return maybeSOV.Value(), nil
}

bytes, err := db.Get(validationID[:])
if err == database.ErrNotFound {
cache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]())
return SubnetOnlyValidator{}, database.ErrNotFound
}
if err != nil {
return SubnetOnlyValidator{}, err
}

vdr := SubnetOnlyValidator{
sov := SubnetOnlyValidator{
ValidationID: validationID,
}
if _, err := block.GenesisCodec.Unmarshal(bytes, &vdr); err != nil {
if _, err := block.GenesisCodec.Unmarshal(bytes, &sov); err != nil {
return SubnetOnlyValidator{}, fmt.Errorf("failed to unmarshal SubnetOnlyValidator: %w", err)
}
return vdr, nil

cache.Put(validationID, maybe.Some(sov))
return sov, nil
}

func putSubnetOnlyValidator(db database.KeyValueWriter, vdr SubnetOnlyValidator) error {
bytes, err := block.GenesisCodec.Marshal(block.CodecVersion, vdr)
func putSubnetOnlyValidator(
db database.KeyValueWriter,
cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]],
sov SubnetOnlyValidator,
) error {
bytes, err := block.GenesisCodec.Marshal(block.CodecVersion, sov)
if err != nil {
return fmt.Errorf("failed to marshal SubnetOnlyValidator: %w", err)
}
return db.Put(vdr.ValidationID[:], bytes)
if err := db.Put(sov.ValidationID[:], bytes); err != nil {
return err
}

cache.Put(sov.ValidationID, maybe.Some(sov))
return nil
}

func deleteSubnetOnlyValidator(db database.KeyValueDeleter, validationID ids.ID) error {
return db.Delete(validationID[:])
func deleteSubnetOnlyValidator(
db database.KeyValueDeleter,
cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]],
validationID ids.ID,
) error {
if err := db.Delete(validationID[:]); err != nil {
return err
}

cache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]())
return nil
}
163 changes: 104 additions & 59 deletions vms/platformvm/state/subnet_only_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import (

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/cache"
"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/database/memdb"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
"github.com/ava-labs/avalanchego/utils/maybe"
"github.com/ava-labs/avalanchego/vms/platformvm/block"
"github.com/ava-labs/avalanchego/vms/platformvm/fx"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
)

func TestSubnetOnlyValidator_Compare(t *testing.T) {
Expand Down Expand Up @@ -77,33 +77,22 @@ func TestSubnetOnlyValidator_Compare(t *testing.T) {

func TestSubnetOnlyValidator_immutableFieldsAreUnmodified(t *testing.T) {
var (
randomSOV = func() SubnetOnlyValidator {
return SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
SubnetID: ids.GenerateTestID(),
NodeID: ids.GenerateTestNodeID(),
PublicKey: utils.RandomBytes(bls.PublicKeyLen),
RemainingBalanceOwner: utils.RandomBytes(32),
DeactivationOwner: utils.RandomBytes(32),
StartTime: rand.Uint64(), // #nosec G404
}
}
randomizeSOV = func(sov SubnetOnlyValidator) SubnetOnlyValidator {
// Randomize unrelated fields
sov.Weight = rand.Uint64() // #nosec G404
sov.MinNonce = rand.Uint64() // #nosec G404
sov.EndAccumulatedFee = rand.Uint64() // #nosec G404
return sov
}
sov = randomSOV()
sov = newSoV()
)

t.Run("equal", func(t *testing.T) {
v := randomizeSOV(sov)
require.True(t, sov.immutableFieldsAreUnmodified(v))
})
t.Run("everything is different", func(t *testing.T) {
v := randomizeSOV(randomSOV())
v := randomizeSOV(newSoV())
require.True(t, sov.immutableFieldsAreUnmodified(v))
})
t.Run("different subnetID", func(t *testing.T) {
Expand Down Expand Up @@ -138,64 +127,120 @@ func TestSubnetOnlyValidator_immutableFieldsAreUnmodified(t *testing.T) {
})
}

func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) {
require := require.New(t)
db := memdb.New()
func TestGetSubnetOnlyValidator(t *testing.T) {
var (
sov = newSoV()
dbWithSoV = memdb.New()
dbWithoutSoV = memdb.New()
cacheWithSoV = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10}
cacheWithoutSoV = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10}
)

sk, err := bls.NewSecretKey()
require.NoError(err)
pk := bls.PublicFromSecretKey(sk)
pkBytes := bls.PublicKeyToUncompressedBytes(pk)
require.NoError(t, putSubnetOnlyValidator(dbWithSoV, cacheWithSoV, sov))
require.NoError(t, deleteSubnetOnlyValidator(dbWithoutSoV, cacheWithoutSoV, sov.ValidationID))

var remainingBalanceOwner fx.Owner = &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
ids.GenerateTestShortID(),
tests := []struct {
name string
cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]]
db database.KeyValueReader
expectedSoV SubnetOnlyValidator
expectedErr error
expectedEntry maybe.Maybe[SubnetOnlyValidator]
}{
{
name: "cached with validator",
cache: cacheWithSoV,
db: dbWithoutSoV,
expectedSoV: sov,
expectedEntry: maybe.Some(sov),
},
{
name: "from disk with validator",
cache: &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10},
db: dbWithSoV,
expectedSoV: sov,
expectedEntry: maybe.Some(sov),
},
{
name: "cached without validator",
cache: cacheWithoutSoV,
db: dbWithSoV,
expectedErr: database.ErrNotFound,
},
{
name: "from disk without validator",
cache: &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10},
db: dbWithoutSoV,
expectedErr: database.ErrNotFound,
},
}
remainingBalanceOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &remainingBalanceOwner)
require.NoError(err)
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)

var deactivationOwner fx.Owner = &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
ids.GenerateTestShortID(),
},
gotSoV, err := getSubnetOnlyValidator(test.cache, test.db, sov.ValidationID)
require.ErrorIs(err, test.expectedErr)
require.Equal(test.expectedSoV, gotSoV)

cachedSoV, ok := test.cache.Get(sov.ValidationID)
require.True(ok)
require.Equal(test.expectedEntry, cachedSoV)
})
}
deactivationOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &deactivationOwner)
}

func TestPutSubnetOnlyValidator(t *testing.T) {
var (
require = require.New(t)
sov = newSoV()
db = memdb.New()
cache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10}
)
expectedSoVBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, sov)
require.NoError(err)

require.NoError(putSubnetOnlyValidator(db, cache, sov))

sovBytes, err := db.Get(sov.ValidationID[:])
require.NoError(err)
require.Equal(expectedSoVBytes, sovBytes)

sovFromCache, ok := cache.Get(sov.ValidationID)
require.True(ok)
require.Equal(maybe.Some(sov), sovFromCache)
}

func TestDeleteSubnetOnlyValidator(t *testing.T) {
var (
require = require.New(t)
validationID = ids.GenerateTestID()
db = memdb.New()
cache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10}
)
require.NoError(db.Put(validationID[:], nil))

require.NoError(deleteSubnetOnlyValidator(db, cache, validationID))

vdr := SubnetOnlyValidator{
hasSoV, err := db.Has(validationID[:])
require.NoError(err)
require.False(hasSoV)

sovFromCache, ok := cache.Get(validationID)
require.True(ok)
require.Equal(maybe.Nothing[SubnetOnlyValidator](), sovFromCache)
}

func newSoV() SubnetOnlyValidator {
return SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
SubnetID: ids.GenerateTestID(),
NodeID: ids.GenerateTestNodeID(),
PublicKey: pkBytes,
RemainingBalanceOwner: remainingBalanceOwnerBytes,
DeactivationOwner: deactivationOwnerBytes,
PublicKey: utils.RandomBytes(bls.PublicKeyLen),
RemainingBalanceOwner: utils.RandomBytes(32),
DeactivationOwner: utils.RandomBytes(32),
StartTime: rand.Uint64(), // #nosec G404
Weight: rand.Uint64(), // #nosec G404
MinNonce: rand.Uint64(), // #nosec G404
EndAccumulatedFee: rand.Uint64(), // #nosec G404
}

// Validator hasn't been put on disk yet
gotVdr, err := getSubnetOnlyValidator(db, vdr.ValidationID)
require.ErrorIs(err, database.ErrNotFound)
require.Zero(gotVdr)

// Place the validator on disk
require.NoError(putSubnetOnlyValidator(db, vdr))

// Verify that the validator can be fetched from disk
gotVdr, err = getSubnetOnlyValidator(db, vdr.ValidationID)
require.NoError(err)
require.Equal(vdr, gotVdr)

// Remove the validator from disk
require.NoError(deleteSubnetOnlyValidator(db, vdr.ValidationID))

// Verify that the validator has been removed from disk
gotVdr, err = getSubnetOnlyValidator(db, vdr.ValidationID)
require.ErrorIs(err, database.ErrNotFound)
require.Zero(gotVdr)
}

0 comments on commit 45b4507

Please sign in to comment.