Skip to content

Commit

Permalink
Add SoV state struct (#3370)
Browse files Browse the repository at this point in the history
Signed-off-by: Dhruba Basu <[email protected]>
Co-authored-by: dhrubabasu <[email protected]>
  • Loading branch information
2 people authored and michaelkaplan13 committed Sep 11, 2024
1 parent 92820ce commit 5f04349
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 0 deletions.
96 changes: 96 additions & 0 deletions vms/platformvm/state/subnet_only_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package state

import (
"fmt"

"github.com/google/btree"

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

var _ btree.LessFunc[*SubnetOnlyValidator] = (*SubnetOnlyValidator).Less

type SubnetOnlyValidator struct {
// ValidationID is not serialized because it is used as the key in the
// database, so it doesn't need to be stored in the value.
ValidationID ids.ID

SubnetID ids.ID `serialize:"true"`
NodeID ids.NodeID `serialize:"true"`

// PublicKey is the uncompressed BLS public key of the validator. It is
// guaranteed to be populated.
PublicKey []byte `serialize:"true"`

// StartTime is the unix timestamp, in seconds, when this validator was
// added to the set.
StartTime uint64 `serialize:"true"`

// Weight of this validator. It can be updated when the MinNonce is
// increased. If the weight is being set to 0, the validator is being
// removed.
Weight uint64 `serialize:"true"`

// MinNonce is the smallest nonce that can be used to modify this
// validator's weight. It is initially set to 0 and is set to one higher
// than the last nonce used. It is not valid to use nonce MaxUint64 unless
// the weight is being set to 0, which removes the validator from the set.
MinNonce uint64 `serialize:"true"`

// EndAccumulatedFee is the amount of globally accumulated fees that can
// accrue before this validator must be deactivated. It is equal to the
// amount of fees this validator is willing to pay plus the amount of
// globally accumulated fees when this validator started validating.
EndAccumulatedFee uint64 `serialize:"true"`
}

// Less determines a canonical ordering of *SubnetOnlyValidators based on their
// EndAccumulatedFees and ValidationIDs.
//
// Returns true if:
//
// 1. This validator has a lower EndAccumulatedFee than the other.
// 2. This validator has an equal EndAccumulatedFee to the other and has a
// lexicographically lower ValidationID.
func (v *SubnetOnlyValidator) Less(o *SubnetOnlyValidator) bool {
switch {
case v.EndAccumulatedFee < o.EndAccumulatedFee:
return true
case o.EndAccumulatedFee < v.EndAccumulatedFee:
return false
default:
return v.ValidationID.Compare(o.ValidationID) == -1
}
}

func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (*SubnetOnlyValidator, error) {
bytes, err := db.Get(validationID[:])
if err != nil {
return nil, err
}

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

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

func deleteSubnetOnlyValidator(db database.KeyValueDeleter, validationID ids.ID) error {
return db.Delete(validationID[:])
}
113 changes: 113 additions & 0 deletions vms/platformvm/state/subnet_only_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package state

import (
"math/rand"
"testing"

"github.com/stretchr/testify/require"

"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/crypto/bls"
)

func TestSubnetOnlyValidator_Less(t *testing.T) {
tests := []struct {
name string
v *SubnetOnlyValidator
o *SubnetOnlyValidator
equal bool
}{
{
name: "v.EndAccumulatedFee < o.EndAccumulatedFee",
v: &SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
EndAccumulatedFee: 1,
},
o: &SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
EndAccumulatedFee: 2,
},
equal: false,
},
{
name: "v.EndAccumulatedFee = o.EndAccumulatedFee, v.ValidationID < o.ValidationID",
v: &SubnetOnlyValidator{
ValidationID: ids.ID{0},
EndAccumulatedFee: 1,
},
o: &SubnetOnlyValidator{
ValidationID: ids.ID{1},
EndAccumulatedFee: 1,
},
equal: false,
},
{
name: "v.EndAccumulatedFee = o.EndAccumulatedFee, v.ValidationID = o.ValidationID",
v: &SubnetOnlyValidator{
ValidationID: ids.ID{0},
EndAccumulatedFee: 1,
},
o: &SubnetOnlyValidator{
ValidationID: ids.ID{0},
EndAccumulatedFee: 1,
},
equal: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)

less := test.v.Less(test.o)
require.Equal(!test.equal, less)

greater := test.o.Less(test.v)
require.False(greater)
})
}
}

func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) {
require := require.New(t)
db := memdb.New()

sk, err := bls.NewSecretKey()
require.NoError(err)

vdr := &SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
SubnetID: ids.GenerateTestID(),
NodeID: ids.GenerateTestNodeID(),
PublicKey: bls.PublicKeyToUncompressedBytes(bls.PublicFromSecretKey(sk)),
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.Nil(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.Nil(gotVdr)
}

0 comments on commit 5f04349

Please sign in to comment.