diff --git a/go.mod b/go.mod index 413347fdb575..a9af1d80e3af 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/huin/goupnp v1.0.3 github.com/jackpal/gateway v1.0.6 github.com/jackpal/go-nat-pmp v1.0.2 + github.com/leanovate/gopter v0.2.9 github.com/mr-tron/base58 v1.2.0 github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d github.com/onsi/ginkgo/v2 v2.4.0 diff --git a/go.sum b/go.sum index 836fb8c766af..a3005081e397 100644 --- a/go.sum +++ b/go.sum @@ -313,6 +313,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= diff --git a/vms/platformvm/validator_set_property_test.go b/vms/platformvm/validator_set_property_test.go new file mode 100644 index 000000000000..ef1297cd9a99 --- /dev/null +++ b/vms/platformvm/validator_set_property_test.go @@ -0,0 +1,879 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package platformvm + +import ( + "context" + "errors" + "fmt" + "reflect" + "testing" + "time" + + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/gen" + "github.com/leanovate/gopter/prop" + + "github.com/ava-labs/avalanchego/chains" + "github.com/ava-labs/avalanchego/chains/atomic" + "github.com/ava-labs/avalanchego/database/manager" + "github.com/ava-labs/avalanchego/database/prefixdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/consensus/snowman" + "github.com/ava-labs/avalanchego/snow/engine/common" + "github.com/ava-labs/avalanchego/snow/uptime" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/formatting" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/json" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/version" + "github.com/ava-labs/avalanchego/vms/avm/blocks/executor" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/api" + "github.com/ava-labs/avalanchego/vms/platformvm/blocks" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/utxo" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/blocks/executor" +) + +const ( + startPrimaryWithBLS uint8 = iota + startPrimaryWithoutBLS + startSubnetValidator +) + +var errEmptyEventsList = errors.New("empty events list") + +// for a given (permissioned) subnet, the test stakes and restakes multiple +// times a node as a primary and subnet validator. The BLS key of the node is +// changed across staking periods, and it can even be nil. We test that +// GetValidatorSet returns the correct primary and subnet validators data, with +// the right BLS key version at all relevant heights. +func TestGetValidatorsSetProperty(t *testing.T) { + properties := gopter.NewProperties(nil) + + // to reproduce a given scenario do something like this: + // parameters := gopter.DefaultTestParametersWithSeed(1685887576153675816) + // properties := gopter.NewProperties(parameters) + + properties.Property("check GetValidatorSet", prop.ForAll( + func(events []uint8) string { + vm, subnetID, err := buildVM() + if err != nil { + return fmt.Sprintf("failed building vm: %s", err.Error()) + } + vm.ctx.Lock.Lock() + defer func() { + _ = vm.Shutdown(context.Background()) + vm.ctx.Lock.Unlock() + }() + nodeID := ids.GenerateTestNodeID() + + currentTime := defaultGenesisTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + // build a valid sequence of validators start/end times, given the + // random events sequence received as test input + validatorsTimes, err := buildTimestampsList(events, currentTime, nodeID) + if err != nil { + return fmt.Sprintf("failed building events sequence: %s", err.Error()) + } + + validatorsSetByHeightAndSubnet := make(map[uint64]map[ids.ID]map[ids.NodeID]*validators.GetValidatorOutput) + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + + // insert validator sequence + var ( + currentPrimaryValidator = (*state.Staker)(nil) + currentSubnetValidator = (*state.Staker)(nil) + ) + for _, ev := range validatorsTimes { + // at each we remove at least a subnet validator + if currentSubnetValidator != nil { + err := terminateSubnetValidator(vm, currentSubnetValidator) + if err != nil { + return fmt.Sprintf("could not terminate current subnet validator: %s", err.Error()) + } + currentSubnetValidator = nil + + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + } + + switch ev.eventType { + case startSubnetValidator: + currentSubnetValidator, err = addSubnetValidator(vm, ev, subnetID) + if err != nil { + return fmt.Sprintf("could not add subnet validator: %s", err.Error()) + } + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + + case startPrimaryWithoutBLS: + // when adding a primary validator, also remove the current + // primary one + if currentPrimaryValidator != nil { + err := terminatePrimaryValidator(vm, currentPrimaryValidator) + if err != nil { + return fmt.Sprintf("could not terminate current primary validator: %s", err.Error()) + } + // no need to nil current primary validator, we'll + // reassign immediately + + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + } + currentPrimaryValidator, err = addPrimaryValidatorWithoutBLSKey(vm, ev) + if err != nil { + return fmt.Sprintf("could not add primary validator without BLS key: %s", err.Error()) + } + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + + case startPrimaryWithBLS: + // when adding a primary validator, also remove the current + // primary one + if currentPrimaryValidator != nil { + err := terminatePrimaryValidator(vm, currentPrimaryValidator) + if err != nil { + return fmt.Sprintf("could not terminate current primary validator: %s", err.Error()) + } + // no need to nil current primary validator, we'll + // reassign immediately + + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + } + currentPrimaryValidator, err = addPrimaryValidatorWithBLSKey(vm, ev) + if err != nil { + return fmt.Sprintf("could not add primary validator with BLS key: %s", err.Error()) + } + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + + default: + return fmt.Sprintf("unexpected staker type: %v", ev.eventType) + } + } + if err := takeValidatorsSnapshotAtCurrentHeight(vm, validatorsSetByHeightAndSubnet); err != nil { + return fmt.Sprintf("could not take validators snapshot: %s", err.Error()) + } + + // finally test validators set + for height, subnetSets := range validatorsSetByHeightAndSubnet { + for subnet, validatorsSet := range subnetSets { + res, err := vm.GetValidatorSet(context.Background(), height, subnet) + if err != nil { + return fmt.Sprintf("failed GetValidatorSet: %s", err.Error()) + } + if !reflect.DeepEqual(validatorsSet, res) { + return fmt.Sprintf("failed validators set comparison: %s", err.Error()) + } + } + } + return "" + }, + gen.SliceOfN( + 10, + gen.OneConstOf( + startPrimaryWithBLS, + startPrimaryWithoutBLS, + startSubnetValidator, + ), + ).SuchThat(func(v interface{}) bool { + list := v.([]uint8) + return len(list) > 0 && (list[0] == startPrimaryWithBLS || list[0] == startPrimaryWithoutBLS) + }), + )) + + properties.TestingRun(t) +} + +func takeValidatorsSnapshotAtCurrentHeight(vm *VM, validatorsSetByHeightAndSubnet map[uint64]map[ids.ID]map[ids.NodeID]*validators.GetValidatorOutput) error { + if validatorsSetByHeightAndSubnet == nil { + validatorsSetByHeightAndSubnet = make(map[uint64]map[ids.ID]map[ids.NodeID]*validators.GetValidatorOutput) + } + + lastBlkID := vm.state.GetLastAccepted() + lastBlk, _, err := vm.state.GetStatelessBlock(lastBlkID) + if err != nil { + return err + } + height := lastBlk.Height() + validatorsSetBySubnet, ok := validatorsSetByHeightAndSubnet[height] + if !ok { + validatorsSetByHeightAndSubnet[height] = make(map[ids.ID]map[ids.NodeID]*validators.GetValidatorOutput) + validatorsSetBySubnet = validatorsSetByHeightAndSubnet[height] + } + + stakerIt, err := vm.state.GetCurrentStakerIterator() + if err != nil { + return err + } + for stakerIt.Next() { + v := *stakerIt.Value() + validatorsSet, ok := validatorsSetBySubnet[v.SubnetID] + if !ok { + validatorsSetBySubnet[v.SubnetID] = make(map[ids.NodeID]*validators.GetValidatorOutput) + validatorsSet = validatorsSetBySubnet[v.SubnetID] + } + + blsKey := v.PublicKey + if v.SubnetID != constants.PrimaryNetworkID { + // pick bls key from primary validator + s, err := vm.state.GetCurrentValidator(constants.PlatformChainID, v.NodeID) + if err != nil { + return err + } + blsKey = s.PublicKey + } + + validatorsSet[v.NodeID] = &validators.GetValidatorOutput{ + NodeID: v.NodeID, + PublicKey: blsKey, + Weight: v.Weight, + } + } + return nil +} + +func addSubnetValidator(vm *VM, data *validatorInputData, subnetID ids.ID) (*state.Staker, error) { + addr := keys[0].PublicKey().Address() + signedTx, err := vm.txBuilder.NewAddSubnetValidatorTx( + vm.Config.MinValidatorStake, + uint64(data.startTime.Unix()), + uint64(data.endTime.Unix()), + data.nodeID, + subnetID, + []*secp256k1.PrivateKey{keys[0], keys[1]}, + addr, + ) + if err != nil { + return nil, fmt.Errorf("could not create AddSubnetValidatorTx: %w", err) + } + return internalAddValidator(vm, signedTx) +} + +func addPrimaryValidatorWithBLSKey(vm *VM, data *validatorInputData) (*state.Staker, error) { + addr := keys[0].PublicKey().Address() + utxoHandler := utxo.NewHandler(vm.ctx, &vm.clock, vm.fx) + ins, unstakedOuts, stakedOuts, signers, err := utxoHandler.Spend( + vm.state, + keys, + vm.MinValidatorStake, + vm.Config.AddPrimaryNetworkValidatorFee, + addr, // change Addresss + ) + if err != nil { + return nil, fmt.Errorf("could not create inputs/outputs for permissionless validator: %w", err) + } + sk, err := bls.NewSecretKey() + if err != nil { + return nil, fmt.Errorf("could not create secret key: %w", err) + } + + uPrimaryTx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: unstakedOuts, + }}, + Validator: txs.Validator{ + NodeID: data.nodeID, + Start: uint64(data.startTime.Unix()), + End: uint64(data.endTime.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + Signer: signer.NewProofOfPossession(sk), + StakeOuts: stakedOuts, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegationShares: reward.PercentDenominator, + } + signedTx, err := txs.NewSigned(uPrimaryTx, txs.Codec, signers) + if err != nil { + return nil, fmt.Errorf("could not create AddPermissionlessValidatorTx with BLS key: %w", err) + } + if err := signedTx.SyntacticVerify(vm.ctx); err != nil { + return nil, fmt.Errorf("failed syntax verification of AddPermissionlessValidatorTx: %w", err) + } + return internalAddValidator(vm, signedTx) +} + +func addPrimaryValidatorWithoutBLSKey(vm *VM, data *validatorInputData) (*state.Staker, error) { + addr := keys[0].PublicKey().Address() + signedTx, err := vm.txBuilder.NewAddValidatorTx( + vm.Config.MinValidatorStake, + uint64(data.startTime.Unix()), + uint64(data.endTime.Unix()), + data.nodeID, + addr, + reward.PercentDenominator, + []*secp256k1.PrivateKey{keys[0], keys[1]}, + addr, + ) + if err != nil { + return nil, fmt.Errorf("could not create AddValidatorTx: %w", err) + } + return internalAddValidator(vm, signedTx) +} + +func internalAddValidator(vm *VM, signedTx *txs.Tx) (*state.Staker, error) { + stakerTx := signedTx.Unsigned.(txs.StakerTx) + if err := vm.Builder.AddUnverifiedTx(signedTx); err != nil { + return nil, fmt.Errorf("could not add tx to mempool: %w", err) + } + + blk, err := vm.Builder.BuildBlock(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed building block: %w", err) + } + if err := blk.Verify(context.Background()); err != nil { + return nil, fmt.Errorf("failed verifying block: %w", err) + } + if err := blk.Accept(context.Background()); err != nil { + return nil, fmt.Errorf("failed accepting block: %w", err) + } + if err := vm.SetPreference(context.Background(), vm.manager.LastAccepted()); err != nil { + return nil, fmt.Errorf("failed setting preference: %w", err) + } + + // move time ahead, promoting the validator to current + currentTime := stakerTx.StartTime() + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + blk, err = vm.Builder.BuildBlock(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed building block: %w", err) + } + if err := blk.Verify(context.Background()); err != nil { + return nil, fmt.Errorf("failed verifying block: %w", err) + } + if err := blk.Accept(context.Background()); err != nil { + return nil, fmt.Errorf("failed accepting block: %w", err) + } + if err := vm.SetPreference(context.Background(), vm.manager.LastAccepted()); err != nil { + return nil, fmt.Errorf("failed setting preference: %w", err) + } + + return vm.state.GetCurrentValidator(stakerTx.SubnetID(), stakerTx.NodeID()) +} + +func terminateSubnetValidator(vm *VM, validator *state.Staker) error { + currentTime := validator.EndTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + blk, err := vm.Builder.BuildBlock(context.Background()) + if err != nil { + return fmt.Errorf("failed building block: %w", err) + } + if err := blk.Verify(context.Background()); err != nil { + return fmt.Errorf("failed verifying block: %w", err) + } + if err := blk.Accept(context.Background()); err != nil { + return fmt.Errorf("failed accepting block: %w", err) + } + if err := vm.SetPreference(context.Background(), vm.manager.LastAccepted()); err != nil { + return fmt.Errorf("failed setting preference: %w", err) + } + + return nil +} + +func terminatePrimaryValidator(vm *VM, validator *state.Staker) error { + currentTime := validator.EndTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + blk, err := vm.Builder.BuildBlock(context.Background()) + if err != nil { + return fmt.Errorf("failed building block: %w", err) + } + if err := blk.Verify(context.Background()); err != nil { + return fmt.Errorf("failed verifying block: %w", err) + } + + proposalBlk := blk.(snowman.OracleBlock) + options, err := proposalBlk.Options(context.Background()) + if err != nil { + return fmt.Errorf("failed retrieving options: %w", err) + } + + commit := options[0].(*blockexecutor.Block) + _, ok := commit.Block.(*blocks.BanffCommitBlock) + if !ok { + return fmt.Errorf("failed retrieving commit option: %w", err) + } + if err := blk.Accept(context.Background()); err != nil { + return fmt.Errorf("failed accepting block: %w", err) + } + + if err := commit.Verify(context.Background()); err != nil { + return fmt.Errorf("failed verifying commit block: %w", err) + } + if err := commit.Accept(context.Background()); err != nil { + return fmt.Errorf("failed accepting commit block: %w", err) + } + + if err := vm.SetPreference(context.Background(), vm.manager.LastAccepted()); err != nil { + return fmt.Errorf("failed setting preference: %w", err) + } + + return nil +} + +type validatorInputData struct { + eventType uint8 + startTime time.Time + endTime time.Time + nodeID ids.NodeID + publicKey *bls.PublicKey +} + +// buildTimestampsList creates validators start and end time, given the event list. +// output is returned as a list of state.Stakers, just because it's a convenient object to +// collect all relevant information. +func buildTimestampsList(events []uint8, currentTime time.Time, nodeID ids.NodeID) ([]*validatorInputData, error) { + res := make([]*validatorInputData, 0, len(events)) + + currentTime = currentTime.Add(executor.SyncBound) + switch endTime := currentTime.Add(defaultMinStakingDuration); events[0] { + case startPrimaryWithBLS: + sk, err := bls.NewSecretKey() + if err != nil { + return nil, fmt.Errorf("could not make private key: %w", err) + } + + res = append(res, &validatorInputData{ + eventType: startPrimaryWithBLS, + startTime: currentTime, + endTime: endTime, + nodeID: nodeID, + publicKey: bls.PublicFromSecretKey(sk), + }) + case startPrimaryWithoutBLS: + res = append(res, &validatorInputData{ + eventType: startPrimaryWithoutBLS, + startTime: currentTime, + endTime: endTime, + nodeID: nodeID, + publicKey: nil, + }) + default: + return nil, fmt.Errorf("unexpected initial event %d", events[0]) + } + + // track current primary validator to make sure its staking period + // covers all of its subnet validators + currentPrimaryVal := res[0] + for i := 1; i < len(events); i++ { + currentTime = currentTime.Add(executor.SyncBound) + + switch currentEvent := events[i]; currentEvent { + case startSubnetValidator: + endTime := currentTime.Add(defaultMinStakingDuration) + res = append(res, &validatorInputData{ + eventType: startSubnetValidator, + startTime: currentTime, + endTime: endTime, + nodeID: nodeID, + publicKey: nil, + }) + + currentPrimaryVal.endTime = endTime.Add(time.Second) + currentTime = endTime.Add(time.Second) + + case startPrimaryWithBLS: + currentTime = currentPrimaryVal.endTime.Add(executor.SyncBound) + sk, err := bls.NewSecretKey() + if err != nil { + return nil, fmt.Errorf("could not make private key: %w", err) + } + + endTime := currentTime.Add(defaultMinStakingDuration) + val := &validatorInputData{ + eventType: startPrimaryWithBLS, + startTime: currentTime, + endTime: endTime, + nodeID: nodeID, + publicKey: bls.PublicFromSecretKey(sk), + } + res = append(res, val) + currentPrimaryVal = val + + case startPrimaryWithoutBLS: + currentTime = currentPrimaryVal.endTime.Add(executor.SyncBound) + endTime := currentTime.Add(defaultMinStakingDuration) + val := &validatorInputData{ + eventType: startPrimaryWithoutBLS, + startTime: currentTime, + endTime: endTime, + nodeID: nodeID, + publicKey: nil, + } + res = append(res, val) + currentPrimaryVal = val + } + } + return res, nil +} + +func TestTimestampListGenerator(t *testing.T) { + properties := gopter.NewProperties(nil) + + properties.Property("primary validators are returned in sequence", prop.ForAll( + func(events []uint8) string { + currentTime := time.Now() + nodeID := ids.GenerateTestNodeID() + validatorsTimes, err := buildTimestampsList(events, currentTime, nodeID) + if err != nil { + return fmt.Sprintf("failed building events sequence: %s", err.Error()) + } + + if len(validatorsTimes) == 0 { + return errEmptyEventsList.Error() + } + + // nil out non subnet validators + subnetIndexes := make([]int, 0) + for idx, ev := range validatorsTimes { + if ev.eventType == startSubnetValidator { + subnetIndexes = append(subnetIndexes, idx) + } + } + for _, idx := range subnetIndexes { + validatorsTimes[idx] = nil + } + + currentEventTime := currentTime + for i, ev := range validatorsTimes { + if ev == nil { + continue // a subnet validator + } + if currentEventTime.After(ev.startTime) { + return fmt.Sprintf("validator %d start time larger than current event time", i) + } + + if ev.startTime.After(ev.endTime) { + return fmt.Sprintf("validator %d start time larger than its end time", i) + } + + currentEventTime = ev.endTime + } + + return "" + }, + gen.SliceOf(gen.OneConstOf( + startPrimaryWithBLS, + startPrimaryWithoutBLS, + startSubnetValidator, + )).SuchThat(func(v interface{}) bool { + list := v.([]uint8) + return len(list) > 0 && (list[0] == startPrimaryWithBLS || list[0] == startPrimaryWithoutBLS) + }), + )) + + properties.Property("subnet validators are returned in sequence", prop.ForAll( + func(events []uint8) string { + currentTime := time.Now() + nodeID := ids.GenerateTestNodeID() + validatorsTimes, err := buildTimestampsList(events, currentTime, nodeID) + if err != nil { + return fmt.Sprintf("failed building events sequence: %s", err.Error()) + } + + if len(validatorsTimes) == 0 { + return errEmptyEventsList.Error() + } + + // nil out non subnet validators + nonSubnetIndexes := make([]int, 0) + for idx, ev := range validatorsTimes { + if ev.eventType != startSubnetValidator { + nonSubnetIndexes = append(nonSubnetIndexes, idx) + } + } + for _, idx := range nonSubnetIndexes { + validatorsTimes[idx] = nil + } + + currentEventTime := currentTime + for i, ev := range validatorsTimes { + if ev == nil { + continue // a non-subnet validator + } + if currentEventTime.After(ev.startTime) { + return fmt.Sprintf("validator %d start time larger than current event time", i) + } + + if ev.startTime.After(ev.endTime) { + return fmt.Sprintf("validator %d start time larger than its end time", i) + } + + currentEventTime = ev.endTime + } + + return "" + }, + gen.SliceOf(gen.OneConstOf( + startPrimaryWithBLS, + startPrimaryWithoutBLS, + startSubnetValidator, + )).SuchThat(func(v interface{}) bool { + list := v.([]uint8) + return len(list) > 0 && (list[0] == startPrimaryWithBLS || list[0] == startPrimaryWithoutBLS) + }), + )) + + properties.Property("subnet validators' times are bound by a primary validator's times", prop.ForAll( + func(events []uint8) string { + currentTime := time.Now() + nodeID := ids.GenerateTestNodeID() + validatorsTimes, err := buildTimestampsList(events, currentTime, nodeID) + if err != nil { + return fmt.Sprintf("failed building events sequence: %s", err.Error()) + } + + if len(validatorsTimes) == 0 { + return errEmptyEventsList.Error() + } + + currentPrimaryValidator := validatorsTimes[0] + for i := 1; i < len(validatorsTimes); i++ { + if validatorsTimes[i].eventType != startSubnetValidator { + currentPrimaryValidator = validatorsTimes[i] + continue + } + + subnetVal := validatorsTimes[i] + if currentPrimaryValidator.startTime.After(subnetVal.startTime) || + subnetVal.endTime.After(currentPrimaryValidator.endTime) { + return "subnet validator not bounded by primary network ones" + } + } + return "" + }, + gen.SliceOf(gen.OneConstOf( + startPrimaryWithBLS, + startPrimaryWithoutBLS, + startSubnetValidator, + )).SuchThat(func(v interface{}) bool { + list := v.([]uint8) + return len(list) > 0 && (list[0] == startPrimaryWithBLS || list[0] == startPrimaryWithoutBLS) + }), + )) + + properties.TestingRun(t) +} + +// add a single validator at the end of times, +// to make sure it won't pollute our tests +func buildVM() (*VM, ids.ID, error) { + vdrs := validators.NewManager() + primaryVdrs := validators.NewSet() + _ = vdrs.Add(constants.PrimaryNetworkID, primaryVdrs) + + forkTime := defaultGenesisTime + vm := &VM{Config: config.Config{ + Chains: chains.TestManager, + UptimeLockedCalculator: uptime.NewLockedCalculator(), + SybilProtectionEnabled: true, + Validators: vdrs, + TxFee: defaultTxFee, + CreateSubnetTxFee: 100 * defaultTxFee, + TransformSubnetTxFee: 100 * defaultTxFee, + CreateBlockchainTxFee: 100 * defaultTxFee, + MinValidatorStake: defaultMinValidatorStake, + MaxValidatorStake: defaultMaxValidatorStake, + MinDelegatorStake: defaultMinDelegatorStake, + MinStakeDuration: defaultMinStakingDuration, + MaxStakeDuration: defaultMaxStakingDuration, + RewardConfig: defaultRewardConfig, + ApricotPhase3Time: forkTime, + ApricotPhase5Time: forkTime, + BanffTime: forkTime, + CortinaTime: forkTime, + }} + vm.clock.Set(forkTime.Add(time.Second)) + + baseDBManager := manager.NewMemDB(version.Semantic1_0_0) + chainDBManager := baseDBManager.NewPrefixDBManager([]byte{0}) + atomicDB := prefixdb.New([]byte{1}, baseDBManager.Current().Database) + + msgChan := make(chan common.Message, 1) + ctx := defaultContext() + + m := atomic.NewMemory(atomicDB) + ctx.SharedMemory = m.NewSharedMemory(ctx.ChainID) + + ctx.Lock.Lock() + defer ctx.Lock.Unlock() + appSender := &common.SenderTest{} + appSender.CantSendAppGossip = true + appSender.SendAppGossipF = func(context.Context, []byte) error { + return nil + } + + genesisBytes, err := buildCustomGenesis() + if err != nil { + return nil, ids.Empty, err + } + + err = vm.Initialize( + context.Background(), + ctx, + chainDBManager, + genesisBytes, + nil, + nil, + msgChan, + nil, + appSender, + ) + if err != nil { + return nil, ids.Empty, err + } + + err = vm.SetState(context.Background(), snow.NormalOp) + if err != nil { + return nil, ids.Empty, err + } + + // Create a subnet and store it in testSubnet1 + // Note: following Banff activation, block acceptance will move + // chain time ahead + testSubnet1, err = vm.txBuilder.NewCreateSubnetTx( + 1, // threshold + []ids.ShortID{keys[0].PublicKey().Address()}, + []*secp256k1.PrivateKey{keys[len(keys)-1]}, // pays tx fee + keys[0].PublicKey().Address(), // change addr + ) + if err != nil { + return nil, ids.Empty, err + } + if err := vm.Builder.AddUnverifiedTx(testSubnet1); err != nil { + return nil, ids.Empty, err + } + + blk, err := vm.Builder.BuildBlock(context.Background()) + if err != nil { + return nil, ids.Empty, err + } + if err := blk.Verify(context.Background()); err != nil { + return nil, ids.Empty, err + } + if err := blk.Accept(context.Background()); err != nil { + return nil, ids.Empty, err + } + if err := vm.SetPreference(context.Background(), vm.manager.LastAccepted()); err != nil { + return nil, ids.Empty, err + } + + return vm, testSubnet1.ID(), nil +} + +func buildCustomGenesis() ([]byte, error) { + genesisUTXOs := make([]api.UTXO, len(keys)) + for i, key := range keys { + id := key.PublicKey().Address() + addr, err := address.FormatBech32(constants.UnitTestHRP, id.Bytes()) + if err != nil { + return nil, err + } + genesisUTXOs[i] = api.UTXO{ + Amount: json.Uint64(defaultBalance), + Address: addr, + } + } + + // we need at least a validator, otherwise BuildBlock would fail, since it + // won't find next staker to promote/evict from stakers set. Contrary to + // what happens with production code we push such validator at the end of + // times, so to avoid interference with our tests + nodeID := ids.NodeID(keys[len(keys)-1].PublicKey().Address()) + addr, err := address.FormatBech32(constants.UnitTestHRP, nodeID.Bytes()) + if err != nil { + return nil, err + } + + starTime := mockable.MaxTime.Add(-1 * defaultMinStakingDuration) + endTime := mockable.MaxTime + genesisValidator := api.PermissionlessValidator{ + Staker: api.Staker{ + StartTime: json.Uint64(starTime.Unix()), + EndTime: json.Uint64(endTime.Unix()), + NodeID: nodeID, + }, + RewardOwner: &api.Owner{ + Threshold: 1, + Addresses: []string{addr}, + }, + Staked: []api.UTXO{{ + Amount: json.Uint64(defaultWeight), + Address: addr, + }}, + DelegationFee: reward.PercentDenominator, + } + + buildGenesisArgs := api.BuildGenesisArgs{ + Encoding: formatting.Hex, + NetworkID: json.Uint32(constants.UnitTestID), + AvaxAssetID: avaxAssetID, + UTXOs: genesisUTXOs, + Validators: []api.PermissionlessValidator{genesisValidator}, + Chains: nil, + Time: json.Uint64(defaultGenesisTime.Unix()), + InitialSupply: json.Uint64(360 * units.MegaAvax), + } + + buildGenesisResponse := api.BuildGenesisReply{} + platformvmSS := api.StaticService{} + if err := platformvmSS.BuildGenesis(nil, &buildGenesisArgs, &buildGenesisResponse); err != nil { + return nil, err + } + + genesisBytes, err := formatting.Decode(buildGenesisResponse.Encoding, buildGenesisResponse.Bytes) + if err != nil { + return nil, err + } + + return genesisBytes, nil +} diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index 7ded28c7c4e5..6105839869a2 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -131,7 +131,31 @@ func (m *manager) GetCurrentHeight(context.Context) (uint64, error) { return lastAccepted.Height(), nil } -func (m *manager) GetValidatorSet(ctx context.Context, height uint64, subnetID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { +func (m *manager) GetValidatorSet( + ctx context.Context, + height uint64, + subnetID ids.ID, +) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + lastAcceptedHeight, err := m.GetCurrentHeight(ctx) + if err != nil { + return nil, err + } + if lastAcceptedHeight < height { + return nil, database.ErrNotFound + } + + return m.getValidatorSetFrom(lastAcceptedHeight, height, subnetID) +} + +// getValidatorSetFrom fetches the validator set of [subnetID] at [targetHeight] +// or builds it starting from [currentHeight]. +// +// Invariant: [m.cfg.Validators] contains the validator set at [currentHeight]. +func (m *manager) getValidatorSetFrom( + currentHeight uint64, + targetHeight uint64, + subnetID ids.ID, +) (map[ids.NodeID]*validators.GetValidatorOutput, error) { validatorSetsCache, exists := m.caches[subnetID] if !exists { validatorSetsCache = &cache.LRU[uint64, map[ids.NodeID]*validators.GetValidatorOutput]{Size: validatorSetsCacheSize} @@ -141,122 +165,177 @@ func (m *manager) GetValidatorSet(ctx context.Context, height uint64, subnetID i } } - if validatorSet, ok := validatorSetsCache.Get(height); ok { + if validatorSet, ok := validatorSetsCache.Get(targetHeight); ok { m.metrics.IncValidatorSetsCached() return validatorSet, nil } - lastAcceptedHeight, err := m.GetCurrentHeight(ctx) + // get the start time to track metrics + startTime := m.clk.Time() + + var ( + validatorSet map[ids.NodeID]*validators.GetValidatorOutput + err error + ) + if subnetID == constants.PrimaryNetworkID { + validatorSet, err = m.makePrimaryNetworkValidatorSet(currentHeight, targetHeight) + } else { + validatorSet, err = m.makeSubnetValidatorSet(currentHeight, targetHeight, subnetID) + } if err != nil { return nil, err } - if lastAcceptedHeight < height { - return nil, database.ErrNotFound - } - // get the start time to track metrics - startTime := m.clk.Time() + // cache the validator set + validatorSetsCache.Put(targetHeight, validatorSet) - currentSubnetValidators, ok := m.cfg.Validators.Get(subnetID) - if !ok { - currentSubnetValidators = validators.NewSet() - if err := m.state.ValidatorSet(subnetID, currentSubnetValidators); err != nil { - return nil, err - } - } - currentPrimaryNetworkValidators, ok := m.cfg.Validators.Get(constants.PrimaryNetworkID) + endTime := m.clk.Time() + m.metrics.IncValidatorSetsCreated() + m.metrics.AddValidatorSetsDuration(endTime.Sub(startTime)) + m.metrics.AddValidatorSetsHeightDiff(currentHeight - targetHeight) + return validatorSet, nil +} + +func (m *manager) makePrimaryNetworkValidatorSet( + currentHeight uint64, + targetHeight uint64, +) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + currentValidators, ok := m.cfg.Validators.Get(constants.PrimaryNetworkID) if !ok { // This should never happen return nil, ErrMissingValidator } + currentValidatorList := currentValidators.List() - currentSubnetValidatorList := currentSubnetValidators.List() - vdrSet := make(map[ids.NodeID]*validators.GetValidatorOutput, len(currentSubnetValidatorList)) - for _, vdr := range currentSubnetValidatorList { - primaryVdr, ok := currentPrimaryNetworkValidators.Get(vdr.NodeID) - if !ok { - // This should never happen - return nil, fmt.Errorf("%w: %s", ErrMissingValidator, vdr.NodeID) - } - vdrSet[vdr.NodeID] = &validators.GetValidatorOutput{ + // Node ID --> Validator information for the node validating the Primary + // Network. + validatorSet := make(map[ids.NodeID]*validators.GetValidatorOutput, len(currentValidatorList)) + for _, vdr := range currentValidatorList { + validatorSet[vdr.NodeID] = &validators.GetValidatorOutput{ NodeID: vdr.NodeID, - PublicKey: primaryVdr.PublicKey, + PublicKey: vdr.PublicKey, Weight: vdr.Weight, } } - for diffHeight := lastAcceptedHeight; diffHeight > height; diffHeight-- { - err := m.applyValidatorDiffs(vdrSet, subnetID, diffHeight) + // Rebuild primary network validators at [height] + for diffHeight := currentHeight; diffHeight > targetHeight; diffHeight-- { + weightDiffs, err := m.state.GetValidatorWeightDiffs(diffHeight, constants.PlatformChainID) if err != nil { return nil, err } - } - - // cache the validator set - validatorSetsCache.Put(height, vdrSet) + for nodeID, weightDiff := range weightDiffs { + if err := applyWeightDiff(validatorSet, nodeID, weightDiff); err != nil { + return nil, err + } + } - endTime := m.clk.Time() - m.metrics.IncValidatorSetsCreated() - m.metrics.AddValidatorSetsDuration(endTime.Sub(startTime)) - m.metrics.AddValidatorSetsHeightDiff(lastAcceptedHeight - height) - return vdrSet, nil + pkDiffs, err := m.state.GetValidatorPublicKeyDiffs(diffHeight) + if err != nil { + return nil, err + } + for nodeID, pk := range pkDiffs { + if vdr, ok := validatorSet[nodeID]; ok { + // The validator's public key was removed at this block, so it + // was in the validator set before. + vdr.PublicKey = pk + } + } + } + return validatorSet, nil } -func (m *manager) applyValidatorDiffs( - vdrSet map[ids.NodeID]*validators.GetValidatorOutput, +func (m *manager) makeSubnetValidatorSet( + currentHeight uint64, + targetHeight uint64, subnetID ids.ID, - height uint64, -) error { - weightDiffs, err := m.state.GetValidatorWeightDiffs(height, subnetID) - if err != nil { - return err +) (map[ids.NodeID]*validators.GetValidatorOutput, error) { + currentValidators, ok := m.cfg.Validators.Get(subnetID) + if !ok { + currentValidators = validators.NewSet() + if err := m.state.ValidatorSet(subnetID, currentValidators); err != nil { + return nil, err + } + } + currentValidatorList := currentValidators.List() + + // Node ID --> Validator information for the node validating the Subnet. + subnetValidatorSet := make(map[ids.NodeID]*validators.GetValidatorOutput, len(currentValidatorList)) + for _, vdr := range currentValidatorList { + subnetValidatorSet[vdr.NodeID] = &validators.GetValidatorOutput{ + NodeID: vdr.NodeID, + // PublicKey will be picked from primary validators + Weight: vdr.Weight, + } } - for nodeID, weightDiff := range weightDiffs { - vdr, ok := vdrSet[nodeID] - if !ok { - // This node isn't in the current validator set. - vdr = &validators.GetValidatorOutput{ - NodeID: nodeID, - } - vdrSet[nodeID] = vdr + // Rebuild subnet validators at [targetHeight] + for diffHeight := currentHeight; diffHeight > targetHeight; diffHeight-- { + weightDiffs, err := m.state.GetValidatorWeightDiffs(diffHeight, subnetID) + if err != nil { + return nil, err } - // The weight of this node changed at this block. - if weightDiff.Decrease { - // The validator's weight was decreased at this block, so in the - // prior block it was higher. - vdr.Weight, err = math.Add64(vdr.Weight, weightDiff.Amount) - } else { - // The validator's weight was increased at this block, so in the - // prior block it was lower. - vdr.Weight, err = math.Sub(vdr.Weight, weightDiff.Amount) + for nodeID, weightDiff := range weightDiffs { + if err := applyWeightDiff(subnetValidatorSet, nodeID, weightDiff); err != nil { + return nil, err + } } - if err != nil { - return err + } + + // Get the public keys for all the validators at [targetHeight] + primarySet, err := m.getValidatorSetFrom(currentHeight, targetHeight, constants.PrimaryNetworkID) + if err != nil { + return nil, err + } + + // Update the subnet validator set to include the public keys at + // [targetHeight]. + for nodeID, subnetValidator := range subnetValidatorSet { + primaryValidator, ok := primarySet[nodeID] + if !ok { + // This should never happen + return nil, ErrMissingValidator } + subnetValidator.PublicKey = primaryValidator.PublicKey + } + + return subnetValidatorSet, nil +} - if vdr.Weight == 0 { - // The validator's weight was 0 before this block so - // they weren't in the validator set. - delete(vdrSet, nodeID) +func applyWeightDiff( + targetSet map[ids.NodeID]*validators.GetValidatorOutput, + nodeID ids.NodeID, + weightDiff *state.ValidatorWeightDiff, +) error { + vdr, ok := targetSet[nodeID] + if !ok { + // This node isn't in the current validator set. + vdr = &validators.GetValidatorOutput{ + NodeID: nodeID, } + targetSet[nodeID] = vdr } - pkDiffs, err := m.state.GetValidatorPublicKeyDiffs(height) + // The weight of this node changed at this block. + var err error + if weightDiff.Decrease { + // The validator's weight was decreased at this block, so in the + // prior block it was higher. + vdr.Weight, err = math.Add64(vdr.Weight, weightDiff.Amount) + } else { + // The validator's weight was increased at this block, so in the + // prior block it was lower. + vdr.Weight, err = math.Sub(vdr.Weight, weightDiff.Amount) + } if err != nil { return err } - for nodeID, pk := range pkDiffs { - // pkDiffs includes all primary network key diffs, if we are - // fetching a subnet's validator set, we should ignore non-subnet - // validators. - if vdr, ok := vdrSet[nodeID]; ok { - // The validator's public key was removed at this block, so it - // was in the validator set before. - vdr.PublicKey = pk - } + if vdr.Weight == 0 { + // The validator's weight was 0 before this block so + // they weren't in the validator set. + delete(targetSet, nodeID) } return nil } diff --git a/vms/platformvm/validators/manager_test.go b/vms/platformvm/validators/manager_test.go index 7f5d8975f427..84259df1c45f 100644 --- a/vms/platformvm/validators/manager_test.go +++ b/vms/platformvm/validators/manager_test.go @@ -14,6 +14,8 @@ import ( "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + "github.com/ava-labs/avalanchego/chains" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" @@ -31,7 +33,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state" ) -// AVAX asset ID in tests var defaultRewardConfig = reward.Config{ MaxConsumptionRate: .12 * reward.PercentDenominator, MinConsumptionRate: .10 * reward.PercentDenominator, @@ -42,16 +43,16 @@ var defaultRewardConfig = reward.Config{ func TestVM_GetValidatorSet(t *testing.T) { // Populate the validator set to use below var ( - numVdrs = 4 - vdrBaseWeight = uint64(1_000) - vdrs []*validators.Validator + numVdrs = 4 + vdrBaseWeight = uint64(1_000) + testValidators []*validators.Validator ) for i := 0; i < numVdrs; i++ { sk, err := bls.NewSecretKey() require.NoError(t, err) - vdrs = append(vdrs, &validators.Validator{ + testValidators = append(testValidators, &validators.Validator{ NodeID: ids.GenerateTestNodeID(), PublicKey: bls.PublicFromSecretKey(sk), Weight: vdrBaseWeight + uint64(i), @@ -65,14 +66,14 @@ func TestVM_GetValidatorSet(t *testing.T) { lastAcceptedHeight uint64 subnetID ids.ID // Validator sets at tip - currentPrimaryNetworkValidators []*validators.Validator + currentPrimaryNetworkValidators map[ids.NodeID]*validators.Validator currentSubnetValidators []*validators.Validator - // Diff at tip, block before tip, etc. - // This must have [lastAcceptedHeight] - [height] elements - weightDiffs []map[ids.NodeID]*state.ValidatorWeightDiff - // Diff at tip, block before tip, etc. - // This must have [lastAcceptedHeight] - [height] elements - pkDiffs []map[ids.NodeID]*bls.PublicKey + + // height --> nodeID --> weightDiff + weightDiffs map[uint64]map[ids.NodeID]*state.ValidatorWeightDiff + + // height --> nodeID --> pkDiff + pkDiffs map[uint64]map[ids.NodeID]*bls.PublicKey expectedVdrSet map[ids.NodeID]*validators.GetValidatorOutput expectedErr error } @@ -87,19 +88,20 @@ func TestVM_GetValidatorSet(t *testing.T) { }, { name: "at tip", + subnetID: constants.PrimaryNetworkID, height: 1, lastAcceptedHeight: 1, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), + currentPrimaryNetworkValidators: map[ids.NodeID]*validators.Validator{ + testValidators[0].NodeID: copyPrimaryValidator(testValidators[0]), }, currentSubnetValidators: []*validators.Validator{ - copySubnetValidator(vdrs[0]), + copySubnetValidator(testValidators[0]), }, expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight, + testValidators[0].NodeID: { + NodeID: testValidators[0].NodeID, + PublicKey: testValidators[0].PublicKey, + Weight: testValidators[0].Weight, }, }, expectedErr: nil, @@ -108,53 +110,53 @@ func TestVM_GetValidatorSet(t *testing.T) { name: "1 before tip", height: 2, lastAcceptedHeight: 3, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), + currentPrimaryNetworkValidators: map[ids.NodeID]*validators.Validator{ + testValidators[0].NodeID: copyPrimaryValidator(testValidators[0]), + testValidators[1].NodeID: copyPrimaryValidator(testValidators[1]), }, currentSubnetValidators: []*validators.Validator{ // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), + copySubnetValidator(testValidators[0]), + copySubnetValidator(testValidators[1]), }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { + weightDiffs: map[uint64]map[ids.NodeID]*state.ValidatorWeightDiff{ + 3: { // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, // and vdrs[2] left - vdrs[0].NodeID: { + testValidators[0].NodeID: { Decrease: true, Amount: 1, }, - vdrs[1].NodeID: { + testValidators[1].NodeID: { Decrease: false, Amount: 1, }, - vdrs[2].NodeID: { + testValidators[2].NodeID: { Decrease: true, - Amount: vdrs[2].Weight, + Amount: testValidators[2].Weight, }, }, }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - { - vdrs[2].NodeID: vdrs[2].PublicKey, + pkDiffs: map[uint64]map[ids.NodeID]*bls.PublicKey{ + 3: { + testValidators[2].NodeID: testValidators[2].PublicKey, }, }, expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 1, + testValidators[0].NodeID: { + NodeID: testValidators[0].NodeID, + PublicKey: testValidators[0].PublicKey, + Weight: testValidators[0].Weight + 1, }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 1, + testValidators[1].NodeID: { + NodeID: testValidators[1].NodeID, + PublicKey: testValidators[1].PublicKey, + Weight: testValidators[1].Weight - 1, }, - vdrs[2].NodeID: { - NodeID: vdrs[2].NodeID, - PublicKey: vdrs[2].PublicKey, - Weight: vdrs[2].Weight, + testValidators[2].NodeID: { + NodeID: testValidators[2].NodeID, + PublicKey: testValidators[2].PublicKey, + Weight: testValidators[2].Weight, }, }, expectedErr: nil, @@ -163,65 +165,65 @@ func TestVM_GetValidatorSet(t *testing.T) { name: "2 before tip", height: 3, lastAcceptedHeight: 5, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), + currentPrimaryNetworkValidators: map[ids.NodeID]*validators.Validator{ + testValidators[0].NodeID: copyPrimaryValidator(testValidators[0]), + testValidators[1].NodeID: copyPrimaryValidator(testValidators[1]), }, currentSubnetValidators: []*validators.Validator{ // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), + copySubnetValidator(testValidators[0]), + copySubnetValidator(testValidators[1]), }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { + weightDiffs: map[uint64]map[ids.NodeID]*state.ValidatorWeightDiff{ + 5: { // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, // and vdrs[2] left - vdrs[0].NodeID: { + testValidators[0].NodeID: { Decrease: true, Amount: 1, }, - vdrs[1].NodeID: { + testValidators[1].NodeID: { Decrease: false, Amount: 1, }, - vdrs[2].NodeID: { + testValidators[2].NodeID: { Decrease: true, - Amount: vdrs[2].Weight, + Amount: testValidators[2].Weight, }, }, - { + 4: { // At the block before tip vdrs[0] lost weight, vdrs[1] gained weight, // vdrs[2] joined - vdrs[0].NodeID: { + testValidators[0].NodeID: { Decrease: true, Amount: 1, }, - vdrs[1].NodeID: { + testValidators[1].NodeID: { Decrease: false, Amount: 1, }, - vdrs[2].NodeID: { + testValidators[2].NodeID: { Decrease: false, - Amount: vdrs[2].Weight, + Amount: testValidators[2].Weight, }, }, }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - { - vdrs[2].NodeID: vdrs[2].PublicKey, + pkDiffs: map[uint64]map[ids.NodeID]*bls.PublicKey{ + 5: { + testValidators[2].NodeID: testValidators[2].PublicKey, }, - {}, + 4: {}, }, expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 2, + testValidators[0].NodeID: { + NodeID: testValidators[0].NodeID, + PublicKey: testValidators[0].PublicKey, + Weight: testValidators[0].Weight + 2, }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 2, + testValidators[1].NodeID: { + NodeID: testValidators[1].NodeID, + PublicKey: testValidators[1].PublicKey, + Weight: testValidators[1].Weight - 2, }, }, expectedErr: nil, @@ -230,50 +232,50 @@ func TestVM_GetValidatorSet(t *testing.T) { name: "1 before tip; nil public key", height: 4, lastAcceptedHeight: 5, - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), + currentPrimaryNetworkValidators: map[ids.NodeID]*validators.Validator{ + testValidators[0].NodeID: copyPrimaryValidator(testValidators[0]), + testValidators[1].NodeID: copyPrimaryValidator(testValidators[1]), }, currentSubnetValidators: []*validators.Validator{ // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), + copySubnetValidator(testValidators[0]), + copySubnetValidator(testValidators[1]), }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { + weightDiffs: map[uint64]map[ids.NodeID]*state.ValidatorWeightDiff{ + 5: { // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, // and vdrs[2] left - vdrs[0].NodeID: { + testValidators[0].NodeID: { Decrease: true, Amount: 1, }, - vdrs[1].NodeID: { + testValidators[1].NodeID: { Decrease: false, Amount: 1, }, - vdrs[2].NodeID: { + testValidators[2].NodeID: { Decrease: true, - Amount: vdrs[2].Weight, + Amount: testValidators[2].Weight, }, }, }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - {}, + pkDiffs: map[uint64]map[ids.NodeID]*bls.PublicKey{ + 5: {}, }, expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 1, + testValidators[0].NodeID: { + NodeID: testValidators[0].NodeID, + PublicKey: testValidators[0].PublicKey, + Weight: testValidators[0].Weight + 1, }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 1, + testValidators[1].NodeID: { + NodeID: testValidators[1].NodeID, + PublicKey: testValidators[1].PublicKey, + Weight: testValidators[1].Weight - 1, }, - vdrs[2].NodeID: { - NodeID: vdrs[2].NodeID, - Weight: vdrs[2].Weight, + testValidators[2].NodeID: { + NodeID: testValidators[2].NodeID, + Weight: testValidators[2].Weight, }, }, expectedErr: nil, @@ -283,51 +285,51 @@ func TestVM_GetValidatorSet(t *testing.T) { height: 5, lastAcceptedHeight: 6, subnetID: ids.GenerateTestID(), - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), - copyPrimaryValidator(vdrs[1]), - copyPrimaryValidator(vdrs[3]), + currentPrimaryNetworkValidators: map[ids.NodeID]*validators.Validator{ + testValidators[0].NodeID: copyPrimaryValidator(testValidators[0]), + testValidators[1].NodeID: copyPrimaryValidator(testValidators[1]), + testValidators[3].NodeID: copyPrimaryValidator(testValidators[3]), }, currentSubnetValidators: []*validators.Validator{ // At tip we have these 2 validators - copySubnetValidator(vdrs[0]), - copySubnetValidator(vdrs[1]), + copySubnetValidator(testValidators[0]), + copySubnetValidator(testValidators[1]), }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - { + weightDiffs: map[uint64]map[ids.NodeID]*state.ValidatorWeightDiff{ + 6: { // At the tip block vdrs[0] lost weight, vdrs[1] gained weight, // and vdrs[2] left - vdrs[0].NodeID: { + testValidators[0].NodeID: { Decrease: true, Amount: 1, }, - vdrs[1].NodeID: { + testValidators[1].NodeID: { Decrease: false, Amount: 1, }, - vdrs[2].NodeID: { + testValidators[2].NodeID: { Decrease: true, - Amount: vdrs[2].Weight, + Amount: testValidators[2].Weight, }, }, }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - {}, + pkDiffs: map[uint64]map[ids.NodeID]*bls.PublicKey{ + 6: {}, }, expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight + 1, + testValidators[0].NodeID: { + NodeID: testValidators[0].NodeID, + PublicKey: testValidators[0].PublicKey, + Weight: testValidators[0].Weight + 1, }, - vdrs[1].NodeID: { - NodeID: vdrs[1].NodeID, - PublicKey: vdrs[1].PublicKey, - Weight: vdrs[1].Weight - 1, + testValidators[1].NodeID: { + NodeID: testValidators[1].NodeID, + PublicKey: testValidators[1].PublicKey, + Weight: testValidators[1].Weight - 1, }, - vdrs[2].NodeID: { - NodeID: vdrs[2].NodeID, - Weight: vdrs[2].Weight, + testValidators[2].NodeID: { + NodeID: testValidators[2].NodeID, + Weight: testValidators[2].Weight, }, }, expectedErr: nil, @@ -337,25 +339,25 @@ func TestVM_GetValidatorSet(t *testing.T) { height: 4, lastAcceptedHeight: 5, subnetID: ids.GenerateTestID(), - currentPrimaryNetworkValidators: []*validators.Validator{ - copyPrimaryValidator(vdrs[0]), + currentPrimaryNetworkValidators: map[ids.NodeID]*validators.Validator{ + testValidators[0].NodeID: copyPrimaryValidator(testValidators[0]), }, currentSubnetValidators: []*validators.Validator{ - copySubnetValidator(vdrs[0]), + copySubnetValidator(testValidators[0]), }, - weightDiffs: []map[ids.NodeID]*state.ValidatorWeightDiff{ - {}, + weightDiffs: map[uint64]map[ids.NodeID]*state.ValidatorWeightDiff{ + 5: {}, }, - pkDiffs: []map[ids.NodeID]*bls.PublicKey{ - { - vdrs[1].NodeID: vdrs[1].PublicKey, + pkDiffs: map[uint64]map[ids.NodeID]*bls.PublicKey{ + 5: { + testValidators[1].NodeID: testValidators[1].PublicKey, }, }, expectedVdrSet: map[ids.NodeID]*validators.GetValidatorOutput{ - vdrs[0].NodeID: { - NodeID: vdrs[0].NodeID, - PublicKey: vdrs[0].PublicKey, - Weight: vdrs[0].Weight, + testValidators[0].NodeID: { + NodeID: testValidators[0].NodeID, + PublicKey: testValidators[0].PublicKey, + Weight: testValidators[0].Weight, }, }, expectedErr: nil, @@ -387,26 +389,33 @@ func TestVM_GetValidatorSet(t *testing.T) { validatorssSet := NewManager(cfg, mockState, metrics, clk) // Mock the VM's validators - mockSubnetVdrSet := validators.NewMockSet(ctrl) - mockSubnetVdrSet.EXPECT().List().Return(tt.currentSubnetValidators).AnyTimes() - vdrs.EXPECT().Get(tt.subnetID).Return(mockSubnetVdrSet, true).AnyTimes() + mockPrimaryVdrSet := validators.NewMockSet(ctrl) + mockPrimaryVdrSet.EXPECT().List().Return(maps.Values(tt.currentPrimaryNetworkValidators)).AnyTimes() + vdrs.EXPECT().Get(constants.PrimaryNetworkID).Return(mockPrimaryVdrSet, true).AnyTimes() - mockPrimaryVdrSet := mockSubnetVdrSet + mockSubnetVdrSet := mockPrimaryVdrSet if tt.subnetID != constants.PrimaryNetworkID { - mockPrimaryVdrSet = validators.NewMockSet(ctrl) - vdrs.EXPECT().Get(constants.PrimaryNetworkID).Return(mockPrimaryVdrSet, true).AnyTimes() + mockSubnetVdrSet = validators.NewMockSet(ctrl) + mockSubnetVdrSet.EXPECT().List().Return(tt.currentSubnetValidators).AnyTimes() } - for _, vdr := range tt.currentPrimaryNetworkValidators { - mockPrimaryVdrSet.EXPECT().Get(vdr.NodeID).Return(vdr, true).AnyTimes() + vdrs.EXPECT().Get(tt.subnetID).Return(mockSubnetVdrSet, true).AnyTimes() + + for _, vdr := range testValidators { + _, current := tt.currentPrimaryNetworkValidators[vdr.NodeID] + if current { + mockPrimaryVdrSet.EXPECT().Get(vdr.NodeID).Return(vdr, true).AnyTimes() + } else { + mockPrimaryVdrSet.EXPECT().Get(vdr.NodeID).Return(nil, false).AnyTimes() + } } // Tell state what diffs to report - for _, weightDiff := range tt.weightDiffs { - mockState.EXPECT().GetValidatorWeightDiffs(gomock.Any(), gomock.Any()).Return(weightDiff, nil) + for height, weightDiff := range tt.weightDiffs { + mockState.EXPECT().GetValidatorWeightDiffs(height, gomock.Any()).Return(weightDiff, nil).AnyTimes() } - for _, pkDiff := range tt.pkDiffs { - mockState.EXPECT().GetValidatorPublicKeyDiffs(gomock.Any()).Return(pkDiff, nil) + for height, pkDiff := range tt.pkDiffs { + mockState.EXPECT().GetValidatorPublicKeyDiffs(height).Return(pkDiff, nil) } // Tell state last accepted block to report diff --git a/vms/platformvm/vm_regression_test.go b/vms/platformvm/vm_regression_test.go index 6a81de2d6348..16796acacb88 100644 --- a/vms/platformvm/vm_regression_test.go +++ b/vms/platformvm/vm_regression_test.go @@ -5,6 +5,7 @@ package platformvm import ( "context" + "errors" "testing" "time" @@ -25,6 +26,7 @@ import ( "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/version" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -32,10 +34,14 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/metrics" "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" + "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + blockexecutor "github.com/ava-labs/avalanchego/vms/platformvm/blocks/executor" ) func TestAddDelegatorTxOverDelegatedRegression(t *testing.T) { @@ -1442,3 +1448,693 @@ func TestRemovePermissionedValidatorDuringPendingToCurrentTransitionTracked(t *t require.NoError(removeSubnetValidatorBlock.Accept(context.Background())) require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) } + +// GetValidatorSet must return the BLS keys for a given validator correctly when +// queried at a previous height, even in case it has currently expired +func TestSubnetValidatorBLSKeyDiffAfterExpiry(t *testing.T) { + // setup + require := require.New(t) + vm, _, _ := defaultVM() + vm.ctx.Lock.Lock() + defer func() { + require.NoError(vm.Shutdown(context.Background())) + + vm.ctx.Lock.Unlock() + }() + subnetID := testSubnet1.TxID + + // setup time + currentTime := defaultGenesisTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + // A subnet validator stakes and then stops; also its primary network counterpart stops staking + var ( + primaryStartTime = currentTime.Add(executor.SyncBound) + subnetStartTime = primaryStartTime.Add(executor.SyncBound) + subnetEndTime = subnetStartTime.Add(defaultMinStakingDuration) + primaryEndTime = subnetEndTime.Add(time.Second) + primaryReStartTime = primaryEndTime.Add(executor.SyncBound) + primaryReEndTime = primaryReStartTime.Add(defaultMinStakingDuration) + ) + + // insert primary network validator + var ( + nodeID = ids.GenerateTestNodeID() + addr = keys[0].PublicKey().Address() + ) + sk1, err := bls.NewSecretKey() + require.NoError(err) + + // build primary network validator with BLS key + utxoHandler := utxo.NewHandler(vm.ctx, &vm.clock, vm.fx) + ins, unstakedOuts, stakedOuts, signers, err := utxoHandler.Spend( + vm.state, + keys, + vm.MinValidatorStake, + vm.Config.AddPrimaryNetworkValidatorFee, + addr, // change Addresss + ) + require.NoError(err) + + uPrimaryTx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: unstakedOuts, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime.Unix()), + End: uint64(primaryEndTime.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + Signer: signer.NewProofOfPossession(sk1), + StakeOuts: stakedOuts, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegationShares: reward.PercentDenominator, + } + primaryTx, err := txs.NewSigned(uPrimaryTx, txs.Codec, signers) + require.NoError(err) + require.NoError(primaryTx.SyntacticVerify(vm.ctx)) + + require.NoError(vm.Builder.AddUnverifiedTx(primaryTx)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting primary validator to current + currentTime = primaryStartTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.NoError(err) + + primaryStartHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // insert the subnet validator + subnetTx, err := vm.txBuilder.NewAddSubnetValidatorTx( + 1, // Weight + uint64(subnetStartTime.Unix()), // Start time + uint64(subnetEndTime.Unix()), // end time + nodeID, // Node ID + subnetID, + []*secp256k1.PrivateKey{keys[0], keys[1]}, + addr, + ) + require.NoError(err) + + require.NoError(vm.Builder.AddUnverifiedTx(subnetTx)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting the subnet validator to current + currentTime = subnetStartTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(subnetID, nodeID) + require.NoError(err) + + subnetStartHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // move time ahead, terminating the subnet validator + currentTime = subnetEndTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(subnetID, nodeID) + require.ErrorIs(err, database.ErrNotFound) + + subnetEndHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // move time ahead, terminating primary network validator + currentTime = primaryEndTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + blk, err := vm.Builder.BuildBlock(context.Background()) // must be a proposal block rewarding the primary validator + require.NoError(err) + require.NoError(blk.Verify(context.Background())) + + proposalBlk := blk.(snowman.OracleBlock) + options, err := proposalBlk.Options(context.Background()) + require.NoError(err) + + commit := options[0].(*blockexecutor.Block) + require.IsType(&blocks.BanffCommitBlock{}, commit.Block) + + require.NoError(blk.Accept(context.Background())) + require.NoError(commit.Verify(context.Background())) + require.NoError(commit.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.ErrorIs(err, database.ErrNotFound) + + primaryEndHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // reinsert primary validator with a different BLS key + sk2, err := bls.NewSecretKey() + require.NoError(err) + require.NotEqual(sk1, sk2) + + ins, unstakedOuts, stakedOuts, signers, err = utxoHandler.Spend( + vm.state, + keys, + vm.MinValidatorStake, + vm.Config.AddPrimaryNetworkValidatorFee, + addr, // change Addresss + ) + require.NoError(err) + + uPrimaryRestartTx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: unstakedOuts, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryReStartTime.Unix()), + End: uint64(primaryReEndTime.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + Signer: signer.NewProofOfPossession(sk2), + StakeOuts: stakedOuts, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegationShares: reward.PercentDenominator, + } + primaryRestartTx, err := txs.NewSigned(uPrimaryRestartTx, txs.Codec, signers) + require.NoError(err) + require.NoError(uPrimaryRestartTx.SyntacticVerify(vm.ctx)) + + require.NoError(vm.Builder.AddUnverifiedTx(primaryRestartTx)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting restarted primary validator to current + currentTime = primaryReStartTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.NoError(err) + + primaryRestartHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // Show that validators are rebuilt with the right BLS key + for height := primaryStartHeight; height < primaryEndHeight; height++ { + require.NoError(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + constants.PrimaryNetworkID, + height, + uPrimaryTx.Signer.Key()), + ) + } + for height := primaryEndHeight; height < primaryRestartHeight; height++ { + require.ErrorIs(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + constants.PrimaryNetworkID, + primaryEndHeight, + uPrimaryTx.Signer.Key()), + database.ErrNotFound, + ) + } + require.NoError(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + constants.PrimaryNetworkID, + primaryRestartHeight, + uPrimaryRestartTx.Signer.Key()), + ) + + for height := subnetStartHeight; height < subnetEndHeight; height++ { + require.NoError(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + subnetID, + height, + uPrimaryTx.Signer.Key()), + ) + } + + for height := subnetEndHeight; height <= primaryRestartHeight; height++ { + require.ErrorIs(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + subnetID, + primaryEndHeight, + uPrimaryTx.Signer.Key()), + database.ErrNotFound, + ) + } +} + +func TestPrimaryNetworkValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { + // A primary network validator has an empty BLS key. Then it restakes adding + // the BLS key. Querying the validator set back when BLS key was empty must + // return an empty BLS key. + + // setup + require := require.New(t) + vm, _, _ := defaultVM() + vm.ctx.Lock.Lock() + defer func() { + require.NoError(vm.Shutdown(context.Background())) + + vm.ctx.Lock.Unlock() + }() + + // setup time + currentTime := defaultGenesisTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + // A primary network validator stake twice + var ( + primaryStartTime1 = currentTime.Add(executor.SyncBound) + primaryEndTime1 = primaryStartTime1.Add(defaultMinStakingDuration) + primaryStartTime2 = primaryEndTime1.Add(executor.SyncBound) + primaryEndTime2 = primaryStartTime2.Add(defaultMinStakingDuration) + ) + + // Add a primary network validator with no BLS key + nodeID := ids.GenerateTestNodeID() + addr := keys[0].PublicKey().Address() + primaryTx1, err := vm.txBuilder.NewAddValidatorTx( + vm.MinValidatorStake, + uint64(primaryStartTime1.Unix()), + uint64(primaryEndTime1.Unix()), + nodeID, + addr, + reward.PercentDenominator, + []*secp256k1.PrivateKey{keys[0]}, + addr, + ) + require.NoError(err) + + require.NoError(vm.Builder.AddUnverifiedTx(primaryTx1)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting primary validator to current + currentTime = primaryStartTime1 + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.NoError(err) + + primaryStartHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // move time ahead, terminating primary network validator + currentTime = primaryEndTime1 + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + blk, err := vm.Builder.BuildBlock(context.Background()) // must be a proposal block rewarding the primary validator + require.NoError(err) + require.NoError(blk.Verify(context.Background())) + + proposalBlk := blk.(snowman.OracleBlock) + options, err := proposalBlk.Options(context.Background()) + require.NoError(err) + + commit := options[0].(*blockexecutor.Block) + require.IsType(&blocks.BanffCommitBlock{}, commit.Block) + + require.NoError(blk.Accept(context.Background())) + require.NoError(commit.Verify(context.Background())) + require.NoError(commit.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.ErrorIs(err, database.ErrNotFound) + + primaryEndHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // reinsert primary validator with a different BLS key + sk2, err := bls.NewSecretKey() + require.NoError(err) + + utxoHandler := utxo.NewHandler(vm.ctx, &vm.clock, vm.fx) + ins, unstakedOuts, stakedOuts, signers, err := utxoHandler.Spend( + vm.state, + keys, + vm.MinValidatorStake, + vm.Config.AddPrimaryNetworkValidatorFee, + addr, // change Addresss + ) + require.NoError(err) + + uPrimaryRestartTx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: unstakedOuts, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime2.Unix()), + End: uint64(primaryEndTime2.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + Signer: signer.NewProofOfPossession(sk2), + StakeOuts: stakedOuts, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegationShares: reward.PercentDenominator, + } + primaryRestartTx, err := txs.NewSigned(uPrimaryRestartTx, txs.Codec, signers) + require.NoError(err) + require.NoError(uPrimaryRestartTx.SyntacticVerify(vm.ctx)) + + require.NoError(vm.Builder.AddUnverifiedTx(primaryRestartTx)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting restarted primary validator to current + currentTime = primaryStartTime2 + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.NoError(err) + + emptySigner := &signer.Empty{} + for height := primaryStartHeight; height < primaryEndHeight; height++ { + require.NoError(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + constants.PrimaryNetworkID, + height, + emptySigner.Key()), + ) + } +} + +func TestSubnetValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { + // A primary network validator has an empty BLS key and a subnet validator. + // Primary network validator terminates its first staking cycle and it + // restakes adding the BLS key. Querying the validator set back when BLS key + // was empty must return an empty BLS key for the subnet validator + + // setup + require := require.New(t) + vm, _, _ := defaultVM() + vm.ctx.Lock.Lock() + defer func() { + require.NoError(vm.Shutdown(context.Background())) + + vm.ctx.Lock.Unlock() + }() + subnetID := testSubnet1.TxID + + // setup time + currentTime := defaultGenesisTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + // A primary network validator stake twice + var ( + primaryStartTime1 = currentTime.Add(executor.SyncBound) + subnetStartTime = primaryStartTime1.Add(executor.SyncBound) + subnetEndTime = subnetStartTime.Add(defaultMinStakingDuration) + primaryEndTime1 = subnetEndTime.Add(time.Second) + primaryStartTime2 = primaryEndTime1.Add(executor.SyncBound) + primaryEndTime2 = primaryStartTime2.Add(defaultMinStakingDuration) + ) + + // Add a primary network validator with no BLS key + nodeID := ids.GenerateTestNodeID() + addr := keys[0].PublicKey().Address() + primaryTx1, err := vm.txBuilder.NewAddValidatorTx( + vm.MinValidatorStake, + uint64(primaryStartTime1.Unix()), + uint64(primaryEndTime1.Unix()), + nodeID, + addr, + reward.PercentDenominator, + []*secp256k1.PrivateKey{keys[0]}, + addr, + ) + require.NoError(err) + + require.NoError(vm.Builder.AddUnverifiedTx(primaryTx1)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting primary validator to current + currentTime = primaryStartTime1 + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.NoError(err) + + primaryStartHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // insert the subnet validator + subnetTx, err := vm.txBuilder.NewAddSubnetValidatorTx( + 1, // Weight + uint64(subnetStartTime.Unix()), // Start time + uint64(subnetEndTime.Unix()), // end time + nodeID, // Node ID + subnetID, + []*secp256k1.PrivateKey{keys[0], keys[1]}, + addr, + ) + require.NoError(err) + + require.NoError(vm.Builder.AddUnverifiedTx(subnetTx)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting the subnet validator to current + currentTime = subnetStartTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(subnetID, nodeID) + require.NoError(err) + + subnetStartHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // move time ahead, terminating the subnet validator + currentTime = subnetEndTime + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + require.NoError(buildAndAcceptStandardBlock(vm)) + + _, err = vm.state.GetCurrentValidator(subnetID, nodeID) + require.ErrorIs(err, database.ErrNotFound) + + subnetEndHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // move time ahead, terminating primary network validator + currentTime = primaryEndTime1 + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + blk, err := vm.Builder.BuildBlock(context.Background()) // must be a proposal block rewarding the primary validator + require.NoError(err) + require.NoError(blk.Verify(context.Background())) + + proposalBlk := blk.(snowman.OracleBlock) + options, err := proposalBlk.Options(context.Background()) + require.NoError(err) + + commit := options[0].(*blockexecutor.Block) + require.IsType(&blocks.BanffCommitBlock{}, commit.Block) + + require.NoError(blk.Accept(context.Background())) + require.NoError(commit.Verify(context.Background())) + require.NoError(commit.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.ErrorIs(err, database.ErrNotFound) + + primaryEndHeight, err := vm.GetCurrentHeight(context.Background()) + require.NoError(err) + + // reinsert primary validator with a different BLS key + sk2, err := bls.NewSecretKey() + require.NoError(err) + + utxoHandler := utxo.NewHandler(vm.ctx, &vm.clock, vm.fx) + ins, unstakedOuts, stakedOuts, signers, err := utxoHandler.Spend( + vm.state, + keys, + vm.MinValidatorStake, + vm.Config.AddPrimaryNetworkValidatorFee, + addr, // change Addresss + ) + require.NoError(err) + + uPrimaryRestartTx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Ins: ins, + Outs: unstakedOuts, + }}, + Validator: txs.Validator{ + NodeID: nodeID, + Start: uint64(primaryStartTime2.Unix()), + End: uint64(primaryEndTime2.Unix()), + Wght: vm.MinValidatorStake, + }, + Subnet: constants.PrimaryNetworkID, + Signer: signer.NewProofOfPossession(sk2), + StakeOuts: stakedOuts, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + DelegationShares: reward.PercentDenominator, + } + primaryRestartTx, err := txs.NewSigned(uPrimaryRestartTx, txs.Codec, signers) + require.NoError(err) + require.NoError(uPrimaryRestartTx.SyntacticVerify(vm.ctx)) + + require.NoError(vm.Builder.AddUnverifiedTx(primaryRestartTx)) + require.NoError(buildAndAcceptStandardBlock(vm)) + + // move time ahead, promoting restarted primary validator to current + currentTime = primaryStartTime2 + vm.clock.Set(currentTime) + vm.state.SetTimestamp(currentTime) + + require.NoError(buildAndAcceptStandardBlock(vm)) + _, err = vm.state.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + require.NoError(err) + + emptySigner := &signer.Empty{} + for height := primaryStartHeight; height < primaryEndHeight; height++ { + require.NoError(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + constants.PrimaryNetworkID, + height, + emptySigner.Key()), + ) + } + for height := subnetStartHeight; height < subnetEndHeight; height++ { + require.NoError(checkValidatorBlsKeyIsSet( + vm.State, + nodeID, + subnetID, + height, + emptySigner.Key()), + ) + } +} + +func buildAndAcceptStandardBlock(vm *VM) error { + blk, err := vm.Builder.BuildBlock(context.Background()) + if err != nil { + return err + } + + if err := blk.Verify(context.Background()); err != nil { + return err + } + + if err := blk.Accept(context.Background()); err != nil { + return err + } + + return vm.SetPreference(context.Background(), vm.manager.LastAccepted()) +} + +func checkValidatorBlsKeyIsSet( + valState validators.State, + nodeID ids.NodeID, + subnetID ids.ID, + height uint64, + expectedBlsKey *bls.PublicKey, +) error { + vals, err := valState.GetValidatorSet(context.Background(), height, subnetID) + if err != nil { + return err + } + + val, found := vals[nodeID] + if !found { + return database.ErrNotFound + } + if val.PublicKey != expectedBlsKey { + return errors.New("unexpected BLS key") + } + + return nil +}