Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 54 additions & 20 deletions vms/platformvm/state/subnet_only_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@
package state

import (
"bytes"
"errors"
"fmt"

"github.com/google/btree"

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

var _ btree.LessFunc[*SubnetOnlyValidator] = (*SubnetOnlyValidator).Less
var (
_ btree.LessFunc[SubnetOnlyValidator] = SubnetOnlyValidator.Less
_ utils.Sortable[SubnetOnlyValidator] = SubnetOnlyValidator{}

ErrMutatedSubnetOnlyValidator = errors.New("subnet only validator contains mutated constant fields")
)

// SubnetOnlyValidator defines an ACP-77 validator. For a given ValidationID, it
// is expected for SubnetID, NodeID, PublicKey, RemainingBalanceOwner, and
// StartTime to be constant.
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.
Expand All @@ -27,6 +38,14 @@ type SubnetOnlyValidator struct {
// guaranteed to be populated.
PublicKey []byte `serialize:"true"`

// RemainingBalanceOwner is the owner that will be used when returning the
// balance of the validator after removing accrued fees.
RemainingBalanceOwner []byte `serialize:"true"`

// DeactivationOwner is the owner that can manually deactivate the
// validator.
DeactivationOwner []byte `serialize:"true"`

// StartTime is the unix timestamp, in seconds, when this validator was
// added to the set.
StartTime uint64 `serialize:"true"`
Expand All @@ -46,44 +65,59 @@ type SubnetOnlyValidator struct {
// 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.
//
// If this value is 0, the validator is inactive.
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 {
func (v SubnetOnlyValidator) Less(o SubnetOnlyValidator) bool {
return v.Compare(o) == -1
}

// Compare determines a canonical ordering of SubnetOnlyValidators based on
// their EndAccumulatedFees and ValidationIDs. Lower EndAccumulatedFees result
// in an earlier ordering.
func (v SubnetOnlyValidator) Compare(o SubnetOnlyValidator) int {
switch {
case v.EndAccumulatedFee < o.EndAccumulatedFee:
return true
return -1
case o.EndAccumulatedFee < v.EndAccumulatedFee:
return false
return 1
default:
return v.ValidationID.Compare(o.ValidationID) == -1
return v.ValidationID.Compare(o.ValidationID)
}
}

// constantsAreUnmodified returns true if the constants of this validator have
// not been modified compared to the other validator.
func (v SubnetOnlyValidator) constantsAreUnmodified(o SubnetOnlyValidator) bool {
if v.ValidationID != o.ValidationID {
return true
}
return v.SubnetID == o.SubnetID &&
v.NodeID == o.NodeID &&
bytes.Equal(v.PublicKey, o.PublicKey) &&
bytes.Equal(v.RemainingBalanceOwner, o.RemainingBalanceOwner) &&
bytes.Equal(v.DeactivationOwner, o.DeactivationOwner) &&
v.StartTime == o.StartTime
}

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(No action required) 😄

}

func putSubnetOnlyValidator(db database.KeyValueWriter, vdr *SubnetOnlyValidator) error {
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)
Expand Down
148 changes: 118 additions & 30 deletions vms/platformvm/state/subnet_only_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,88 +12,176 @@ import (
"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/vms/platformvm/block"
"github.com/ava-labs/avalanchego/vms/platformvm/fx"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
)

func TestSubnetOnlyValidator_Less(t *testing.T) {
func TestSubnetOnlyValidator_Compare(t *testing.T) {
tests := []struct {
name string
v *SubnetOnlyValidator
o *SubnetOnlyValidator
equal bool
name string
v SubnetOnlyValidator
o SubnetOnlyValidator
expected int
}{
{
name: "v.EndAccumulatedFee < o.EndAccumulatedFee",
v: &SubnetOnlyValidator{
v: SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
EndAccumulatedFee: 1,
},
o: &SubnetOnlyValidator{
o: SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
EndAccumulatedFee: 2,
},
equal: false,
expected: -1,
},
{
name: "v.EndAccumulatedFee = o.EndAccumulatedFee, v.ValidationID < o.ValidationID",
v: &SubnetOnlyValidator{
v: SubnetOnlyValidator{
ValidationID: ids.ID{0},
EndAccumulatedFee: 1,
},
o: &SubnetOnlyValidator{
o: SubnetOnlyValidator{
ValidationID: ids.ID{1},
EndAccumulatedFee: 1,
},
equal: false,
expected: -1,
},
{
name: "v.EndAccumulatedFee = o.EndAccumulatedFee, v.ValidationID = o.ValidationID",
v: &SubnetOnlyValidator{
v: SubnetOnlyValidator{
ValidationID: ids.ID{0},
EndAccumulatedFee: 1,
},
o: &SubnetOnlyValidator{
o: SubnetOnlyValidator{
ValidationID: ids.ID{0},
EndAccumulatedFee: 1,
},
equal: true,
expected: 0,
},
}
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)
require.Equal(test.expected, test.v.Compare(test.o))
require.Equal(-test.expected, test.o.Compare(test.v))
require.Equal(test.expected == -1, test.v.Less(test.o))
require.False(test.o.Less(test.v))
})
}
}

func TestSubnetOnlyValidator_constantsAreUnmodified(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()
)

t.Run("equal", func(t *testing.T) {
v := randomizeSOV(sov)
require.True(t, sov.constantsAreUnmodified(v))
})
t.Run("everything is different", func(t *testing.T) {
v := randomizeSOV(randomSOV())
require.True(t, sov.constantsAreUnmodified(v))
})
t.Run("different subnetID", func(t *testing.T) {
v := randomizeSOV(sov)
v.SubnetID = ids.GenerateTestID()
require.False(t, sov.constantsAreUnmodified(v))
})
t.Run("different nodeID", func(t *testing.T) {
v := randomizeSOV(sov)
v.NodeID = ids.GenerateTestNodeID()
require.False(t, sov.constantsAreUnmodified(v))
})
t.Run("different publicKey", func(t *testing.T) {
v := randomizeSOV(sov)
v.PublicKey = utils.RandomBytes(bls.PublicKeyLen)
require.False(t, sov.constantsAreUnmodified(v))
})
t.Run("different remainingBalanceOwner", func(t *testing.T) {
v := randomizeSOV(sov)
v.RemainingBalanceOwner = utils.RandomBytes(32)
require.False(t, sov.constantsAreUnmodified(v))
})
t.Run("different deactivationOwner", func(t *testing.T) {
v := randomizeSOV(sov)
v.DeactivationOwner = utils.RandomBytes(32)
require.False(t, sov.constantsAreUnmodified(v))
})
t.Run("different startTime", func(t *testing.T) {
v := randomizeSOV(sov)
v.StartTime = rand.Uint64() // #nosec G404
require.False(t, sov.constantsAreUnmodified(v))
})
}

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

sk, err := bls.NewSecretKey()
require.NoError(err)
pk := bls.PublicFromSecretKey(sk)
pkBytes := bls.PublicKeyToUncompressedBytes(pk)

var remainingBalanceOwner fx.Owner = &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
ids.GenerateTestShortID(),
},
}
remainingBalanceOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &remainingBalanceOwner)
require.NoError(err)

var deactivationOwner fx.Owner = &secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{
ids.GenerateTestShortID(),
},
}
deactivationOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &deactivationOwner)
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
vdr := SubnetOnlyValidator{
ValidationID: ids.GenerateTestID(),
SubnetID: ids.GenerateTestID(),
NodeID: ids.GenerateTestNodeID(),
PublicKey: pkBytes,
RemainingBalanceOwner: remainingBalanceOwnerBytes,
DeactivationOwner: deactivationOwnerBytes,
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)
require.Zero(gotVdr)

// Place the validator on disk
require.NoError(putSubnetOnlyValidator(db, vdr))
Expand All @@ -109,5 +197,5 @@ func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) {
// Verify that the validator has been removed from disk
gotVdr, err = getSubnetOnlyValidator(db, vdr.ValidationID)
require.ErrorIs(err, database.ErrNotFound)
require.Nil(gotVdr)
require.Zero(gotVdr)
}
Loading