diff --git a/vms/platformvm/api/static_service.go b/vms/platformvm/api/static_service.go index d06c2e757962..8921a5ec7bae 100644 --- a/vms/platformvm/api/static_service.go +++ b/vms/platformvm/api/static_service.go @@ -89,9 +89,11 @@ func (utxo UTXO) Less(other UTXO) bool { type Staker struct { TxID ids.ID `json:"txID"` StartTime json.Uint64 `json:"startTime"` - EndTime json.Uint64 `json:"endTime"` - Weight json.Uint64 `json:"weight"` - NodeID ids.NodeID `json:"nodeID"` + // TODO ABENEGIA: consider adding Duration here too, since + // endTime may be +infinity for continuous stakers + EndTime json.Uint64 `json:"endTime"` + Weight json.Uint64 `json:"weight"` + NodeID ids.NodeID `json:"nodeID"` // Deprecated: Use Weight instead // TODO: remove [StakeAmount] after enough time for dependencies to update diff --git a/vms/platformvm/blocks/executor/helpers_test.go b/vms/platformvm/blocks/executor/helpers_test.go index 8013e6463428..e20dd3e54f14 100644 --- a/vms/platformvm/blocks/executor/helpers_test.go +++ b/vms/platformvm/blocks/executor/helpers_test.go @@ -99,9 +99,10 @@ var ( type stakerStatus uint type staker struct { - nodeID ids.NodeID - rewardAddress ids.ShortID - startTime, endTime time.Time + nodeID ids.NodeID + rewardAddress ids.ShortID + startTime time.Time + stakingPeriod time.Duration } type test struct { diff --git a/vms/platformvm/blocks/executor/proposal_block_test.go b/vms/platformvm/blocks/executor/proposal_block_test.go index 43d3b935c8d3..c6a7ff8e93fc 100644 --- a/vms/platformvm/blocks/executor/proposal_block_test.go +++ b/vms/platformvm/blocks/executor/proposal_block_test.go @@ -98,6 +98,7 @@ func TestApricotProposalBlockTimeVerification(t *testing.T) { SubnetID: utx.SubnetID(), StartTime: utx.StartTime(), EndTime: chainTime, + NextTime: chainTime, }) currentStakersIt.EXPECT().Release() onParentAccept.EXPECT().GetCurrentStakerIterator().Return(currentStakersIt, nil) @@ -107,9 +108,11 @@ func TestApricotProposalBlockTimeVerification(t *testing.T) { SubnetID: utx.SubnetID(), StartTime: utx.StartTime(), EndTime: chainTime, + NextTime: chainTime, }, nil) onParentAccept.EXPECT().GetTx(addValTx.ID()).Return(addValTx, status.Committed, nil) onParentAccept.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID).Return(uint64(1000), nil).AnyTimes() + onParentAccept.EXPECT().GetRewardUTXOs(gomock.Any()).Return([]*avax.UTXO{}, nil).AnyTimes() onParentAccept.EXPECT().GetDelegateeReward(constants.PrimaryNetworkID, utx.NodeID()).Return(uint64(0), nil).AnyTimes() onParentAccept.EXPECT().GetRewardConfig(gomock.Any()).Return(env.config.RewardConfig, nil).AnyTimes() @@ -150,7 +153,7 @@ func TestBanffProposalBlockTimeVerification(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - env := newEnvironment(t, ctrl, ContinuousStakingFork) + env := newEnvironment(t, ctrl, BanffFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -234,6 +237,7 @@ func TestBanffProposalBlockTimeVerification(t *testing.T) { currentStakersIt.EXPECT().Release().AnyTimes() onParentAccept.EXPECT().GetCurrentStakerIterator().Return(currentStakersIt, nil).AnyTimes() + onParentAccept.EXPECT().GetRewardUTXOs(gomock.Any()).Return([]*avax.UTXO{}, nil).AnyTimes() onParentAccept.EXPECT().GetDelegateeReward(constants.PrimaryNetworkID, unsignedNextStakerTx.NodeID()).Return(uint64(0), nil).AnyTimes() pendingStakersIt := state.NewMockStakerIterator(ctrl) @@ -401,7 +405,7 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { nodeID: ids.NodeID(staker0RewardAddress), rewardAddress: staker0RewardAddress, startTime: defaultGenesisTime, - endTime: time.Time{}, // actual endTime depends on specific test + stakingPeriod: txs.StakerMaxDuration, } staker1RewardAddress := ids.GenerateTestShortID() @@ -409,7 +413,7 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { nodeID: ids.NodeID(staker1RewardAddress), rewardAddress: staker1RewardAddress, startTime: defaultGenesisTime.Add(1 * time.Minute), - endTime: defaultGenesisTime.Add(10 * defaultMinStakingDuration).Add(1 * time.Minute), + stakingPeriod: 10*defaultMinStakingDuration + time.Minute, } staker2RewardAddress := ids.ShortID{1} @@ -417,7 +421,7 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { nodeID: ids.NodeID(staker2RewardAddress), rewardAddress: staker2RewardAddress, startTime: staker1.startTime.Add(1 * time.Minute), - endTime: staker1.startTime.Add(1 * time.Minute).Add(defaultMinStakingDuration), + stakingPeriod: defaultMinStakingDuration + time.Minute, } staker3RewardAddress := ids.GenerateTestShortID() @@ -425,14 +429,14 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { nodeID: ids.NodeID(staker3RewardAddress), rewardAddress: staker3RewardAddress, startTime: staker2.startTime.Add(1 * time.Minute), - endTime: staker2.endTime.Add(1 * time.Minute), + stakingPeriod: staker2.stakingPeriod, } staker3Sub := staker{ nodeID: staker3.nodeID, rewardAddress: staker3.rewardAddress, startTime: staker3.startTime.Add(1 * time.Minute), - endTime: staker3.endTime.Add(-1 * time.Minute), + stakingPeriod: staker3.stakingPeriod - time.Minute, } staker4RewardAddress := ids.GenerateTestShortID() @@ -440,15 +444,15 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { nodeID: ids.NodeID(staker4RewardAddress), rewardAddress: staker4RewardAddress, startTime: staker3.startTime, - endTime: staker3.endTime, + stakingPeriod: staker3.stakingPeriod, } staker5RewardAddress := ids.GenerateTestShortID() staker5 := staker{ nodeID: ids.NodeID(staker5RewardAddress), rewardAddress: staker5RewardAddress, - startTime: staker2.endTime, - endTime: staker2.endTime.Add(defaultMinStakingDuration), + startTime: staker2.startTime.Add(staker2.stakingPeriod), + stakingPeriod: defaultMinStakingDuration, } tests := []test{ @@ -564,7 +568,7 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { require := require.New(t) - env := newEnvironment(t, nil, ContinuousStakingFork) + env := newEnvironment(t, nil, CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -574,10 +578,12 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { env.config.Validators.Add(subnetID, validators.NewSet()) for _, staker := range test.stakers { + start := staker.startTime.Unix() + end := staker.startTime.Add(staker.stakingPeriod).Unix() tx, err := env.txBuilder.NewAddValidatorTx( env.config.MinValidatorStake, - uint64(staker.startTime.Unix()), - uint64(staker.endTime.Unix()), + uint64(start), + uint64(end), staker.nodeID, staker.rewardAddress, reward.PercentDenominator, @@ -598,10 +604,12 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { } for _, subStaker := range test.subnetStakers { + start := subStaker.startTime.Unix() + end := subStaker.startTime.Add(subStaker.stakingPeriod).Unix() tx, err := env.txBuilder.NewAddSubnetValidatorTx( 10, // Weight - uint64(subStaker.startTime.Unix()), - uint64(subStaker.endTime.Unix()), + uint64(start), + uint64(end), subStaker.nodeID, // validator ID subnetID, // Subnet ID []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, @@ -625,11 +633,11 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { // add Staker0 (with the right end time) to state // so to allow proposalBlk issuance - staker0.endTime = newTime + staker0.stakingPeriod = newTime.Sub(staker0.startTime) addStaker0, err := env.txBuilder.NewAddValidatorTx( 10, uint64(staker0.startTime.Unix()), - uint64(staker0.endTime.Unix()), + uint64(staker0.startTime.Add(staker0.stakingPeriod).Unix()), staker0.nodeID, staker0.rewardAddress, reward.PercentDenominator, @@ -712,7 +720,7 @@ func TestBanffProposalBlockUpdateStakers(t *testing.T) { func TestBanffProposalBlockRemoveSubnetValidator(t *testing.T) { require := require.New(t) - env := newEnvironment(t, nil, ContinuousStakingFork) + env := newEnvironment(t, nil, CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -853,7 +861,7 @@ func TestBanffProposalBlockTrackedSubnet(t *testing.T) { for _, tracked := range []bool{true, false} { t.Run(fmt.Sprintf("tracked %t", tracked), func(ts *testing.T) { require := require.New(t) - env := newEnvironment(t, nil, ContinuousStakingFork) + env := newEnvironment(t, nil, CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -958,7 +966,7 @@ func TestBanffProposalBlockTrackedSubnet(t *testing.T) { func TestBanffProposalBlockDelegatorStakerWeight(t *testing.T) { require := require.New(t) - env := newEnvironment(t, nil, ContinuousStakingFork) + env := newEnvironment(t, nil, CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -1142,7 +1150,7 @@ func TestBanffProposalBlockDelegatorStakerWeight(t *testing.T) { func TestBanffProposalBlockDelegatorStakers(t *testing.T) { require := require.New(t) - env := newEnvironment(t, nil, ContinuousStakingFork) + env := newEnvironment(t, nil, CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() diff --git a/vms/platformvm/blocks/executor/standard_block_test.go b/vms/platformvm/blocks/executor/standard_block_test.go index 9dbd92bab7a0..e3746c955ab0 100644 --- a/vms/platformvm/blocks/executor/standard_block_test.go +++ b/vms/platformvm/blocks/executor/standard_block_test.go @@ -369,37 +369,37 @@ func TestBanffStandardBlockUpdateStakers(t *testing.T) { nodeID: ids.GenerateTestNodeID(), rewardAddress: ids.GenerateTestShortID(), startTime: defaultGenesisTime.Add(1 * time.Minute), - endTime: defaultGenesisTime.Add(10 * defaultMinStakingDuration).Add(1 * time.Minute), + stakingPeriod: 10*defaultMinStakingDuration + time.Minute, } staker2 := staker{ nodeID: ids.GenerateTestNodeID(), rewardAddress: ids.GenerateTestShortID(), startTime: staker1.startTime.Add(1 * time.Minute), - endTime: staker1.startTime.Add(1 * time.Minute).Add(defaultMinStakingDuration), + stakingPeriod: defaultMinStakingDuration + time.Minute, } staker3 := staker{ nodeID: ids.GenerateTestNodeID(), rewardAddress: ids.GenerateTestShortID(), startTime: staker2.startTime.Add(1 * time.Minute), - endTime: staker2.endTime.Add(1 * time.Minute), + stakingPeriod: staker2.stakingPeriod, } staker3Sub := staker{ nodeID: staker3.nodeID, rewardAddress: ids.GenerateTestShortID(), startTime: staker3.startTime.Add(1 * time.Minute), - endTime: staker3.endTime.Add(-1 * time.Minute), + stakingPeriod: staker3.stakingPeriod - time.Minute, } staker4 := staker{ nodeID: ids.GenerateTestNodeID(), rewardAddress: ids.GenerateTestShortID(), startTime: staker3.startTime, - endTime: staker3.endTime, + stakingPeriod: staker3.stakingPeriod, } staker5 := staker{ nodeID: ids.GenerateTestNodeID(), rewardAddress: ids.GenerateTestShortID(), - startTime: staker2.endTime, - endTime: staker2.endTime.Add(defaultMinStakingDuration), + startTime: staker2.startTime.Add(staker2.stakingPeriod), + stakingPeriod: defaultMinStakingDuration, } tests := []test{ @@ -492,7 +492,7 @@ func TestBanffStandardBlockUpdateStakers(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { require := require.New(t) - env := newEnvironment(t, nil, ContinuousStakingFork) + env := newEnvironment(t, nil, CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -502,10 +502,12 @@ func TestBanffStandardBlockUpdateStakers(t *testing.T) { env.config.Validators.Add(subnetID, validators.NewSet()) for _, staker := range test.stakers { + start := staker.startTime + end := staker.startTime.Add(staker.stakingPeriod) _, err := addPendingValidator( env, - staker.startTime, - staker.endTime, + start, + end, staker.nodeID, staker.rewardAddress, []*secp256k1.PrivateKey{preFundedKeys[0]}, @@ -514,10 +516,12 @@ func TestBanffStandardBlockUpdateStakers(t *testing.T) { } for _, staker := range test.subnetStakers { + start := staker.startTime + end := staker.startTime.Add(staker.stakingPeriod) tx, err := env.txBuilder.NewAddSubnetValidatorTx( 10, // Weight - uint64(staker.startTime.Unix()), - uint64(staker.endTime.Unix()), + uint64(start.Unix()), + uint64(end.Unix()), staker.nodeID, // validator ID subnetID, // Subnet ID []*secp256k1.PrivateKey{preFundedKeys[0], preFundedKeys[1]}, diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index 118f1156e677..b37f238bbc37 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -27,7 +27,8 @@ type txMetrics struct { numRemoveSubnetValidatorTxs, numTransformSubnetTxs, numAddPermissionlessValidatorTxs, - numAddPermissionlessDelegatorTxs prometheus.Counter + numAddPermissionlessDelegatorTxs, + numStopStakerTxs prometheus.Counter } func newTxMetrics( @@ -49,6 +50,7 @@ func newTxMetrics( numTransformSubnetTxs: newTxMetric(namespace, "transform_subnet", registerer, &errs), numAddPermissionlessValidatorTxs: newTxMetric(namespace, "add_permissionless_validator", registerer, &errs), numAddPermissionlessDelegatorTxs: newTxMetric(namespace, "add_permissionless_delegator", registerer, &errs), + numStopStakerTxs: newTxMetric(namespace, "stop_staker_tx", registerer, &errs), } return m, errs.Err } @@ -132,3 +134,8 @@ func (m *txMetrics) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegator m.numAddPermissionlessDelegatorTxs.Inc() return nil } + +func (m *txMetrics) StopStakerTx(*txs.StopStakerTx) error { + m.numStopStakerTxs.Inc() + return nil +} diff --git a/vms/platformvm/state/masked_iterator_test.go b/vms/platformvm/state/masked_iterator_test.go index 29cc2f4c406e..ff40d166408c 100644 --- a/vms/platformvm/state/masked_iterator_test.go +++ b/vms/platformvm/state/masked_iterator_test.go @@ -233,7 +233,7 @@ func buildMaskedIterator(parentStakers []Staker, deletedIndexes []int, updatedIn s := parentStakers[idx] cpy := s - RotateStakerTimesInPlace(&cpy) + ShiftStakerAheadInPlace(&cpy) updatedStakers[s.TxID] = &cpy } diff --git a/vms/platformvm/state/models/stakers_generator_test.go b/vms/platformvm/state/models/stakers_generator_test.go deleted file mode 100644 index ca4da550c7ec..000000000000 --- a/vms/platformvm/state/models/stakers_generator_test.go +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package models - -import ( - "fmt" - "reflect" - "testing" - "time" - - blst "github.com/supranational/blst/bindings/go" - - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/vms/platformvm/state" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/leanovate/gopter" - "github.com/leanovate/gopter/gen" - "github.com/leanovate/gopter/prop" -) - -// stakerGenerator helps creating random yet reproducible state.Staker objects, which -// can be used in our property tests. -// stakerGenerator takes care of enforcing some state.Staker invariants on each and every random sample. -// TestGeneratedStakersValidity documents and verifies the enforced invariants. -func stakerGenerator(prio priorityType, subnet *ids.ID, nodeID *ids.NodeID) gopter.Gen { - return genStakerTimeData(prio).FlatMap( - func(v interface{}) gopter.Gen { - macro := v.(stakerTimeData) - - genStakerSubnetID := genID - genStakerNodeID := genNodeID - if subnet != nil { - genStakerSubnetID = gen.Const(*subnet) - } - if nodeID != nil { - genStakerNodeID = gen.Const(*nodeID) - } - - return gen.Struct(reflect.TypeOf(state.Staker{}), map[string]gopter.Gen{ - "TxID": genID, - "NodeID": genStakerNodeID, - "PublicKey": genBlsKey, - "SubnetID": genStakerSubnetID, - "Weight": gen.UInt64(), - "StartTime": gen.Const(macro.StartTime), - "EndTime": gen.Const(macro.EndTime), - "PotentialReward": gen.UInt64(), - "NextTime": gen.Const(macro.NextTime), - "Priority": gen.Const(macro.Priority), - }) - }, - reflect.TypeOf(stakerTimeData{}), - ) -} - -func TestGeneratedStakersValidity(t *testing.T) { - properties := gopter.NewProperties(nil) - - properties.Property("EndTime never before StartTime", prop.ForAll( - func(s state.Staker) string { - if s.EndTime.Before(s.StartTime) { - return fmt.Sprintf("startTime %v not before endTime %v, staker %v", - s.StartTime, s.EndTime, s) - } - return "" - }, - stakerGenerator(anyPriority, nil, nil), - )) - - properties.Property("NextTime coherent with priority", prop.ForAll( - func(s state.Staker) string { - switch p := s.Priority; p { - case txs.PrimaryNetworkDelegatorApricotPendingPriority, - txs.PrimaryNetworkDelegatorBanffPendingPriority, - txs.SubnetPermissionlessDelegatorPendingPriority, - txs.PrimaryNetworkValidatorPendingPriority, - txs.SubnetPermissionlessValidatorPendingPriority, - txs.SubnetPermissionedValidatorPendingPriority: - if !s.NextTime.Equal(s.StartTime) { - return fmt.Sprintf("pending staker has nextTime %v different from startTime %v, staker %v", - s.NextTime, s.StartTime, s) - } - return "" - - case txs.PrimaryNetworkDelegatorCurrentPriority, - txs.SubnetPermissionlessDelegatorCurrentPriority, - txs.PrimaryNetworkValidatorCurrentPriority, - txs.SubnetPermissionlessValidatorCurrentPriority, - txs.SubnetPermissionedValidatorCurrentPriority: - if !s.NextTime.Equal(s.EndTime) { - return fmt.Sprintf("current staker has nextTime %v different from endTime %v, staker %v", - s.NextTime, s.EndTime, s) - } - return "" - - default: - return fmt.Sprintf("priority %v unhandled in test", p) - } - }, - stakerGenerator(anyPriority, nil, nil), - )) - - subnetID := ids.GenerateTestID() - nodeID := ids.GenerateTestNodeID() - properties.Property("subnetID and nodeID set as specified", prop.ForAll( - func(s state.Staker) string { - if s.SubnetID != subnetID { - return fmt.Sprintf("unexpected subnetID, expected %v, got %v", - subnetID, s.SubnetID) - } - if s.NodeID != nodeID { - return fmt.Sprintf("unexpected nodeID, expected %v, got %v", - nodeID, s.NodeID) - } - return "" - }, - stakerGenerator(anyPriority, &subnetID, &nodeID), - )) - - properties.TestingRun(t) -} - -// stakerTimeData holds Staker's time related data in order to generate them -// while fullfilling the following constrains: -// 1. EndTime >= StartTime -// 2. NextTime == EndTime for current priorities -// 3. NextTime == StartTime for pending priorities -type stakerTimeData struct { - StartTime time.Time - EndTime time.Time - Priority txs.Priority - NextTime time.Time -} - -func genStakerTimeData(prio priorityType) gopter.Gen { - return genStakerMicroData(prio).FlatMap( - func(v interface{}) gopter.Gen { - micro := v.(stakerMicroData) - - var ( - startTime = micro.StartTime - endTime = micro.StartTime.Add(time.Duration(micro.Duration * int64(time.Hour))) - priority = micro.Priority - ) - - startTimeGen := gen.Const(startTime) - endTimeGen := gen.Const(endTime) - priorityGen := gen.Const(priority) - var nextTimeGen gopter.Gen - if priority == txs.SubnetPermissionedValidatorCurrentPriority || - priority == txs.SubnetPermissionlessDelegatorCurrentPriority || - priority == txs.SubnetPermissionlessValidatorCurrentPriority || - priority == txs.PrimaryNetworkDelegatorCurrentPriority || - priority == txs.PrimaryNetworkValidatorCurrentPriority { - nextTimeGen = gen.Const(endTime) - } else { - nextTimeGen = gen.Const(startTime) - } - - return gen.Struct(reflect.TypeOf(stakerTimeData{}), map[string]gopter.Gen{ - "StartTime": startTimeGen, - "EndTime": endTimeGen, - "Priority": priorityGen, - "NextTime": nextTimeGen, - }) - }, - reflect.TypeOf(stakerMicroData{}), - ) -} - -// stakerMicroData holds seed attributes to generate stakerMacroData -type stakerMicroData struct { - StartTime time.Time - Duration int64 - Priority txs.Priority -} - -// genStakerMicroData is the helper to generate stakerMicroData -func genStakerMicroData(prio priorityType) gopter.Gen { - return gen.Struct(reflect.TypeOf(&stakerMicroData{}), map[string]gopter.Gen{ - "StartTime": gen.Time(), - "Duration": gen.Int64Range(1, 365*24), - "Priority": genPriority(prio), - }) -} - -type priorityType uint8 - -const ( - anyPriority priorityType = iota - currentValidator - currentDelegator - pendingValidator - pendingDelegator -) - -func genPriority(p priorityType) gopter.Gen { - switch p { - case anyPriority: - return gen.OneConstOf( - txs.PrimaryNetworkDelegatorApricotPendingPriority, - txs.PrimaryNetworkValidatorPendingPriority, - txs.PrimaryNetworkDelegatorBanffPendingPriority, - txs.SubnetPermissionlessValidatorPendingPriority, - txs.SubnetPermissionlessDelegatorPendingPriority, - txs.SubnetPermissionedValidatorPendingPriority, - txs.SubnetPermissionedValidatorCurrentPriority, - txs.SubnetPermissionlessDelegatorCurrentPriority, - txs.SubnetPermissionlessValidatorCurrentPriority, - txs.PrimaryNetworkDelegatorCurrentPriority, - txs.PrimaryNetworkValidatorCurrentPriority, - ) - case currentValidator: - return gen.OneConstOf( - txs.SubnetPermissionedValidatorCurrentPriority, - txs.SubnetPermissionlessValidatorCurrentPriority, - txs.PrimaryNetworkValidatorCurrentPriority, - ) - case currentDelegator: - return gen.OneConstOf( - txs.SubnetPermissionlessDelegatorCurrentPriority, - txs.PrimaryNetworkDelegatorCurrentPriority, - ) - case pendingValidator: - return gen.OneConstOf( - txs.PrimaryNetworkValidatorPendingPriority, - txs.SubnetPermissionlessValidatorPendingPriority, - txs.SubnetPermissionedValidatorPendingPriority, - ) - case pendingDelegator: - return gen.OneConstOf( - txs.PrimaryNetworkDelegatorApricotPendingPriority, - txs.PrimaryNetworkDelegatorBanffPendingPriority, - txs.SubnetPermissionlessDelegatorPendingPriority, - ) - default: - panic("unhandled priority type") - } -} - -var genBlsKey = gen.SliceOfN(lengthID, gen.UInt8()).FlatMap( - func(v interface{}) gopter.Gen { - byteSlice := v.([]byte) - sk := blst.KeyGen(byteSlice) - pk := bls.PublicFromSecretKey(sk) - return gen.Const(pk) - }, - reflect.TypeOf([]byte{}), -) - -const ( - lengthID = 32 - lengthNodeID = 20 -) - -// genID is the helper generator for ids.ID objects -var genID = gen.SliceOfN(lengthID, gen.UInt8()).FlatMap( - func(v interface{}) gopter.Gen { - byteSlice := v.([]byte) - var byteArray [lengthID]byte - copy(byteArray[:], byteSlice) - return gen.Const(ids.ID(byteArray)) - }, - reflect.TypeOf([]byte{}), -) - -// genID is the helper generator for ids.NodeID objects -var genNodeID = gen.SliceOfN(lengthNodeID, gen.UInt8()).FlatMap( - func(v interface{}) gopter.Gen { - byteSlice := v.([]byte) - var byteArray [lengthNodeID]byte - copy(byteArray[:], byteSlice) - return gen.Const(ids.NodeID(byteArray)) - }, - reflect.TypeOf([]byte{}), -) diff --git a/vms/platformvm/state/models/stakers_ops_test.go b/vms/platformvm/state/models/stakers_ops_test.go index cd1bfd729337..16a5a2e64a97 100644 --- a/vms/platformvm/state/models/stakers_ops_test.go +++ b/vms/platformvm/state/models/stakers_ops_test.go @@ -167,7 +167,7 @@ func simpleStakerStateProperties(storeCreatorF func() (state.Stakers, error)) *g // to avoid in-place modification of stakers already stored in store, // as it must be done in prod code. updatedStaker := s - state.RotateStakerTimesInPlace(&updatedStaker) + state.ShiftStakerAheadInPlace(&updatedStaker) err = store.UpdateCurrentValidator(&updatedStaker) if err != nil { @@ -433,7 +433,7 @@ func simpleStakerStateProperties(storeCreatorF func() (state.Stakers, error)) *g // to avoid in-place modification of stakers already stored in store, // as it must be done in prod code. updatedStaker := del - state.RotateStakerTimesInPlace(&updatedStaker) + state.ShiftStakerAheadInPlace(&updatedStaker) err = store.UpdateCurrentDelegator(&updatedStaker) if err != nil { diff --git a/vms/platformvm/state/models/stakers_storage_model_test.go b/vms/platformvm/state/models/stakers_storage_model_test.go index d97f6e28c8a7..fd8eeeffbd47 100644 --- a/vms/platformvm/state/models/stakers_storage_model_test.go +++ b/vms/platformvm/state/models/stakers_storage_model_test.go @@ -223,7 +223,7 @@ func (*putCurrentValidatorCommand) PostCondition(cmdState commands.State, res co func (v *putCurrentValidatorCommand) String() string { return fmt.Sprintf("PutCurrentValidator(subnetID: %v, nodeID: %v, txID: %v, priority: %v, unixStartTime: %v, duration: %v)", - v.SubnetID, v.NodeID, v.TxID, v.Priority, v.StartTime.Unix(), v.EndTime.Sub(v.StartTime)) + v.SubnetID, v.NodeID, v.TxID, v.Priority, v.StartTime.Unix(), v.StakingPeriod) } var genPutCurrentValidatorCommand = state.StakerGenerator(state.CurrentValidator, nil, nil, math.MaxUint64).Map( @@ -249,7 +249,7 @@ func updateCurrentValidatorInSystem(sys *sysUnderTest) error { // 1. check if there is a staker, already inserted. If not return // 2. Add diff layer on top // 3. query the staker - // 4. Rotate staker times and update the staker + // 4. Shift staker times and update the staker chain := sys.getTopChainState() @@ -286,9 +286,9 @@ func updateCurrentValidatorInSystem(sys *sysUnderTest) error { return err } - // 4. Rotate staker times and update the staker + // 4. Shift staker times and update the staker updatedStaker := *staker - state.RotateStakerTimesInPlace(&updatedStaker) + state.ShiftStakerAheadInPlace(&updatedStaker) return chain.UpdateCurrentValidator(&updatedStaker) } @@ -326,7 +326,7 @@ func updateCurrentValidatorInModel(model *stakersStorageModel) error { stakerIt.Release() updatedStaker := *staker - state.RotateStakerTimesInPlace(&updatedStaker) + state.ShiftStakerAheadInPlace(&updatedStaker) return model.UpdateCurrentValidator(&updatedStaker) } @@ -556,7 +556,7 @@ func (*putCurrentDelegatorCommand) PostCondition(cmdState commands.State, res co func (v *putCurrentDelegatorCommand) String() string { return fmt.Sprintf("putCurrentDelegator(subnetID: %v, nodeID: %v, txID: %v, priority: %v, unixStartTime: %v, duration: %v)", - v.SubnetID, v.NodeID, v.TxID, v.Priority, v.StartTime.Unix(), v.EndTime.Sub(v.StartTime)) + v.SubnetID, v.NodeID, v.TxID, v.Priority, v.StartTime.Unix(), v.StakingPeriod) } var genPutCurrentDelegatorCommand = state.StakerGenerator(state.CurrentDelegator, nil, nil, 1000).Map( @@ -581,7 +581,7 @@ func (*updateCurrentDelegatorCommand) Run(sut commands.SystemUnderTest) commands func updateCurrentDelegatorInSystem(sys *sysUnderTest) error { // 1. check if there is a staker, already inserted. If not return // 2. Add diff layer on top - // 3. Rotate staker times and update the staker + // 3. Shift staker times and update the staker chain := sys.getTopChainState() @@ -611,9 +611,9 @@ func updateCurrentDelegatorInSystem(sys *sysUnderTest) error { sys.addDiffOnTop() chain = sys.getTopChainState() - // 3. Rotate delegator times and update the staker + // 3. Shift delegator times and update the staker updatedDelegator := *delegator - state.RotateStakerTimesInPlace(&updatedDelegator) + state.ShiftStakerAheadInPlace(&updatedDelegator) return chain.UpdateCurrentDelegator(&updatedDelegator) } @@ -650,7 +650,7 @@ func updateCurrentDelegatorInModel(model *stakersStorageModel) error { stakerIt.Release() updatedDelegator := *delegator - state.RotateStakerTimesInPlace(&updatedDelegator) + state.ShiftStakerAheadInPlace(&updatedDelegator) return model.UpdateCurrentDelegator(&updatedDelegator) } diff --git a/vms/platformvm/state/staker.go b/vms/platformvm/state/staker.go index 495ceaa72774..f3e7acdd3f86 100644 --- a/vms/platformvm/state/staker.go +++ b/vms/platformvm/state/staker.go @@ -10,15 +10,12 @@ import ( "github.com/google/btree" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) -var ( - _ btree.LessFunc[*Staker] = (*Staker).Less - - StakerZeroTime = time.Unix(0, 0) -) +var _ btree.LessFunc[*Staker] = (*Staker).Less // StakerIterator defines an interface for iterating over a set of stakers. type StakerIterator interface { @@ -39,19 +36,42 @@ type StakerIterator interface { // delegator in the current and pending validator sets. // Invariant: Staker's size is bounded to prevent OOM DoS attacks. type Staker struct { - TxID ids.ID - NodeID ids.NodeID - PublicKey *bls.PublicKey - SubnetID ids.ID - Weight uint64 - StartTime time.Time - EndTime time.Time + TxID ids.ID + NodeID ids.NodeID + PublicKey *bls.PublicKey + SubnetID ids.ID + Weight uint64 + + // StartTime is the time this staker enters the current validators set. + // Pre ContinuousStakingFork, StartTime is set by the Add*Tx creating the staker. + // Post ContinuousStakingFork StartTime is initially set to chain time when Add*Tx is accepted; + // Upon restaking, StartTime is moved ahead by StakingDuration. + StartTime time.Time + + // StakingPeriod is the time the staker will stake. + // Note that it's not necessarily true that StakingPeriod == EndTime - StartTime. + // StakingPeriod does not change during a staker lifetime. + StakingPeriod time.Duration + + // StartTime is the time this staker exits the current validators set. + // Pre ContinuousStakingFork, StartTime is set by the Add*Tx creating the staker. + // Post ContinuousStakingFork StartTime is set initially to mockable.MaxTime. An + // explicit StopStaking transaction with set it to a finite value. + // EndTime may change during a staker lifetime. + EndTime time.Time + PotentialReward uint64 - // NextTime is the next time this staker will be moved from a validator set. - // If the staker is in the pending validator set, NextTime will equal - // StartTime. If the staker is in the current validator set, NextTime will - // equal EndTime. + // Pre ContinuousStaking Fork, NextTime is the next time this staker will be + // moved into/out of the validator set. Specifically + // a. If staker is pending, NextTime equals StartTime, i.e. the time the staker + // will enter the current validators set. + // b. If staker is current, NextTime equals EndTime, i.e. the time the staker + // will exit the current validators set (and will be possibly rewarded). + // Post ContinuousStaking Fork, NextTime is the next time the staker will be + // evaluated for reward. Stakers are marked as current as soon as their creation + // tx is accepted. Also they will automatically restake until a StopStaking tx is issued. + // TODO ABENEGIA: consider renaming NextTime to NextRewardTime NextTime time.Time // Priority specifies how to break ties between stakers with the same @@ -87,10 +107,6 @@ func (s *Staker) Less(than *Staker) bool { return bytes.Compare(s.TxID[:], than.TxID[:]) == -1 } -func (s *Staker) Duration() time.Duration { - return s.EndTime.Sub(s.StartTime) -} - func NewCurrentStaker( txID ids.ID, staker txs.Staker, @@ -102,8 +118,7 @@ func NewCurrentStaker( return nil, err } - stakingDuration := staker.Duration() - endTime := startTime.Add(stakingDuration) + stakingPeriod := staker.StakingPeriod() return &Staker{ TxID: txID, NodeID: staker.NodeID(), @@ -111,9 +126,10 @@ func NewCurrentStaker( SubnetID: staker.SubnetID(), Weight: staker.Weight(), StartTime: startTime, - EndTime: endTime, + StakingPeriod: stakingPeriod, + EndTime: staker.EndTime(), PotentialReward: potentialReward, - NextTime: endTime, + NextTime: startTime.Add(stakingPeriod), Priority: staker.CurrentPriority(), }, nil } @@ -125,29 +141,47 @@ func NewPendingStaker(txID ids.ID, staker txs.Staker) (*Staker, error) { } startTime := staker.StartTime() return &Staker{ - TxID: txID, - NodeID: staker.NodeID(), - PublicKey: publicKey, - SubnetID: staker.SubnetID(), - Weight: staker.Weight(), - StartTime: startTime, - EndTime: staker.EndTime(), - NextTime: startTime, - Priority: staker.PendingPriority(), + TxID: txID, + NodeID: staker.NodeID(), + PublicKey: publicKey, + SubnetID: staker.SubnetID(), + Weight: staker.Weight(), + StartTime: startTime, + EndTime: staker.EndTime(), + StakingPeriod: staker.StakingPeriod(), + NextTime: startTime, + Priority: staker.PendingPriority(), }, nil } -func RotateStakerTimesInPlace(s *Staker) { - var ( - currEndTime = s.EndTime - duration = s.EndTime.Sub(s.StartTime) - ) - - s.StartTime = currEndTime - s.EndTime = currEndTime.Add(duration) - if s.NextTime == currEndTime { - s.NextTime = s.EndTime - } else { - s.NextTime = s.StartTime +// ShiftStakerAheadInPlace moves staker times ahead. +func ShiftStakerAheadInPlace(s *Staker) { + if s.Priority.IsPending() { + return // never shift pending stakers + } + if s.NextTime.Equal(s.EndTime) { + return // can't shift, staker reached EOL + } + s.StartTime = s.StartTime.Add(s.StakingPeriod) + s.NextTime = s.NextTime.Add(s.StakingPeriod) +} + +func (s *Staker) EarliestStopTime() time.Time { + candidateStopTime := s.NextTime + if s.Priority.IsValidator() && s.SubnetID == constants.PrimaryNetworkID { + candidateStopTime = s.NextTime.Add(s.StakingPeriod) // stop at T+1 for now + } + if candidateStopTime.Before(s.EndTime) { + return candidateStopTime + } + return s.EndTime +} + +func MarkStakerForRemovalInPlaceBeforeTime(s *Staker, stopTime time.Time) { + if stopTime.Before(s.EndTime) { + end := s.NextTime + for ; end.Before(stopTime); end = end.Add(s.StakingPeriod) { + } + s.EndTime = end } } diff --git a/vms/platformvm/state/staker_test.go b/vms/platformvm/state/staker_test.go index 42bd24a1457e..5f3f8981b552 100644 --- a/vms/platformvm/state/staker_test.go +++ b/vms/platformvm/state/staker_test.go @@ -5,6 +5,7 @@ package state import ( "errors" + "math/rand" "testing" "time" @@ -13,7 +14,9 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) @@ -132,7 +135,7 @@ func TestStakerLess(t *testing.T) { } } -func TestNewCurrentStaker(t *testing.T) { +func TestNewCurrentStakerPreContinuousStakingFork(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -151,7 +154,8 @@ func TestNewCurrentStaker(t *testing.T) { currentPriority := txs.SubnetPermissionedValidatorCurrentPriority stakerTx := txs.NewMockStaker(ctrl) - stakerTx.EXPECT().Duration().Return(duration) + stakerTx.EXPECT().EndTime().Return(endTime) + stakerTx.EXPECT().StakingPeriod().Return(duration) stakerTx.EXPECT().NodeID().Return(nodeID) stakerTx.EXPECT().PublicKey().Return(publicKey, true, nil) stakerTx.EXPECT().SubnetID().Return(subnetID) @@ -167,6 +171,7 @@ func TestNewCurrentStaker(t *testing.T) { require.Equal(subnetID, staker.SubnetID) require.Equal(weight, staker.Weight) require.Equal(startTime, staker.StartTime) + require.Equal(duration, staker.StakingPeriod) require.Equal(endTime, staker.EndTime) require.Equal(potentialReward, staker.PotentialReward) require.Equal(endTime, staker.NextTime) @@ -178,6 +183,54 @@ func TestNewCurrentStaker(t *testing.T) { require.ErrorIs(err, errCustom) } +func TestNewCurrentStaker(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + txID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + sk, err := bls.NewSecretKey() + require.NoError(err) + publicKey := bls.PublicFromSecretKey(sk) + subnetID := ids.GenerateTestID() + weight := uint64(12345) + startTime := time.Unix(rand.Int63(), 0) // #nosec G404 + duration := time.Duration(rand.Int63n(365 * 24 * 60 * 60 * 1000000000)) // #nosec G404 + endTime := mockable.MaxTime + potentialReward := uint64(54321) + currentPriority := txs.SubnetPermissionedValidatorCurrentPriority + + stakerTx := txs.NewMockStaker(ctrl) + stakerTx.EXPECT().EndTime().Return(endTime) + stakerTx.EXPECT().StakingPeriod().Return(duration) + stakerTx.EXPECT().NodeID().Return(nodeID) + stakerTx.EXPECT().PublicKey().Return(publicKey, true, nil) + stakerTx.EXPECT().SubnetID().Return(subnetID) + stakerTx.EXPECT().Weight().Return(weight) + stakerTx.EXPECT().CurrentPriority().Return(currentPriority) + + staker, err := NewCurrentStaker(txID, stakerTx, startTime, potentialReward) + require.NotNil(staker) + require.NoError(err) + require.Equal(txID, staker.TxID) + require.Equal(nodeID, staker.NodeID) + require.Equal(publicKey, staker.PublicKey) + require.Equal(subnetID, staker.SubnetID) + require.Equal(weight, staker.Weight) + require.Equal(startTime, staker.StartTime) + require.Equal(duration, staker.StakingPeriod) + require.Equal(endTime, staker.EndTime) + require.Equal(potentialReward, staker.PotentialReward) + require.Equal(startTime.Add(duration), staker.NextTime) + require.Equal(currentPriority, staker.Priority) + + stakerTx.EXPECT().PublicKey().Return(nil, false, errCustom) + + _, err = NewCurrentStaker(txID, stakerTx, startTime, potentialReward) + require.ErrorIs(err, errCustom) +} + func TestNewPendingStaker(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) @@ -192,6 +245,7 @@ func TestNewPendingStaker(t *testing.T) { weight := uint64(12345) startTime := time.Now() endTime := time.Now() + duration := endTime.Sub(startTime) pendingPriority := txs.SubnetPermissionedValidatorPendingPriority stakerTx := txs.NewMockStaker(ctrl) @@ -200,6 +254,7 @@ func TestNewPendingStaker(t *testing.T) { stakerTx.EXPECT().SubnetID().Return(subnetID) stakerTx.EXPECT().Weight().Return(weight) stakerTx.EXPECT().StartTime().Return(startTime) + stakerTx.EXPECT().StakingPeriod().Return(duration) stakerTx.EXPECT().EndTime().Return(endTime) stakerTx.EXPECT().PendingPriority().Return(pendingPriority) @@ -212,6 +267,7 @@ func TestNewPendingStaker(t *testing.T) { require.Equal(subnetID, staker.SubnetID) require.Equal(weight, staker.Weight) require.Equal(startTime, staker.StartTime) + require.Equal(duration, staker.StakingPeriod) require.Equal(endTime, staker.EndTime) require.Zero(staker.PotentialReward) require.Equal(startTime, staker.NextTime) @@ -222,3 +278,142 @@ func TestNewPendingStaker(t *testing.T) { _, err = NewPendingStaker(txID, stakerTx) require.ErrorIs(err, errCustom) } + +func TestShiftStaker(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // create the staker + var ( + start = time.Now().Truncate(time.Second) + stakingPeriod = 6 * 30 * 24 * time.Hour + end = mockable.MaxTime + ) + + // Shift with max end time + staker := &Staker{ + StartTime: start, + StakingPeriod: stakingPeriod, + NextTime: start.Add(stakingPeriod), + EndTime: end, + } + require.True(staker.NextTime.Before(staker.EndTime)) + + ShiftStakerAheadInPlace(staker) + require.Equal(start.Add(stakingPeriod), staker.StartTime) + require.Equal(stakingPeriod, staker.StakingPeriod) + require.Equal(start.Add(2*stakingPeriod), staker.NextTime) + require.Equal(end, staker.EndTime) + require.False(staker.NextTime.After(staker.EndTime)) // invariant + + // Shift with finite end time set in the future + periods := 5 + end = start.Add(time.Duration(periods) * stakingPeriod) + staker = &Staker{ + StartTime: start, + StakingPeriod: stakingPeriod, + NextTime: start.Add(stakingPeriod), + EndTime: end, + } + require.False(staker.NextTime.After(staker.EndTime)) // invariant + + for i := 1; i < periods; i++ { + ShiftStakerAheadInPlace(staker) + require.Equal(start.Add(time.Duration(i)*stakingPeriod), staker.StartTime) + require.Equal(stakingPeriod, staker.StakingPeriod) + require.Equal(start.Add(time.Duration(i+1)*stakingPeriod), staker.NextTime) + require.Equal(end, staker.EndTime) + require.False(staker.NextTime.After(staker.EndTime)) // invariant + } + require.Equal(staker.EndTime, staker.NextTime) + + // staker reached end of life, shift must be ineffective + cpy := *staker + ShiftStakerAheadInPlace(&cpy) + require.Equal(staker, &cpy) +} + +func TestPrimaryNetworkValidatorStopTimes(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // create the staker + nodeID := ids.GenerateTestNodeID() + subnetID := constants.PrimaryNetworkID + startTime := time.Now().Truncate(time.Second) + duration := 365 * 24 * time.Hour + endTime := mockable.MaxTime + currentPriority := txs.PrimaryNetworkValidatorCurrentPriority + + stakerTx := txs.NewMockStaker(ctrl) + stakerTx.EXPECT().NodeID().Return(nodeID) + stakerTx.EXPECT().SubnetID().Return(subnetID) + stakerTx.EXPECT().StakingPeriod().Return(duration) + stakerTx.EXPECT().EndTime().Return(endTime) + stakerTx.EXPECT().CurrentPriority().Return(currentPriority) + + stakerTx.EXPECT().PublicKey().Return(nil, true, nil) + stakerTx.EXPECT().Weight().Return(uint64(123)) + + txID := ids.GenerateTestID() + potentialReward := uint64(54321) + staker, err := NewCurrentStaker(txID, stakerTx, startTime, potentialReward) + require.NoError(err) + + // stopTime should be at T+1 staking period + require.Equal(startTime.Add(duration), staker.NextTime) + require.Equal(mockable.MaxTime, staker.EndTime) + stopTime := staker.EarliestStopTime() + require.Equal(staker.NextTime.Add(staker.StakingPeriod), stopTime) + + MarkStakerForRemovalInPlaceBeforeTime(staker, stopTime) + require.Equal(stopTime, staker.EndTime) + require.Equal(stopTime, staker.EarliestStopTime()) + + // staker shift must not change stop time + ShiftStakerAheadInPlace(staker) + require.Equal(stopTime, staker.EndTime) + require.Equal(stopTime, staker.EarliestStopTime()) +} + +func TestNonPrimaryNetworkValidatorStopTimes(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // create the staker + nodeID := ids.GenerateTestNodeID() + subnetID := constants.PrimaryNetworkID + startTime := time.Now().Truncate(time.Second) + duration := 365 * 24 * time.Hour + endTime := mockable.MaxTime + currentPriority := txs.PrimaryNetworkDelegatorCurrentPriority + + stakerTx := txs.NewMockStaker(ctrl) + stakerTx.EXPECT().NodeID().Return(nodeID) + stakerTx.EXPECT().SubnetID().Return(subnetID) + stakerTx.EXPECT().StakingPeriod().Return(duration) + stakerTx.EXPECT().EndTime().Return(endTime) + stakerTx.EXPECT().CurrentPriority().Return(currentPriority) + + stakerTx.EXPECT().PublicKey().Return(nil, true, nil) + stakerTx.EXPECT().Weight().Return(uint64(123)) + + txID := ids.GenerateTestID() + potentialReward := uint64(54321) + staker, err := NewCurrentStaker(txID, stakerTx, startTime, potentialReward) + require.NoError(err) + + // stopTime should be at end of staking period + require.Equal(startTime.Add(duration), staker.NextTime) + require.Equal(mockable.MaxTime, staker.EndTime) + stopTime := staker.EarliestStopTime() + require.Equal(staker.NextTime, stopTime) + + MarkStakerForRemovalInPlaceBeforeTime(staker, stopTime) + require.Equal(stopTime, staker.NextTime) + require.Equal(stopTime, staker.EndTime) + require.Equal(stopTime, staker.EarliestStopTime()) +} diff --git a/vms/platformvm/state/stakers_test.go b/vms/platformvm/state/stakers_test.go index 7a25efce8ed4..4653212bafed 100644 --- a/vms/platformvm/state/stakers_test.go +++ b/vms/platformvm/state/stakers_test.go @@ -219,13 +219,15 @@ func TestDiffStakersDelegator(t *testing.T) { func newTestStaker() *Staker { startTime := time.Now().Round(time.Second) - endTime := startTime.Add(28 * 24 * time.Hour) + duration := 28 * 24 * time.Hour + endTime := startTime.Add(duration) return &Staker{ TxID: ids.GenerateTestID(), NodeID: ids.GenerateTestNodeID(), SubnetID: ids.GenerateTestID(), Weight: 1, StartTime: startTime, + StakingPeriod: duration, EndTime: endTime, PotentialReward: 1, diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 1a528dc0c89a..a3aca657df5d 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1046,8 +1046,8 @@ func (s *state) syncGenesis(genesisBlk blocks.Block, genesis *genesis.State) err return fmt.Errorf("expected tx type *txs.AddValidatorTx but got %T", vdrTx.Unsigned) } - stakeAmount := tx.Validator.Wght - stakeDuration := tx.Validator.Duration() + stakeAmount := tx.Weight() + stakeDuration := tx.StakingPeriod() currentSupply, err := s.GetCurrentSupply(constants.PrimaryNetworkID) if err != nil { return err diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index b54ed41c5f30..b2b179e70abc 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -667,7 +667,8 @@ func TestStateAddRemoveValidator(t *testing.T) { numNodes = 3 subnetID = ids.GenerateTestID() startTime = time.Now() - endTime = startTime.Add(24 * time.Hour) + duration = 24 * time.Hour + endTime = startTime.Add(duration) stakers = make([]Staker, numNodes) ) for i := 0; i < numNodes; i++ { @@ -676,6 +677,7 @@ func TestStateAddRemoveValidator(t *testing.T) { NodeID: ids.GenerateTestNodeID(), Weight: uint64(i + 1), StartTime: startTime.Add(time.Duration(i) * time.Second), + StakingPeriod: duration, EndTime: endTime.Add(time.Duration(i) * time.Second), PotentialReward: uint64(i + 1), } diff --git a/vms/platformvm/state/test_stakers_generator.go b/vms/platformvm/state/test_stakers_generator.go index d170eb51a901..674a9e56df06 100644 --- a/vms/platformvm/state/test_stakers_generator.go +++ b/vms/platformvm/state/test_stakers_generator.go @@ -20,11 +20,15 @@ import ( "github.com/leanovate/gopter/prop" ) +// StakerGenerator helps creating random yet reproducible Staker objects, which +// can be used in our property tests. +// StakerGenerator takes care of enforcing some Staker invariants on each and every random sample. +// TestGeneratedStakersValidity documents and verifies the enforced invariants. func StakerGenerator( prio StakerGeneratorPriorityType, subnet *ids.ID, nodeID *ids.NodeID, - maxWeight uint64, // helps avoiding overflowS in delegator tests + maxWeight uint64, // helps avoiding overflows in delegator tests ) gopter.Gen { return genStakerTimeData(prio).FlatMap( func(v interface{}) gopter.Gen { @@ -46,6 +50,7 @@ func StakerGenerator( "SubnetID": genStakerSubnetID, "Weight": gen.UInt64Range(0, maxWeight), "StartTime": gen.Const(macro.StartTime), + "Duration": gen.Const(macro.Duration), "EndTime": gen.Const(macro.EndTime), "PotentialReward": gen.UInt64(), "NextTime": gen.Const(macro.NextTime), @@ -130,6 +135,7 @@ func TestGeneratedStakersValidity(t *testing.T) { // 3. NextTime == StartTime for pending priorities type stakerTimeData struct { StartTime time.Time + Duration time.Duration EndTime time.Time Priority txs.Priority NextTime time.Time @@ -142,11 +148,13 @@ func genStakerTimeData(prio StakerGeneratorPriorityType) gopter.Gen { var ( startTime = micro.StartTime - endTime = micro.StartTime.Add(time.Duration(micro.Duration * int64(time.Hour))) + duration = time.Duration(micro.Duration * int64(time.Hour)) + endTime = micro.StartTime.Add(duration) priority = micro.Priority ) startTimeGen := gen.Const(startTime) + durationGen := gen.Const(duration) endTimeGen := gen.Const(endTime) priorityGen := gen.Const(priority) var nextTimeGen gopter.Gen @@ -162,6 +170,7 @@ func genStakerTimeData(prio StakerGeneratorPriorityType) gopter.Gen { return gen.Struct(reflect.TypeOf(stakerTimeData{}), map[string]gopter.Gen{ "StartTime": startTimeGen, + "Duration": durationGen, "EndTime": endTimeGen, "Priority": priorityGen, "NextTime": nextTimeGen, diff --git a/vms/platformvm/txs/builder/builder.go b/vms/platformvm/txs/builder/builder.go index 279202087619..f720e697bb1a 100644 --- a/vms/platformvm/txs/builder/builder.go +++ b/vms/platformvm/txs/builder/builder.go @@ -166,6 +166,13 @@ type ProposalTxBuilder interface { // RewardStakerTx creates a new transaction that proposes to remove the staker // [validatorID] from the default validator set. NewRewardValidatorTx(txID ids.ID) (*txs.Tx, error) + + // NewStopStakerTx creates a new transaction that stops the staker with TxID txID. + NewStopStakerTx( + txID ids.ID, + keys []*secp256k1.PrivateKey, + changeAddr ids.ShortID, + ) (*txs.Tx, error) } func New( @@ -609,3 +616,37 @@ func (b *builder) NewRewardValidatorTx(txID ids.ID) (*txs.Tx, error) { return tx, tx.SyntacticVerify(b.ctx) } + +func (b *builder) NewStopStakerTx( + txID ids.ID, + keys []*secp256k1.PrivateKey, + changeAddr ids.ShortID, +) (*txs.Tx, error) { + ins, outs, _, signers, err := b.Spend(b.state, keys, 0, b.cfg.TxFee, changeAddr) + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + stopStakerAuth, subnetSigners, err := b.AuthorizeStopStaking(txID, b.state, keys) + if err != nil { + return nil, fmt.Errorf("couldn't authorize staker stopping: %w", err) + } + signers = append(signers, subnetSigners) + + utx := &txs.StopStakerTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.ctx.NetworkID, + BlockchainID: b.ctx.ChainID, + Ins: ins, + Outs: outs, + }}, + TxID: txID, + StakerAuth: stopStakerAuth, + } + + tx, err := txs.NewSigned(utx, txs.Codec, signers) + if err != nil { + return nil, err + } + return tx, tx.SyntacticVerify(b.ctx) +} diff --git a/vms/platformvm/txs/builder/mock_builder.go b/vms/platformvm/txs/builder/mock_builder.go index 1f7c4f3d2d27..d668f7a2d327 100644 --- a/vms/platformvm/txs/builder/mock_builder.go +++ b/vms/platformvm/txs/builder/mock_builder.go @@ -189,3 +189,18 @@ func (mr *MockBuilderMockRecorder) NewRewardValidatorTx(arg0 interface{}) *gomoc mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewRewardValidatorTx", reflect.TypeOf((*MockBuilder)(nil).NewRewardValidatorTx), arg0) } + +// NewStopStakerTx mocks base method. +func (m *MockBuilder) NewStopStakerTx(arg0 ids.ID, arg1 []*secp256k1.PrivateKey, arg2 ids.ShortID) (*txs.Tx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewStopStakerTx", arg0, arg1, arg2) + ret0, _ := ret[0].(*txs.Tx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewStopStakerTx indicates an expected call of NewStopStakerTx. +func (mr *MockBuilderMockRecorder) NewStopStakerTx(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStopStakerTx", reflect.TypeOf((*MockBuilder)(nil).NewStopStakerTx), arg0, arg1, arg2) +} diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index d3667b9406db..9c6660da9f72 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -91,6 +91,9 @@ func RegisterUnsignedTxsTypes(targetCodec codec.Registry) error { targetCodec.RegisterType(&signer.Empty{}), targetCodec.RegisterType(&signer.ProofOfPossession{}), + + // Continuous Staking addition: + targetCodec.RegisterType(&StopStakerTx{}), ) return errs.Err } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 00cb03404009..2f89b7030c0a 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -72,6 +72,10 @@ func (*AtomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDele return ErrWrongTxType } +func (*AtomicTxExecutor) StopStakerTx(*txs.StopStakerTx) error { + return ErrWrongTxType +} + func (e *AtomicTxExecutor) ImportTx(tx *txs.ImportTx) error { return e.atomicTx(tx) } diff --git a/vms/platformvm/txs/executor/create_subnet_test.go b/vms/platformvm/txs/executor/create_subnet_test.go index e665fa8b1a19..65e6ea29a4c8 100644 --- a/vms/platformvm/txs/executor/create_subnet_test.go +++ b/vms/platformvm/txs/executor/create_subnet_test.go @@ -48,7 +48,7 @@ func TestCreateSubnetTxAP3FeeChange(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) - env := newEnvironment(ContinuousStakingFork) + env := newEnvironment(ApricotFork) env.config.ApricotPhase3Time = ap3Time env.ctx.Lock.Lock() defer func() { diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index e9106f51a171..a42bb9f3445a 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -97,6 +97,10 @@ func (*ProposalTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDe return ErrWrongTxType } +func (*ProposalTxExecutor) StopStakerTx(*txs.StopStakerTx) error { + return ErrWrongTxType +} + func (e *ProposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { // AddValidatorTx is a proposal transaction until the Banff fork // activation. Following the activation, AddValidatorTxs must be issued into @@ -160,7 +164,7 @@ func (e *ProposalTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) ) } - if err := verifyAddSubnetValidatorTx( + if _, err := verifyAddSubnetValidatorTx( e.Backend, e.OnCommitState, e.Tx, @@ -208,7 +212,7 @@ func (e *ProposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { ) } - onAbortOuts, err := verifyAddDelegatorTx( + onAbortOuts, _, err := verifyAddDelegatorTx( e.Backend, e.OnCommitState, e.Tx, @@ -334,13 +338,13 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error // Verify that the chain's timestamp is the validator's end time currentChainTime := e.OnCommitState.GetTimestamp() - if !stakerToRemove.EndTime.Equal(currentChainTime) { + if !stakerToRemove.NextTime.Equal(currentChainTime) { return fmt.Errorf( "%w: TxID = %s with %s < %s", ErrRemoveStakerTooEarly, tx.TxID, currentChainTime, - stakerToRemove.EndTime, + stakerToRemove.NextTime, ) } @@ -361,29 +365,54 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error switch uStakerTx := stakerTx.Unsigned.(type) { case txs.ValidatorTx: - e.OnCommitState.DeleteCurrentValidator(stakerToRemove) - e.OnAbortState.DeleteCurrentValidator(stakerToRemove) + var ( + shouldRestake = stakerToRemove.NextTime.Before(stakerToRemove.EndTime) - stake := uStakerTx.Stake() - outputs := uStakerTx.Outputs() - // Invariant: The staked asset must be equal to the reward asset. - stakeAsset := stake[0].Asset + stake = uStakerTx.Stake() + outputs = uStakerTx.Outputs() + // Invariant: The staked asset must be equal to the reward asset. + stakeAsset = stake[0].Asset + ) - // Refund the stake here - for i, out := range stake { - utxo := &avax.UTXO{ - UTXOID: avax.UTXOID{ - TxID: tx.TxID, - OutputIndex: uint32(len(outputs) + i), - }, - Asset: out.Asset, - Out: out.Output(), + if shouldRestake { + shiftedStaker := *stakerToRemove + state.ShiftStakerAheadInPlace(&shiftedStaker) + if err := e.OnCommitState.UpdateCurrentValidator(&shiftedStaker); err != nil { + return fmt.Errorf("failed updating current validator: %w", err) + } + if err := e.OnAbortState.UpdateCurrentValidator(&shiftedStaker); err != nil { + return fmt.Errorf("failed updating current validator: %w", err) + } + // staked utxos will be returned only at the end of the staking period. + } else { + e.OnCommitState.DeleteCurrentValidator(stakerToRemove) + e.OnAbortState.DeleteCurrentValidator(stakerToRemove) + + // Refund the stake only when validator is about to leave + // the staking set + for i, out := range stake { + utxo := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: tx.TxID, + OutputIndex: uint32(len(outputs) + i), + }, + Asset: out.Asset, + Out: out.Output(), + } + e.OnCommitState.AddUTXO(utxo) + e.OnAbortState.AddUTXO(utxo) } - e.OnCommitState.AddUTXO(utxo) - e.OnAbortState.AddUTXO(utxo) } - offset := 0 + // following Continuous staking fork activation multiple rewards UTXOS + // can be cumulated, each related to a different staking period. We make + // sure to index the reward UTXOs correctly by appending them to previous ones. + utxosOffset := len(outputs) + len(stake) + currentRewardUTXOs, err := e.OnCommitState.GetRewardUTXOs(tx.TxID) + if err != nil { + return fmt.Errorf("failed to create output: %w", err) + } + utxosOffset += len(currentRewardUTXOs) // Provide the reward here if stakerToRemove.PotentialReward > 0 { @@ -400,16 +429,15 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error utxo := &avax.UTXO{ UTXOID: avax.UTXOID{ TxID: tx.TxID, - OutputIndex: uint32(len(outputs) + len(stake)), + OutputIndex: uint32(utxosOffset), }, Asset: stakeAsset, Out: out, } - e.OnCommitState.AddUTXO(utxo) e.OnCommitState.AddRewardUTXO(tx.TxID, utxo) - offset++ + utxosOffset++ } // Provide the accrued delegatee rewards from successful delegations here. @@ -417,7 +445,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error stakerToRemove.SubnetID, stakerToRemove.NodeID, ) - if err != nil { + if err != nil && err != database.ErrNotFound { return fmt.Errorf("failed to fetch accrued delegatee rewards: %w", err) } @@ -435,7 +463,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error onCommitUtxo := &avax.UTXO{ UTXOID: avax.UTXOID{ TxID: tx.TxID, - OutputIndex: uint32(len(outputs) + len(stake) + offset), + OutputIndex: uint32(utxosOffset), }, Asset: stakeAsset, Out: out, @@ -448,7 +476,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error TxID: tx.TxID, // Note: There is no [offset] if the RewardValidatorTx is // aborted, because the validator reward is not awarded. - OutputIndex: uint32(len(outputs) + len(stake)), + OutputIndex: uint32(utxosOffset - 1), }, Asset: stakeAsset, Out: out, @@ -459,29 +487,47 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error // Invariant: A [txs.DelegatorTx] does not also implement the // [txs.ValidatorTx] interface. case txs.DelegatorTx: - e.OnCommitState.DeleteCurrentDelegator(stakerToRemove) - e.OnAbortState.DeleteCurrentDelegator(stakerToRemove) + var ( + shouldRestake = stakerToRemove.NextTime.Before(stakerToRemove.EndTime) - stake := uStakerTx.Stake() - outputs := uStakerTx.Outputs() - stakeAsset := stake[0].Asset + stake = uStakerTx.Stake() + outputs = uStakerTx.Outputs() + // Invariant: The staked asset must be equal to the reward asset. + stakeAsset = stake[0].Asset + ) - // Refund the stake here - for i, out := range stake { - utxo := &avax.UTXO{ - UTXOID: avax.UTXOID{ - TxID: tx.TxID, - OutputIndex: uint32(len(outputs) + i), - }, - Asset: out.Asset, - Out: out.Output(), + if shouldRestake { + shiftedStaker := *stakerToRemove + state.ShiftStakerAheadInPlace(&shiftedStaker) + if err := e.OnCommitState.UpdateCurrentDelegator(&shiftedStaker); err != nil { + return fmt.Errorf("failed updating current delegator: %w", err) + } + if err := e.OnAbortState.UpdateCurrentDelegator(&shiftedStaker); err != nil { + return fmt.Errorf("failed updating current delegator: %w", err) + } + // staked utxos will be returned only at the end of the staking period. + } else { + e.OnCommitState.DeleteCurrentDelegator(stakerToRemove) + e.OnAbortState.DeleteCurrentDelegator(stakerToRemove) + + // Refund the stake only when delegator is about to leave + // the staking set + for i, out := range stake { + utxo := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: tx.TxID, + OutputIndex: uint32(len(outputs) + i), + }, + Asset: out.Asset, + Out: out.Output(), + } + e.OnCommitState.AddUTXO(utxo) + e.OnAbortState.AddUTXO(utxo) } - e.OnCommitState.AddUTXO(utxo) - e.OnAbortState.AddUTXO(utxo) } - // We're removing a delegator, so we need to fetch the validator they - // are delegated to. + // We're (possibly) rewarding a delegator, so we need to fetch + // the validator they are delegated to. vdrStaker, err := e.OnCommitState.GetCurrentValidator( stakerToRemove.SubnetID, stakerToRemove.NodeID, @@ -523,7 +569,15 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error } delegateeReward := stakerToRemove.PotentialReward - delegatorReward // delegatorReward <= reward so no underflow - offset := 0 + // following Continuous staking fork activation multiple rewards UTXOS + // can be cumulated, each related to a different staking period. We make + // sure to index the reward UTXOs correctly by appending them to previous ones. + utxosOffset := len(outputs) + len(stake) + currentRewardUTXOs, err := e.OnCommitState.GetRewardUTXOs(tx.TxID) + if err != nil { + return fmt.Errorf("failed to create output: %w", err) + } + utxosOffset += len(currentRewardUTXOs) // Reward the delegator here if delegatorReward > 0 { @@ -539,7 +593,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error utxo := &avax.UTXO{ UTXOID: avax.UTXOID{ TxID: tx.TxID, - OutputIndex: uint32(len(outputs) + len(stake)), + OutputIndex: uint32(utxosOffset), }, Asset: stakeAsset, Out: out, @@ -548,7 +602,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error e.OnCommitState.AddUTXO(utxo) e.OnCommitState.AddRewardUTXO(tx.TxID, utxo) - offset++ + utxosOffset++ } // Reward the delegatee here @@ -592,7 +646,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error utxo := &avax.UTXO{ UTXOID: avax.UTXOID{ TxID: tx.TxID, - OutputIndex: uint32(len(outputs) + len(stake) + offset), + OutputIndex: uint32(utxosOffset), }, Asset: stakeAsset, Out: out, @@ -716,6 +770,9 @@ func canDelegate( if delegator.StartTime.Before(validator.StartTime) { return false, nil } + if delegator.StakingPeriod > validator.StakingPeriod { + return false, nil + } if delegator.EndTime.After(validator.EndTime) { return false, nil } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor_test.go b/vms/platformvm/txs/executor/proposal_tx_executor_apricot_test.go similarity index 100% rename from vms/platformvm/txs/executor/proposal_tx_executor_test.go rename to vms/platformvm/txs/executor/proposal_tx_executor_apricot_test.go diff --git a/vms/platformvm/txs/executor/reward_validator_test.go b/vms/platformvm/txs/executor/reward_validator_test.go index 4f458c3aa499..59d11874e056 100644 --- a/vms/platformvm/txs/executor/reward_validator_test.go +++ b/vms/platformvm/txs/executor/reward_validator_test.go @@ -23,7 +23,7 @@ import ( "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) -func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { +func TestContinuousStakingForkRewardValidatorTxExecuteOnCommit(t *testing.T) { require := require.New(t) env := newEnvironment(ContinuousStakingFork) defer func() { @@ -31,19 +31,40 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { }() dummyHeight := uint64(1) - currentStakerIterator, err := env.state.GetCurrentStakerIterator() + // Add a continuous validator + onParentState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) - require.True(currentStakerIterator.Next()) - stakerToRemove := currentStakerIterator.Value() - currentStakerIterator.Release() + valRewardAddress := ids.GenerateTestShortID() + addValTx, err := env.txBuilder.NewAddValidatorTx( + env.config.MinValidatorStake, + uint64(0), + uint64(24*60*60), // 24h in seconds + ids.GenerateTestNodeID(), + valRewardAddress, + reward.PercentDenominator, + []*secp256k1.PrivateKey{preFundedKeys[0]}, + ids.ShortEmpty, + ) + require.NoError(err) + continuousStakerTx := addValTx.Unsigned.(*txs.AddValidatorTx) - stakerToRemoveTxIntf, _, err := env.state.GetTx(stakerToRemove.TxID) + addValExecutor := StandardTxExecutor{ + State: onParentState, + Backend: &env.backend, + Tx: addValTx, + } + require.NoError(addValTx.Unsigned.Visit(&addValExecutor)) + onParentState.AddTx(addValTx, status.Committed) + require.NoError(onParentState.Apply(env.state)) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) + + continuousStaker, err := env.state.GetCurrentValidator(continuousStakerTx.SubnetID(), continuousStakerTx.NodeID()) require.NoError(err) - stakerToRemoveTx := stakerToRemoveTxIntf.Unsigned.(*txs.AddValidatorTx) // Case 1: Chain timestamp is wrong - tx, err := env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) + tx, err := env.txBuilder.NewRewardValidatorTx(continuousStaker.TxID) require.NoError(err) onCommitState, err := state.NewDiff(lastAcceptedID, env) @@ -61,8 +82,8 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { err = tx.Unsigned.Visit(&txExecutor) require.ErrorIs(err, ErrRemoveStakerTooEarly) - // Advance chain timestamp to time that next validator leaves - env.state.SetTimestamp(stakerToRemove.EndTime) + // Advance chain timestamp to time that next validator should be rewarded + env.state.SetTimestamp(continuousStaker.NextTime) // Case 2: Wrong validator tx, err = env.txBuilder.NewRewardValidatorTx(ids.GenerateTestID()) @@ -84,7 +105,7 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { require.ErrorIs(err, ErrRemoveWrongStaker) // Case 3: Happy path - tx, err = env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) + tx, err = env.txBuilder.NewRewardValidatorTx(continuousStaker.TxID) require.NoError(err) onCommitState, err = state.NewDiff(lastAcceptedID, env) @@ -103,29 +124,47 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { onCommitStakerIterator, err := txExecutor.OnCommitState.GetCurrentStakerIterator() require.NoError(err) - require.True(onCommitStakerIterator.Next()) - nextToRemove := onCommitStakerIterator.Value() + // check that post ContinuousStakingFork, staker is shifted ahead by its staking period + var shiftedStaker *state.Staker + for onCommitStakerIterator.Next() { + nextStaker := onCommitStakerIterator.Value() + if nextStaker.TxID == continuousStaker.TxID { + shiftedStaker = nextStaker + break + } + } onCommitStakerIterator.Release() - require.NotEqual(stakerToRemove.TxID, nextToRemove.TxID) + require.NotNil(shiftedStaker) + require.Equal(continuousStaker.StakingPeriod, shiftedStaker.StakingPeriod) + require.Equal(continuousStaker.StartTime.Add(continuousStaker.StakingPeriod), shiftedStaker.StartTime) + require.Equal(continuousStaker.NextTime.Add(continuousStaker.StakingPeriod), shiftedStaker.NextTime) + require.Equal(continuousStaker.EndTime.Add(continuousStaker.StakingPeriod), shiftedStaker.EndTime) - // check that stake/reward is given back - stakeOwners := stakerToRemoveTx.StakeOuts[0].Out.(*secp256k1fx.TransferOutput).AddressesSet() + // check that reward is given, while stake is not (because of restaking) + stakeOwners := continuousStakerTx.StakeOuts[0].Out.(*secp256k1fx.TransferOutput).AddressesSet() + rewardAddresses := set.NewSet[ids.ShortID](0) + rewardAddresses.Add(valRewardAddress) - // Get old balances - oldBalance, err := avax.GetBalance(env.state, stakeOwners) + stakeOldBalance, err := avax.GetBalance(env.state, stakeOwners) + require.NoError(err) + rewardsOldBalance, err := avax.GetBalance(env.state, rewardAddresses) require.NoError(err) require.NoError(txExecutor.OnCommitState.Apply(env.state)) - env.state.SetHeight(dummyHeight) + env.state.SetHeight(dummyHeight + 1) require.NoError(env.state.Commit()) - onCommitBalance, err := avax.GetBalance(env.state, stakeOwners) + stakeOnCommitBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - require.Equal(oldBalance+stakerToRemove.Weight+27697, onCommitBalance) + rewardsOnCommitBalance, err := avax.GetBalance(env.state, rewardAddresses) + require.NoError(err) + + require.Equal(stakeOnCommitBalance, stakeOldBalance) + require.Equal(rewardsOnCommitBalance, rewardsOldBalance+1370) } -func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { +func TestContinuousStakingForkRewardValidatorTxExecuteOnAbort(t *testing.T) { require := require.New(t) env := newEnvironment(ContinuousStakingFork) defer func() { @@ -133,19 +172,40 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { }() dummyHeight := uint64(1) - currentStakerIterator, err := env.state.GetCurrentStakerIterator() + // Add a continuous validator + onParentState, err := state.NewDiff(lastAcceptedID, env) require.NoError(err) - require.True(currentStakerIterator.Next()) - stakerToRemove := currentStakerIterator.Value() - currentStakerIterator.Release() + valRewardAddress := ids.GenerateTestShortID() + addValTx, err := env.txBuilder.NewAddValidatorTx( + env.config.MinValidatorStake, + uint64(0), + uint64(24*60*60), // 24h in seconds + ids.GenerateTestNodeID(), + valRewardAddress, + reward.PercentDenominator, + []*secp256k1.PrivateKey{preFundedKeys[0]}, + ids.ShortEmpty, + ) + require.NoError(err) + continuousStakerTx := addValTx.Unsigned.(*txs.AddValidatorTx) - stakerToRemoveTxIntf, _, err := env.state.GetTx(stakerToRemove.TxID) + addValExecutor := StandardTxExecutor{ + State: onParentState, + Backend: &env.backend, + Tx: addValTx, + } + require.NoError(addValTx.Unsigned.Visit(&addValExecutor)) + onParentState.AddTx(addValTx, status.Committed) + require.NoError(onParentState.Apply(env.state)) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) + + continuousStaker, err := env.state.GetCurrentValidator(continuousStakerTx.SubnetID(), continuousStakerTx.NodeID()) require.NoError(err) - stakerToRemoveTx := stakerToRemoveTxIntf.Unsigned.(*txs.AddValidatorTx) // Case 1: Chain timestamp is wrong - tx, err := env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) + tx, err := env.txBuilder.NewRewardValidatorTx(continuousStaker.TxID) require.NoError(err) onCommitState, err := state.NewDiff(lastAcceptedID, env) @@ -164,7 +224,7 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { require.ErrorIs(err, ErrRemoveStakerTooEarly) // Advance chain timestamp to time that next validator leaves - env.state.SetTimestamp(stakerToRemove.EndTime) + env.state.SetTimestamp(continuousStaker.NextTime) // Case 2: Wrong validator tx, err = env.txBuilder.NewRewardValidatorTx(ids.GenerateTestID()) @@ -180,7 +240,7 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { require.ErrorIs(err, ErrRemoveWrongStaker) // Case 3: Happy path - tx, err = env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) + tx, err = env.txBuilder.NewRewardValidatorTx(continuousStaker.TxID) require.NoError(err) onCommitState, err = state.NewDiff(lastAcceptedID, env) @@ -199,31 +259,50 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { onAbortStakerIterator, err := txExecutor.OnAbortState.GetCurrentStakerIterator() require.NoError(err) - require.True(onAbortStakerIterator.Next()) - nextToRemove := onAbortStakerIterator.Value() + // check that post ContinuousStakingFork, staker is shifted ahead by its staking period + var shiftedStaker *state.Staker + for onAbortStakerIterator.Next() { + nextStaker := onAbortStakerIterator.Value() + if nextStaker.TxID == continuousStaker.TxID { + shiftedStaker = nextStaker + break + } + } onAbortStakerIterator.Release() - require.NotEqual(stakerToRemove.TxID, nextToRemove.TxID) + require.NotNil(shiftedStaker) + require.NotNil(shiftedStaker) + require.Equal(continuousStaker.StakingPeriod, shiftedStaker.StakingPeriod) + require.Equal(continuousStaker.StartTime.Add(continuousStaker.StakingPeriod), shiftedStaker.StartTime) + require.Equal(continuousStaker.NextTime.Add(continuousStaker.StakingPeriod), shiftedStaker.NextTime) + require.Equal(continuousStaker.EndTime.Add(continuousStaker.StakingPeriod), shiftedStaker.EndTime) - // check that stake/reward isn't given back - stakeOwners := stakerToRemoveTx.StakeOuts[0].Out.(*secp256k1fx.TransferOutput).AddressesSet() + // check that reward is given, while stake is not (because of restaking) + stakeOwners := continuousStakerTx.StakeOuts[0].Out.(*secp256k1fx.TransferOutput).AddressesSet() + rewardAddresses := set.NewSet[ids.ShortID](0) + rewardAddresses.Add(valRewardAddress) - // Get old balances - oldBalance, err := avax.GetBalance(env.state, stakeOwners) + stakeOldBalance, err := avax.GetBalance(env.state, stakeOwners) + require.NoError(err) + rewardsOldBalance, err := avax.GetBalance(env.state, rewardAddresses) require.NoError(err) require.NoError(txExecutor.OnAbortState.Apply(env.state)) - env.state.SetHeight(dummyHeight) + env.state.SetHeight(dummyHeight + 1) require.NoError(env.state.Commit()) - onAbortBalance, err := avax.GetBalance(env.state, stakeOwners) + stakeOnCommitBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - require.Equal(oldBalance+stakerToRemove.Weight, onAbortBalance) + rewardsOnCommitBalance, err := avax.GetBalance(env.state, rewardAddresses) + require.NoError(err) + + require.Equal(stakeOnCommitBalance, stakeOldBalance) + require.Equal(rewardsOnCommitBalance, rewardsOldBalance) } -func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { +func TestContinuousStakingForkRewardDelegatorTxExecuteOnCommit(t *testing.T) { require := require.New(t) - env := newEnvironment(BanffFork) + env := newEnvironment(ContinuousStakingFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -231,71 +310,365 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { vdrRewardAddress := ids.GenerateTestShortID() delRewardAddress := ids.GenerateTestShortID() - - vdrStartTime := uint64(defaultValidateStartTime.Unix()) + 1 - vdrEndTime := uint64(defaultValidateStartTime.Add(2 * defaultMinStakingDuration).Unix()) vdrNodeID := ids.GenerateTestNodeID() + stakersPeriod := uint64(24 * 60 * 60) // 24h in seconds vdrTx, err := env.txBuilder.NewAddValidatorTx( - env.config.MinValidatorStake, // stakeAmt - vdrStartTime, - vdrEndTime, - vdrNodeID, // node ID - vdrRewardAddress, // reward address + env.config.MinValidatorStake, + uint64(0), + stakersPeriod, + vdrNodeID, + vdrRewardAddress, reward.PercentDenominator/4, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, + ids.ShortEmpty, /*=changeAddr*/ ) require.NoError(err) - delStartTime := vdrStartTime - delEndTime := vdrEndTime - delTx, err := env.txBuilder.NewAddDelegatorTx( env.config.MinDelegatorStake, - delStartTime, - delEndTime, + uint64(0), + stakersPeriod, vdrNodeID, delRewardAddress, []*secp256k1.PrivateKey{preFundedKeys[0]}, - ids.ShortEmpty, // Change address + ids.ShortEmpty, /*=changeAddr*/ + ) + require.NoError(err) + + currentChainTime := env.state.GetTimestamp() + addValTx := vdrTx.Unsigned.(*txs.AddValidatorTx) + vdrRewardAmt := uint64(2000000) + vdrStaker, err := state.NewCurrentStaker( + vdrTx.ID(), + addValTx, + currentChainTime, + vdrRewardAmt, ) require.NoError(err) - addValTx := vdrTx.Unsigned.(*txs.AddValidatorTx) - vdrStaker, err := state.NewCurrentStaker( - vdrTx.ID(), - addValTx, - addValTx.StartTime(), - 0, - ) + addDelTx := delTx.Unsigned.(*txs.AddDelegatorTx) + delRewardAmt := uint64(1000000) + delStaker, err := state.NewCurrentStaker( + delTx.ID(), + addDelTx, + currentChainTime, + delRewardAmt, + ) + require.NoError(err) + + env.state.PutCurrentValidator(vdrStaker) + env.state.AddTx(vdrTx, status.Committed) + env.state.PutCurrentDelegator(delStaker) + env.state.AddTx(delTx, status.Committed) + env.state.SetTimestamp(delStaker.NextTime) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) + + vdrDestSet := set.NewSet[ids.ShortID](0) + vdrDestSet.Add(vdrRewardAddress) + delDestSet := set.NewSet[ids.ShortID](0) + delDestSet.Add(delRewardAddress) + + oldVdrBalance, err := avax.GetBalance(env.state, vdrDestSet) + require.NoError(err) + oldDelBalance, err := avax.GetBalance(env.state, delDestSet) + require.NoError(err) + + // test validator stake + vdrSet, ok := env.config.Validators.Get(constants.PrimaryNetworkID) + require.True(ok) + + stake := vdrSet.GetWeight(vdrNodeID) + require.Equal(env.config.MinValidatorStake+env.config.MinDelegatorStake, stake) + + tx, err := env.txBuilder.NewRewardValidatorTx(delTx.ID()) + require.NoError(err) + + // Create Delegator Diff + onCommitState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + onAbortState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + txExecutor := ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + err = tx.Unsigned.Visit(&txExecutor) + require.NoError(err) + + // check that delegator is not dropped from the staker set + onCommitStakerIterator, err := txExecutor.OnCommitState.GetCurrentStakerIterator() + require.NoError(err) + + var shiftedStaker *state.Staker + for onCommitStakerIterator.Next() { + nextStaker := onCommitStakerIterator.Value() + if nextStaker.TxID == delStaker.TxID { + shiftedStaker = nextStaker + break + } + } + onCommitStakerIterator.Release() + require.NotNil(shiftedStaker) + require.Equal(delStaker.StakingPeriod, shiftedStaker.StakingPeriod) + require.Equal(delStaker.StartTime.Add(delStaker.StakingPeriod), shiftedStaker.StartTime) + require.Equal(delStaker.NextTime.Add(delStaker.StakingPeriod), shiftedStaker.NextTime) + require.Equal(delStaker.EndTime.Add(delStaker.StakingPeriod), shiftedStaker.EndTime) + + // The delegator should be rewarded if the ProposalTx is committed. Since the + // delegatee's share is 25%, we expect the delegator to receive 75% of the reward. + // Since this is post [CortinaTime], the delegatee should not be rewarded until a + // RewardValidatorTx is issued for the delegatee. + numDelStakeUTXOs := uint32(len(delTx.Unsigned.InputIDs())) + delRewardUTXOID := &avax.UTXOID{ + TxID: delTx.ID(), + OutputIndex: numDelStakeUTXOs + 1, + } + + utxo, err := onCommitState.GetUTXO(delRewardUTXOID.InputID()) + require.NoError(err) + castUTXO, ok := utxo.Out.(*secp256k1fx.TransferOutput) + require.True(ok) + require.Equal(delRewardAmt*3/4, castUTXO.Amt, "expected delegator balance to increase by 3/4 of reward amount") + require.True(delDestSet.Equals(castUTXO.AddressesSet()), "expected reward UTXO to be issued to delDestSet") + + preCortinaVdrRewardUTXOID := &avax.UTXOID{ + TxID: delTx.ID(), + OutputIndex: numDelStakeUTXOs + 2, + } + _, err = onCommitState.GetUTXO(preCortinaVdrRewardUTXOID.InputID()) + require.ErrorIs(err, database.ErrNotFound) + + // Commit Delegator Diff + require.NoError(txExecutor.OnCommitState.Apply(env.state)) + + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) + + tx, err = env.txBuilder.NewRewardValidatorTx(vdrStaker.TxID) + require.NoError(err) + + // Create Validator Diff + onCommitState, err = state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + onAbortState, err = state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + txExecutor = ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + require.NoError(tx.Unsigned.Visit(&txExecutor)) + + require.NotEqual(vdrStaker.TxID, delStaker.TxID) + + numVdrStakeUTXOs := uint32(len(delTx.Unsigned.InputIDs())) + + // check for validator reward here + vdrRewardUTXOID := &avax.UTXOID{ + TxID: vdrTx.ID(), + OutputIndex: numVdrStakeUTXOs + 1, + } + + utxo, err = onCommitState.GetUTXO(vdrRewardUTXOID.InputID()) + require.NoError(err) + castUTXO, ok = utxo.Out.(*secp256k1fx.TransferOutput) + require.True(ok) + require.Equal(vdrRewardAmt, castUTXO.Amt, "expected validator to be rewarded") + require.True(vdrDestSet.Equals(castUTXO.AddressesSet()), "expected reward UTXO to be issued to vdrDestSet") + + // check for validator's batched delegator rewards here + onCommitVdrDelRewardUTXOID := &avax.UTXOID{ + TxID: vdrTx.ID(), + OutputIndex: numVdrStakeUTXOs + 2, + } + + utxo, err = onCommitState.GetUTXO(onCommitVdrDelRewardUTXOID.InputID()) + require.NoError(err) + castUTXO, ok = utxo.Out.(*secp256k1fx.TransferOutput) + require.True(ok) + require.Equal(delRewardAmt/4, castUTXO.Amt, "expected validator to be rewarded with accrued delegator rewards") + require.True(vdrDestSet.Equals(castUTXO.AddressesSet()), "expected reward UTXO to be issued to vdrDestSet") + + // aborted validator tx should still distribute accrued delegator rewards + onAbortVdrDelRewardUTXOID := &avax.UTXOID{ + TxID: vdrTx.ID(), + OutputIndex: numVdrStakeUTXOs + 1, + } + + utxo, err = onAbortState.GetUTXO(onAbortVdrDelRewardUTXOID.InputID()) + require.NoError(err) + castUTXO, ok = utxo.Out.(*secp256k1fx.TransferOutput) + require.True(ok) + require.Equal(delRewardAmt/4, castUTXO.Amt, "expected validator to be rewarded with accrued delegator rewards") + require.True(vdrDestSet.Equals(castUTXO.AddressesSet()), "expected reward UTXO to be issued to vdrDestSet") + + _, err = onCommitState.GetUTXO(preCortinaVdrRewardUTXOID.InputID()) + require.ErrorIs(err, database.ErrNotFound) + + // Commit Validator Diff + require.NoError(txExecutor.OnCommitState.Apply(env.state)) + + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) + + // Since the tx was committed, the delegator and the delegatee should be rewarded. + // The delegator reward should be higher since the delegatee's share is 25%. + commitVdrBalance, err := avax.GetBalance(env.state, vdrDestSet) + require.NoError(err) + vdrReward, err := math.Sub(commitVdrBalance, oldVdrBalance) + require.NoError(err) + delegateeReward, err := math.Sub(vdrReward, 2000000) + require.NoError(err) + require.NotZero(delegateeReward, "expected delegatee balance to increase because of reward") + + commitDelBalance, err := avax.GetBalance(env.state, delDestSet) + require.NoError(err) + delReward, err := math.Sub(commitDelBalance, oldDelBalance) + require.NoError(err) + require.NotZero(delReward, "expected delegator balance to increase because of reward") + + require.Less(delegateeReward, delReward, "the delegator's reward should be greater than the delegatee's because the delegatee's share is 25%") + require.Equal(delRewardAmt, delReward+delegateeReward, "expected total reward to be %d but is %d", delRewardAmt, delReward+vdrReward) +} + +func TestCortinaForkRewardValidatorTxExecuteOnCommit(t *testing.T) { + require := require.New(t) + env := newEnvironment(CortinaFork) + defer func() { + require.NoError(shutdownEnvironment(env)) + }() + dummyHeight := uint64(1) + + currentStakerIterator, err := env.state.GetCurrentStakerIterator() + require.NoError(err) + require.True(currentStakerIterator.Next()) + + stakerToRemove := currentStakerIterator.Value() + currentStakerIterator.Release() + + stakerToRemoveTxIntf, _, err := env.state.GetTx(stakerToRemove.TxID) + require.NoError(err) + stakerToRemoveTx := stakerToRemoveTxIntf.Unsigned.(*txs.AddValidatorTx) + + // Case 1: Chain timestamp is wrong + tx, err := env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) + require.NoError(err) + + onCommitState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + onAbortState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + txExecutor := ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + err = tx.Unsigned.Visit(&txExecutor) + require.ErrorIs(err, ErrRemoveStakerTooEarly) + + // Advance chain timestamp to time that next validator leaves + env.state.SetTimestamp(stakerToRemove.EndTime) + + // Case 2: Wrong validator + tx, err = env.txBuilder.NewRewardValidatorTx(ids.GenerateTestID()) + require.NoError(err) + + onCommitState, err = state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + onAbortState, err = state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + txExecutor = ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + err = tx.Unsigned.Visit(&txExecutor) + require.ErrorIs(err, ErrRemoveWrongStaker) + + // Case 3: Happy path + tx, err = env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) + require.NoError(err) + + onCommitState, err = state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + onAbortState, err = state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + txExecutor = ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + require.NoError(tx.Unsigned.Visit(&txExecutor)) + + onCommitStakerIterator, err := txExecutor.OnCommitState.GetCurrentStakerIterator() require.NoError(err) - addDelTx := delTx.Unsigned.(*txs.AddDelegatorTx) - delStaker, err := state.NewCurrentStaker( - delTx.ID(), - addDelTx, - addDelTx.StartTime(), - 1000000, - ) + stakerRemoved := true + for onCommitStakerIterator.Next() { + nextStaker := onCommitStakerIterator.Value() + if nextStaker.TxID == stakerToRemove.TxID { + stakerRemoved = false + break + } + } + onCommitStakerIterator.Release() + require.True(stakerRemoved) + + // check that stake/reward is given back + stakeOwners := stakerToRemoveTx.StakeOuts[0].Out.(*secp256k1fx.TransferOutput).AddressesSet() + + // Get old balances + oldBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - env.state.PutCurrentValidator(vdrStaker) - env.state.AddTx(vdrTx, status.Committed) - env.state.PutCurrentDelegator(delStaker) - env.state.AddTx(delTx, status.Committed) - env.state.SetTimestamp(time.Unix(int64(delEndTime), 0)) + require.NoError(txExecutor.OnCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) - // test validator stake - vdrSet, ok := env.config.Validators.Get(constants.PrimaryNetworkID) - require.True(ok) + onCommitBalance, err := avax.GetBalance(env.state, stakeOwners) + require.NoError(err) + require.Equal(oldBalance+stakerToRemove.Weight+27697, onCommitBalance) +} - stake := vdrSet.GetWeight(vdrNodeID) - require.Equal(env.config.MinValidatorStake+env.config.MinDelegatorStake, stake) +func TestCortinaStakingForkRewardValidatorTxExecuteOnAbort(t *testing.T) { + require := require.New(t) + env := newEnvironment(CortinaFork) + defer func() { + require.NoError(shutdownEnvironment(env)) + }() + dummyHeight := uint64(1) - tx, err := env.txBuilder.NewRewardValidatorTx(delTx.ID()) + currentStakerIterator, err := env.state.GetCurrentStakerIterator() + require.NoError(err) + require.True(currentStakerIterator.Next()) + + stakerToRemove := currentStakerIterator.Value() + currentStakerIterator.Release() + + stakerToRemoveTxIntf, _, err := env.state.GetTx(stakerToRemove.TxID) + require.NoError(err) + stakerToRemoveTx := stakerToRemoveTxIntf.Unsigned.(*txs.AddValidatorTx) + + // Case 1: Chain timestamp is wrong + tx, err := env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) require.NoError(err) onCommitState, err := state.NewDiff(lastAcceptedID, env) @@ -311,47 +684,75 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { Tx: tx, } err = tx.Unsigned.Visit(&txExecutor) + require.ErrorIs(err, ErrRemoveStakerTooEarly) + + // Advance chain timestamp to time that next validator leaves + env.state.SetTimestamp(stakerToRemove.EndTime) + + // Case 2: Wrong validator + tx, err = env.txBuilder.NewRewardValidatorTx(ids.GenerateTestID()) require.NoError(err) - vdrDestSet := set.Set[ids.ShortID]{} - vdrDestSet.Add(vdrRewardAddress) - delDestSet := set.Set[ids.ShortID]{} - delDestSet.Add(delRewardAddress) + txExecutor = ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + err = tx.Unsigned.Visit(&txExecutor) + require.ErrorIs(err, ErrRemoveWrongStaker) - expectedReward := uint64(1000000) + // Case 3: Happy path + tx, err = env.txBuilder.NewRewardValidatorTx(stakerToRemove.TxID) + require.NoError(err) - oldVdrBalance, err := avax.GetBalance(env.state, vdrDestSet) + onCommitState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - oldDelBalance, err := avax.GetBalance(env.state, delDestSet) + + onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - require.NoError(txExecutor.OnCommitState.Apply(env.state)) - env.state.SetHeight(dummyHeight) - require.NoError(env.state.Commit()) + txExecutor = ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + require.NoError(tx.Unsigned.Visit(&txExecutor)) - // Since the tx was committed, the delegator and the delegatee should be rewarded. - // The delegator reward should be higher since the delegatee's share is 25%. - commitVdrBalance, err := avax.GetBalance(env.state, vdrDestSet) - require.NoError(err) - vdrReward, err := math.Sub(commitVdrBalance, oldVdrBalance) + onAbortStakerIterator, err := txExecutor.OnAbortState.GetCurrentStakerIterator() require.NoError(err) - require.NotZero(vdrReward, "expected delegatee balance to increase because of reward") - commitDelBalance, err := avax.GetBalance(env.state, delDestSet) - require.NoError(err) - delReward, err := math.Sub(commitDelBalance, oldDelBalance) + stakerRemoved := true + for onAbortStakerIterator.Next() { + nextStaker := onAbortStakerIterator.Value() + if nextStaker.TxID == stakerToRemove.TxID { + stakerRemoved = false + break + } + } + onAbortStakerIterator.Release() + require.True(stakerRemoved) + + // check that stake/reward isn't given back + stakeOwners := stakerToRemoveTx.StakeOuts[0].Out.(*secp256k1fx.TransferOutput).AddressesSet() + + // Get old balances + oldBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - require.NotZero(delReward, "expected delegator balance to increase because of reward") - require.Less(vdrReward, delReward, "the delegator's reward should be greater than the delegatee's because the delegatee's share is 25%") - require.Equal(expectedReward, delReward+vdrReward, "expected total reward to be %d but is %d", expectedReward, delReward+vdrReward) + require.NoError(txExecutor.OnAbortState.Apply(env.state)) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) - require.Equal(env.config.MinValidatorStake, vdrSet.GetWeight(vdrNodeID)) + onAbortBalance, err := avax.GetBalance(env.state, stakeOwners) + require.NoError(err) + require.Equal(oldBalance+stakerToRemove.Weight, onAbortBalance) } -func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { +func TestCortinaForkRewardDelegatorTxExecuteOnCommit(t *testing.T) { require := require.New(t) - env := newEnvironment(ContinuousStakingFork) + env := newEnvironment(CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -574,9 +975,9 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { require.Equal(delRewardAmt, delReward+delegateeReward, "expected total reward to be %d but is %d", delRewardAmt, delReward+vdrReward) } -func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { +func TestCortinaForkRewardDelegatorTxAndValidatorTxExecuteOnCommit(t *testing.T) { require := require.New(t) - env := newEnvironment(ContinuousStakingFork) + env := newEnvironment(CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -738,9 +1139,9 @@ func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t * require.Equal(delRewardAmt, delReward+delegateeReward, "expected total reward to be %d but is %d", delRewardAmt, delReward+vdrReward) } -func TestRewardDelegatorTxExecuteOnAbort(t *testing.T) { +func TestCortinaForkRewardDelegatorTxExecuteOnAbort(t *testing.T) { require := require.New(t) - env := newEnvironment(ContinuousStakingFork) + env := newEnvironment(CortinaFork) defer func() { require.NoError(shutdownEnvironment(env)) }() @@ -858,3 +1259,131 @@ func TestRewardDelegatorTxExecuteOnAbort(t *testing.T) { require.NoError(err) require.Equal(initialSupply-expectedReward, newSupply, "should have removed un-rewarded tokens from the potential supply") } + +func TestBanffForkRewardDelegatorTxExecuteOnCommit(t *testing.T) { + require := require.New(t) + env := newEnvironment(BanffFork) + defer func() { + require.NoError(shutdownEnvironment(env)) + }() + dummyHeight := uint64(1) + + vdrRewardAddress := ids.GenerateTestShortID() + delRewardAddress := ids.GenerateTestShortID() + + vdrStartTime := uint64(defaultValidateStartTime.Unix()) + 1 + vdrEndTime := uint64(defaultValidateStartTime.Add(2 * defaultMinStakingDuration).Unix()) + vdrNodeID := ids.GenerateTestNodeID() + + vdrTx, err := env.txBuilder.NewAddValidatorTx( + env.config.MinValidatorStake, // stakeAmt + vdrStartTime, + vdrEndTime, + vdrNodeID, // node ID + vdrRewardAddress, // reward address + reward.PercentDenominator/4, + []*secp256k1.PrivateKey{preFundedKeys[0]}, + ids.ShortEmpty, + ) + require.NoError(err) + + delStartTime := vdrStartTime + delEndTime := vdrEndTime + + delTx, err := env.txBuilder.NewAddDelegatorTx( + env.config.MinDelegatorStake, + delStartTime, + delEndTime, + vdrNodeID, + delRewardAddress, + []*secp256k1.PrivateKey{preFundedKeys[0]}, + ids.ShortEmpty, // Change address + ) + require.NoError(err) + + addValTx := vdrTx.Unsigned.(*txs.AddValidatorTx) + vdrStaker, err := state.NewCurrentStaker( + vdrTx.ID(), + addValTx, + addValTx.StartTime(), + 0, + ) + require.NoError(err) + + addDelTx := delTx.Unsigned.(*txs.AddDelegatorTx) + delStaker, err := state.NewCurrentStaker( + delTx.ID(), + addDelTx, + addDelTx.StartTime(), + 1000000, + ) + require.NoError(err) + + env.state.PutCurrentValidator(vdrStaker) + env.state.AddTx(vdrTx, status.Committed) + env.state.PutCurrentDelegator(delStaker) + env.state.AddTx(delTx, status.Committed) + env.state.SetTimestamp(time.Unix(int64(delEndTime), 0)) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) + + // test validator stake + vdrSet, ok := env.config.Validators.Get(constants.PrimaryNetworkID) + require.True(ok) + + stake := vdrSet.GetWeight(vdrNodeID) + require.Equal(env.config.MinValidatorStake+env.config.MinDelegatorStake, stake) + + tx, err := env.txBuilder.NewRewardValidatorTx(delTx.ID()) + require.NoError(err) + + onCommitState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + onAbortState, err := state.NewDiff(lastAcceptedID, env) + require.NoError(err) + + txExecutor := ProposalTxExecutor{ + OnCommitState: onCommitState, + OnAbortState: onAbortState, + Backend: &env.backend, + Tx: tx, + } + err = tx.Unsigned.Visit(&txExecutor) + require.NoError(err) + + vdrDestSet := set.Set[ids.ShortID]{} + vdrDestSet.Add(vdrRewardAddress) + delDestSet := set.Set[ids.ShortID]{} + delDestSet.Add(delRewardAddress) + + expectedReward := uint64(1000000) + + oldVdrBalance, err := avax.GetBalance(env.state, vdrDestSet) + require.NoError(err) + oldDelBalance, err := avax.GetBalance(env.state, delDestSet) + require.NoError(err) + + require.NoError(txExecutor.OnCommitState.Apply(env.state)) + env.state.SetHeight(dummyHeight) + require.NoError(env.state.Commit()) + + // Since the tx was committed, the delegator and the delegatee should be rewarded. + // The delegator reward should be higher since the delegatee's share is 25%. + commitVdrBalance, err := avax.GetBalance(env.state, vdrDestSet) + require.NoError(err) + vdrReward, err := math.Sub(commitVdrBalance, oldVdrBalance) + require.NoError(err) + require.NotZero(vdrReward, "expected delegatee balance to increase because of reward") + + commitDelBalance, err := avax.GetBalance(env.state, delDestSet) + require.NoError(err) + delReward, err := math.Sub(commitDelBalance, oldDelBalance) + require.NoError(err) + require.NotZero(delReward, "expected delegator balance to increase because of reward") + + require.Less(vdrReward, delReward, "the delegator's reward should be greater than the delegatee's because the delegatee's share is 25%") + require.Equal(expectedReward, delReward+vdrReward, "expected total reward to be %d but is %d", expectedReward, delReward+vdrReward) + + require.Equal(env.config.MinValidatorStake, vdrSet.GetWeight(vdrNodeID)) +} diff --git a/vms/platformvm/txs/executor/staker_tx_verification.go b/vms/platformvm/txs/executor/staker_tx_verification.go index 2b94bb20c2f7..a619aebb2cdd 100644 --- a/vms/platformvm/txs/executor/staker_tx_verification.go +++ b/vms/platformvm/txs/executor/staker_tx_verification.go @@ -14,7 +14,10 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) @@ -39,6 +42,7 @@ var ( ErrDuplicateValidator = errors.New("duplicate validator") ErrDelegateToPermissionedValidator = errors.New("delegation to permissioned validator") ErrWrongStakedAssetID = errors.New("incorrect staked assetID") + ErrUnauthorizedStakerStopping = errors.New("unauthorized staker stopping") ) // verifyAddValidatorTx carries out the validation for an AddValidatorTx. @@ -58,8 +62,7 @@ func verifyAddValidatorTx( return nil, err } - duration := tx.Validator.Duration() - + duration := tx.StakingPeriod() switch { case tx.Validator.Wght < backend.Config.MinValidatorStake: // Ensure validator is staking at least the minimum amount @@ -123,7 +126,7 @@ func verifyAddValidatorTx( currentTimestamp := chainState.GetTimestamp() startTime := tx.StartTime() if backend.Config.IsContinuousStakingActivated(currentTimestamp) { - if startTime != state.StakerZeroTime { + if startTime != txs.StakerZeroTime { return nil, fmt.Errorf( "%w: %s", ErrStartTimeMustBeZero, @@ -158,37 +161,55 @@ func verifyAddValidatorTx( // verifyAddSubnetValidatorTx carries out the validation for an // AddSubnetValidatorTx. +// Returns the primary network validator EndTime, which bounds +// subnet staker endTime func verifyAddSubnetValidatorTx( backend *Backend, chainState state.Chain, sTx *txs.Tx, tx *txs.AddSubnetValidatorTx, -) error { +) (time.Time, error) { // Verify the tx is well-formed if err := sTx.SyntacticVerify(backend.Ctx); err != nil { - return err + return time.Time{}, err } - duration := tx.Validator.Duration() + stakingPeriod := tx.StakingPeriod() switch { - case duration < backend.Config.MinStakeDuration: + case stakingPeriod < backend.Config.MinStakeDuration: // Ensure staking length is not too short - return ErrStakeTooShort + return time.Time{}, ErrStakeTooShort - case duration > backend.Config.MaxStakeDuration: + case stakingPeriod > backend.Config.MaxStakeDuration: // Ensure staking length is not too long - return ErrStakeTooLong + return time.Time{}, ErrStakeTooLong + } + + primaryNetworkValidator, err := GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID) + if err == database.ErrNotFound { + return time.Time{}, fmt.Errorf( + "%s %w of the primary network", + tx.Validator.NodeID, + ErrNotValidator, + ) + } + if err != nil { + return time.Time{}, fmt.Errorf( + "failed to fetch the primary network validator for %s: %w", + tx.Validator.NodeID, + err, + ) } if !backend.Bootstrapped.Get() { - return nil + return primaryNetworkValidator.EndTime, nil } - currentTimestamp := chainState.GetTimestamp() + currentChainTime := chainState.GetTimestamp() startTime := tx.StartTime() - if backend.Config.IsContinuousStakingActivated(currentTimestamp) { - if startTime != state.StakerZeroTime { - return fmt.Errorf( + if backend.Config.IsContinuousStakingActivated(currentChainTime) { + if startTime != txs.StakerZeroTime { + return time.Time{}, fmt.Errorf( "%w: %s", ErrStartTimeMustBeZero, startTime, @@ -196,19 +217,18 @@ func verifyAddSubnetValidatorTx( } } else { // Ensure the proposed validator starts after the current timestamp - if !currentTimestamp.Before(startTime) { - return fmt.Errorf( + if !currentChainTime.Before(startTime) { + return time.Time{}, fmt.Errorf( "%w: %s >= %s", ErrTimestampNotBeforeStartTime, - currentTimestamp, + currentChainTime, startTime, ) } } - _, err := GetValidator(chainState, tx.SubnetValidator.Subnet, tx.Validator.NodeID) - if err == nil { - return fmt.Errorf( + if _, err = GetValidator(chainState, tx.SubnetValidator.Subnet, tx.Validator.NodeID); err == nil { + return time.Time{}, fmt.Errorf( "attempted to issue %w for %s on subnet %s", ErrDuplicateValidator, tx.Validator.NodeID, @@ -216,43 +236,34 @@ func verifyAddSubnetValidatorTx( ) } if err != database.ErrNotFound { - return fmt.Errorf( + return time.Time{}, fmt.Errorf( "failed to find whether %s is a subnet validator: %w", tx.Validator.NodeID, err, ) } - primaryNetworkValidator, err := GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID) - if err == database.ErrNotFound { - return fmt.Errorf( - "%s %w of the primary network", - tx.Validator.NodeID, - ErrNotValidator, - ) - } - if err != nil { - return fmt.Errorf( - "failed to fetch the primary network validator for %s: %w", - tx.Validator.NodeID, - err, - ) - } + if backend.Config.IsContinuousStakingActivated(currentChainTime) { + if stakingPeriod > primaryNetworkValidator.StakingPeriod { + return time.Time{}, ErrValidatorSubset + } - if backend.Config.IsContinuousStakingActivated(currentTimestamp) { - primaryNetworkValDuration := primaryNetworkValidator.EndTime.Sub(primaryNetworkValidator.StartTime) - if tx.Validator.Duration() > primaryNetworkValDuration { - return ErrValidatorSubset + // TODO ABENEGIA: we assume that the subnet validator may be accepted + // if its primary network counterpart will validate for at least another + // period. We may change this + firstStakinPeriodEndTime := currentChainTime.Add(stakingPeriod) + if firstStakinPeriodEndTime.After(primaryNetworkValidator.EndTime) { + return time.Time{}, ErrValidatorSubset } } else if !tx.Validator.BoundedBy(primaryNetworkValidator.StartTime, primaryNetworkValidator.EndTime) { // Ensure that the period this validator validates the specified subnet // is a subset of the time they validate the primary network. - return ErrValidatorSubset + return time.Time{}, ErrValidatorSubset } baseTxCreds, err := verifyPoASubnetAuthorization(backend, chainState, sTx, tx.SubnetValidator.Subnet, tx.SubnetAuth) if err != nil { - return err + return time.Time{}, err } // Verify the flowcheck @@ -266,7 +277,7 @@ func verifyAddSubnetValidatorTx( backend.Ctx.AVAXAssetID: backend.Config.AddSubnetValidatorFee, }, ); err != nil { - return fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) + return time.Time{}, fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) } // Make sure the tx doesn't start too far in the future. This is done last @@ -275,12 +286,12 @@ func verifyAddSubnetValidatorTx( // However Post Continuous Staking fork txs are guaranteed to satisfy the test // (start time is zero). I didn't bother guarding the check (which must be the // last one made). - maxStartTime := currentTimestamp.Add(MaxFutureStartTime) + maxStartTime := currentChainTime.Add(MaxFutureStartTime) if startTime.After(maxStartTime) { - return ErrFutureStakeTime + return time.Time{}, ErrFutureStakeTime } - return nil + return primaryNetworkValidator.EndTime, nil } // Returns the representation of [tx.NodeID] validating [tx.Subnet]. @@ -352,7 +363,7 @@ func removeSubnetValidatorValidation( // verifyAddDelegatorTx carries out the validation for an AddDelegatorTx. // It returns the tx outputs that should be returned if this delegator is not -// added to the staking set. +// added to the staking set; moreover it returns the primary validator end time. func verifyAddDelegatorTx( backend *Backend, chainState state.Chain, @@ -360,41 +371,51 @@ func verifyAddDelegatorTx( tx *txs.AddDelegatorTx, ) ( []*avax.TransferableOutput, + time.Time, error, ) { // Verify the tx is well-formed if err := sTx.SyntacticVerify(backend.Ctx); err != nil { - return nil, err + return nil, time.Time{}, err } - duration := tx.Validator.Duration() + duration := tx.StakingPeriod() switch { case duration < backend.Config.MinStakeDuration: // Ensure staking length is not too short - return nil, ErrStakeTooShort + return nil, time.Time{}, ErrStakeTooShort case duration > backend.Config.MaxStakeDuration: // Ensure staking length is not too long - return nil, ErrStakeTooLong + return nil, time.Time{}, ErrStakeTooLong case tx.Validator.Wght < backend.Config.MinDelegatorStake: // Ensure validator is staking at least the minimum amount - return nil, ErrWeightTooSmall + return nil, time.Time{}, ErrWeightTooSmall } outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) copy(outs, tx.Outs) copy(outs[len(tx.Outs):], tx.StakeOuts) + primaryNetworkValidator, err := GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID) + if err != nil { + return nil, time.Time{}, fmt.Errorf( + "failed to fetch the primary network validator for %s: %w", + tx.Validator.NodeID, + err, + ) + } + if !backend.Bootstrapped.Get() { - return outs, nil + return outs, primaryNetworkValidator.EndTime, nil } currentTimestamp := chainState.GetTimestamp() startTime := tx.StartTime() if backend.Config.IsContinuousStakingActivated(currentTimestamp) { - if startTime != state.StakerZeroTime { - return nil, fmt.Errorf( + if startTime != txs.StakerZeroTime { + return nil, time.Time{}, fmt.Errorf( "%w: %s", ErrStartTimeMustBeZero, startTime, @@ -403,7 +424,7 @@ func verifyAddDelegatorTx( } else { // Ensure the proposed validator starts after the current timestamp if !currentTimestamp.Before(startTime) { - return nil, fmt.Errorf( + return nil, time.Time{}, fmt.Errorf( "%w: %s >= %s", ErrTimestampNotBeforeStartTime, currentTimestamp, @@ -412,18 +433,9 @@ func verifyAddDelegatorTx( } } - primaryNetworkValidator, err := GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID) - if err != nil { - return nil, fmt.Errorf( - "failed to fetch the primary network validator for %s: %w", - tx.Validator.NodeID, - err, - ) - } - maximumWeight, err := math.Mul64(MaxValidatorWeightFactor, primaryNetworkValidator.Weight) if err != nil { - return nil, ErrStakeOverflow + return nil, time.Time{}, ErrStakeOverflow } if backend.Config.IsApricotPhase3Activated(currentTimestamp) { @@ -439,15 +451,15 @@ func verifyAddDelegatorTx( newStaker, err = state.NewPendingStaker(txID, tx) } if err != nil { - return nil, err + return nil, time.Time{}, err } canDelegate, err := canDelegate(chainState, primaryNetworkValidator, maximumWeight, newStaker) if err != nil { - return nil, err + return nil, time.Time{}, err } if !canDelegate { - return nil, ErrOverDelegated + return nil, time.Time{}, ErrOverDelegated } // Verify the flowcheck @@ -461,7 +473,7 @@ func verifyAddDelegatorTx( backend.Ctx.AVAXAssetID: backend.Config.AddPrimaryNetworkDelegatorFee, }, ); err != nil { - return nil, fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) + return nil, time.Time{}, fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) } // Make sure the tx doesn't start too far in the future. This is done last @@ -472,10 +484,10 @@ func verifyAddDelegatorTx( // last one made). maxStartTime := currentTimestamp.Add(MaxFutureStartTime) if startTime.After(maxStartTime) { - return nil, ErrFutureStakeTime + return nil, time.Time{}, ErrFutureStakeTime } - return outs, nil + return outs, primaryNetworkValidator.EndTime, nil } // verifyAddPermissionlessValidatorTx carries out the validation for an @@ -485,21 +497,38 @@ func verifyAddPermissionlessValidatorTx( chainState state.Chain, sTx *txs.Tx, tx *txs.AddPermissionlessValidatorTx, -) error { +) (time.Time, error) { // Verify the tx is well-formed if err := sTx.SyntacticVerify(backend.Ctx); err != nil { - return err + return time.Time{}, err + } + + var ( + primaryNetworkValidator *state.Staker + primaryValidatorEndTime = mockable.MaxTime + err error + ) + if tx.Subnet != constants.PlatformChainID { + primaryNetworkValidator, err = GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID) + if err != nil { + return time.Time{}, fmt.Errorf( + "failed to fetch the primary network validator for %s: %w", + tx.Validator.NodeID, + err, + ) + } + primaryValidatorEndTime = primaryNetworkValidator.EndTime } if !backend.Bootstrapped.Get() { - return nil + return primaryValidatorEndTime, nil } - currentTimestamp := chainState.GetTimestamp() + currentChainTime := chainState.GetTimestamp() startTime := tx.StartTime() - if backend.Config.IsContinuousStakingActivated(currentTimestamp) { - if startTime != state.StakerZeroTime { - return fmt.Errorf( + if backend.Config.IsContinuousStakingActivated(currentChainTime) { + if startTime != txs.StakerZeroTime { + return time.Time{}, fmt.Errorf( "%w: %s", ErrStartTimeMustBeZero, startTime, @@ -507,11 +536,11 @@ func verifyAddPermissionlessValidatorTx( } } else { // Ensure the proposed validator starts after the current time - if !currentTimestamp.Before(startTime) { - return fmt.Errorf( + if !currentChainTime.Before(startTime) { + return time.Time{}, fmt.Errorf( "%w: %s >= %s", ErrTimestampNotBeforeStartTime, - currentTimestamp, + currentChainTime, startTime, ) } @@ -519,35 +548,37 @@ func verifyAddPermissionlessValidatorTx( validatorRules, err := getValidatorRules(backend, chainState, tx.Subnet) if err != nil { - return err + return time.Time{}, err } - duration := tx.Validator.Duration() - stakedAssetID := tx.StakeOuts[0].AssetID() + var ( + stakingPeriod = tx.StakingPeriod() + stakedAssetID = tx.StakeOuts[0].AssetID() + ) switch { case tx.Validator.Wght < validatorRules.minValidatorStake: // Ensure validator is staking at least the minimum amount - return ErrWeightTooSmall + return time.Time{}, ErrWeightTooSmall case tx.Validator.Wght > validatorRules.maxValidatorStake: // Ensure validator isn't staking too much - return ErrWeightTooLarge + return time.Time{}, ErrWeightTooLarge case tx.DelegationShares < validatorRules.minDelegationFee: // Ensure the validator fee is at least the minimum amount - return ErrInsufficientDelegationFee + return time.Time{}, ErrInsufficientDelegationFee - case duration < validatorRules.minStakeDuration: + case stakingPeriod < validatorRules.minStakeDuration: // Ensure staking length is not too short - return ErrStakeTooShort + return time.Time{}, ErrStakeTooShort - case duration > validatorRules.maxStakeDuration: + case stakingPeriod > validatorRules.maxStakeDuration: // Ensure staking length is not too long - return ErrStakeTooLong + return time.Time{}, ErrStakeTooLong case stakedAssetID != validatorRules.assetID: // Wrong assetID used - return fmt.Errorf( + return time.Time{}, fmt.Errorf( "%w: %s != %s", ErrWrongStakedAssetID, validatorRules.assetID, @@ -557,7 +588,7 @@ func verifyAddPermissionlessValidatorTx( _, err = GetValidator(chainState, tx.Subnet, tx.Validator.NodeID) if err == nil { - return fmt.Errorf( + return time.Time{}, fmt.Errorf( "%w: %s on %s", ErrDuplicateValidator, tx.Validator.NodeID, @@ -565,7 +596,7 @@ func verifyAddPermissionlessValidatorTx( ) } if err != database.ErrNotFound { - return fmt.Errorf( + return time.Time{}, fmt.Errorf( "failed to find whether %s is a validator on %s: %w", tx.Validator.NodeID, tx.Subnet, @@ -575,23 +606,22 @@ func verifyAddPermissionlessValidatorTx( var txFee uint64 if tx.Subnet != constants.PrimaryNetworkID { - primaryNetworkValidator, err := GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID) - if err != nil { - return fmt.Errorf( - "failed to fetch the primary network validator for %s: %w", - tx.Validator.NodeID, - err, - ) - } + if backend.Config.IsContinuousStakingActivated(currentChainTime) { + if stakingPeriod > primaryNetworkValidator.StakingPeriod { + return time.Time{}, ErrValidatorSubset + } - if backend.Config.IsContinuousStakingActivated(currentTimestamp) { - if tx.Validator.Duration() > primaryNetworkValidator.Duration() { - return ErrValidatorSubset + // TODO ABENEGIA: we assume that the subnet validator may be accepted + // if its primary network counterpart will validate for at least another + // period. We may change this + candidateEndTime := currentChainTime.Add(stakingPeriod) + if candidateEndTime.After(primaryNetworkValidator.EndTime) { + return time.Time{}, ErrValidatorSubset } } else if !tx.Validator.BoundedBy(primaryNetworkValidator.StartTime, primaryNetworkValidator.EndTime) { // Ensure that the period this validator validates the specified subnet // is a subset of the time they validate the primary network. - return ErrValidatorSubset + return time.Time{}, ErrValidatorSubset } txFee = backend.Config.AddSubnetValidatorFee @@ -614,7 +644,7 @@ func verifyAddPermissionlessValidatorTx( backend.Ctx.AVAXAssetID: txFee, }, ); err != nil { - return fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) + return time.Time{}, fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) } // Make sure the tx doesn't start too far in the future. This is done last @@ -623,12 +653,12 @@ func verifyAddPermissionlessValidatorTx( // However Post Continuous Staking fork txs are guaranteed to satisfy the test // (start time is zero). I didn't bother guarding the check (which must be the // last one made). - maxStartTime := currentTimestamp.Add(MaxFutureStartTime) + maxStartTime := currentChainTime.Add(MaxFutureStartTime) if startTime.After(maxStartTime) { - return ErrFutureStakeTime + return time.Time{}, ErrFutureStakeTime } - return nil + return primaryValidatorEndTime, nil } type addValidatorRules struct { @@ -682,21 +712,31 @@ func verifyAddPermissionlessDelegatorTx( chainState state.Chain, sTx *txs.Tx, tx *txs.AddPermissionlessDelegatorTx, -) error { +) (time.Time, error) { // Verify the tx is well-formed if err := sTx.SyntacticVerify(backend.Ctx); err != nil { - return err + return time.Time{}, err + } + + validator, err := GetValidator(chainState, tx.Subnet, tx.Validator.NodeID) + if err != nil { + return time.Time{}, fmt.Errorf( + "failed to fetch the validator for %s on %s: %w", + tx.Validator.NodeID, + tx.Subnet, + err, + ) } if !backend.Bootstrapped.Get() { - return nil + return validator.EndTime, nil } currentTimestamp := chainState.GetTimestamp() startTime := tx.StartTime() if backend.Config.IsContinuousStakingActivated(currentTimestamp) { - if startTime != state.StakerZeroTime { - return fmt.Errorf( + if startTime != txs.StakerZeroTime { + return time.Time{}, fmt.Errorf( "%w: %s", ErrStartTimeMustBeZero, startTime, @@ -705,7 +745,7 @@ func verifyAddPermissionlessDelegatorTx( } else { // Ensure the proposed validator starts after the current timestamp if !currentTimestamp.Before(startTime) { - return fmt.Errorf( + return time.Time{}, fmt.Errorf( "chain timestamp (%s) not before validator's start time (%s)", currentTimestamp, startTime, @@ -715,27 +755,29 @@ func verifyAddPermissionlessDelegatorTx( delegatorRules, err := getDelegatorRules(backend, chainState, tx.Subnet) if err != nil { - return err + return time.Time{}, err } - duration := tx.Validator.Duration() - stakedAssetID := tx.StakeOuts[0].AssetID() + var ( + duration = tx.StakingPeriod() + stakedAssetID = tx.StakeOuts[0].AssetID() + ) switch { case tx.Validator.Wght < delegatorRules.minDelegatorStake: // Ensure delegator is staking at least the minimum amount - return ErrWeightTooSmall + return time.Time{}, ErrWeightTooSmall case duration < delegatorRules.minStakeDuration: // Ensure staking length is not too short - return ErrStakeTooShort + return time.Time{}, ErrStakeTooShort case duration > delegatorRules.maxStakeDuration: // Ensure staking length is not too long - return ErrStakeTooLong + return time.Time{}, ErrStakeTooLong case stakedAssetID != delegatorRules.assetID: // Wrong assetID used - return fmt.Errorf( + return time.Time{}, fmt.Errorf( "%w: %s != %s", ErrWrongStakedAssetID, delegatorRules.assetID, @@ -743,16 +785,6 @@ func verifyAddPermissionlessDelegatorTx( ) } - validator, err := GetValidator(chainState, tx.Subnet, tx.Validator.NodeID) - if err != nil { - return fmt.Errorf( - "failed to fetch the validator for %s on %s: %w", - tx.Validator.NodeID, - tx.Subnet, - err, - ) - } - maximumWeight, err := math.Mul64( uint64(delegatorRules.maxValidatorWeightFactor), validator.Weight, @@ -771,15 +803,15 @@ func verifyAddPermissionlessDelegatorTx( newStaker, err = state.NewPendingStaker(txID, tx) } if err != nil { - return err + return time.Time{}, err } canDelegate, err := canDelegate(chainState, validator, maximumWeight, newStaker) if err != nil { - return err + return time.Time{}, err } if !canDelegate { - return ErrOverDelegated + return time.Time{}, ErrOverDelegated } outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts)) @@ -795,7 +827,7 @@ func verifyAddPermissionlessDelegatorTx( // permissioned validator, so we verify this delegator is // pointing to a permissionless validator. if validator.Priority.IsPermissionedValidator() { - return ErrDelegateToPermissionedValidator + return time.Time{}, ErrDelegateToPermissionedValidator } txFee = backend.Config.AddSubnetDelegatorFee @@ -814,7 +846,7 @@ func verifyAddPermissionlessDelegatorTx( backend.Ctx.AVAXAssetID: txFee, }, ); err != nil { - return fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) + return time.Time{}, fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) } // Make sure the tx doesn't start too far in the future. This is done last @@ -825,10 +857,10 @@ func verifyAddPermissionlessDelegatorTx( // last one made). maxStartTime := currentTimestamp.Add(MaxFutureStartTime) if startTime.After(maxStartTime) { - return ErrFutureStakeTime + return time.Time{}, ErrFutureStakeTime } - return nil + return validator.EndTime, nil } type addDelegatorRules struct { @@ -874,3 +906,145 @@ func getDelegatorRules( maxValidatorWeightFactor: transformSubnet.MaxValidatorWeightFactor, }, nil } + +func verifyStopStakerTx( + backend *Backend, + chainState state.Chain, + sTx *txs.Tx, + tx *txs.StopStakerTx, +) ([]*state.Staker, time.Time, error) { + if !backend.Config.IsContinuousStakingActivated(chainState.GetTimestamp()) { + return nil, time.Time{}, errors.New("StopStakerTx cannot be accepted before continuous staking fork activation") + } + + // Verify the tx is well-formed + if err := sTx.SyntacticVerify(backend.Ctx); err != nil { + return nil, time.Time{}, err + } + + // retrieve staker to be stopped + var ( + txID = tx.TxID + stakerToStop *state.Staker + ) + + stakersIt, err := chainState.GetCurrentStakerIterator() + if err != nil { + stakersIt.Release() + return nil, time.Time{}, err + } + for stakersIt.Next() { + if stakersIt.Value().TxID == txID { + stakerToStop = stakersIt.Value() + break + } + } + stakersIt.Release() + if stakerToStop == nil { + return nil, time.Time{}, errors.New("could not find staker to stop among current ones") + } + + if backend.Bootstrapped.Get() { + // Full verification only one bootstrapping is done. Otherwise only execution + + baseTxCreds, err := verifyStopStakerAuthorization(backend, chainState, sTx, txID, tx.StakerAuth) + if err != nil { + return nil, time.Time{}, err + } + + // Verify the flowcheck + if err := backend.FlowChecker.VerifySpend( + tx, + chainState, + tx.Ins, + tx.Outs, + baseTxCreds, + map[ids.ID]uint64{ + backend.Ctx.AVAXAssetID: backend.Config.TxFee, + }, + ); err != nil { + return nil, time.Time{}, fmt.Errorf("%w: %v", ErrFlowCheckFailed, err) + } + } + + if !stakerToStop.Priority.IsValidator() || stakerToStop.SubnetID != constants.PrimaryNetworkID { + return []*state.Staker{stakerToStop}, stakerToStop.EarliestStopTime(), nil + } + + // primary network validators are special since, when stopping them, we need to handle + // their delegators and subnet validators as well, to make sure they don't outlive the + // primary network validators + res := []*state.Staker{stakerToStop} + stakersIt, err = chainState.GetCurrentStakerIterator() + if err != nil { + stakersIt.Release() + return nil, time.Time{}, err + } + for stakersIt.Next() { + staker := stakersIt.Value() + if staker.NodeID == stakerToStop.NodeID && staker.TxID != stakerToStop.TxID { + res = append(res, staker) + } + } + stakersIt.Release() + return res, stakerToStop.EarliestStopTime(), nil +} + +func verifyStopStakerAuthorization( + backend *Backend, + chainState state.Chain, + sTx *txs.Tx, + stakerTxID ids.ID, + stakerAuth verify.Verifiable, +) ([]verify.Verifiable, error) { + if len(sTx.Creds) == 0 { + // Ensure there is at least one credential for the subnet authorization + return nil, errWrongNumberOfCredentials + } + + baseTxCredsLen := len(sTx.Creds) - 1 + stakerCred := sTx.Creds[baseTxCredsLen] + + stakerTx, _, err := chainState.GetTx(stakerTxID) + if err != nil { + return nil, fmt.Errorf( + "staker tx not found %q: %v", + stakerTxID, + err, + ) + } + + var stakerOwner fx.Owner + switch uStakerTx := stakerTx.Unsigned.(type) { + case txs.ValidatorTx: + stakerOwner = uStakerTx.ValidationRewardsOwner() + case txs.DelegatorTx: + stakerOwner = uStakerTx.RewardsOwner() + case *txs.AddSubnetValidatorTx: + signedSubnetTx, _, err := chainState.GetTx(uStakerTx.Subnet) + if err != nil { + return nil, fmt.Errorf( + "tx creating subnet not found %q: %v", + uStakerTx.Subnet, + err, + ) + } + subnetTx, ok := signedSubnetTx.Unsigned.(*txs.CreateSubnetTx) + if !ok { + return nil, ErrWrongTxType + } + stakerOwner = subnetTx.Owner + default: + return nil, fmt.Errorf( + "unhandled staker type: %t", + uStakerTx, + ) + } + + err = backend.Fx.VerifyPermission(sTx.Unsigned, stakerAuth, stakerCred, stakerOwner) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrUnauthorizedStakerStopping, err) + } + + return sTx.Creds[:baseTxCredsLen], nil +} diff --git a/vms/platformvm/txs/executor/staker_tx_verification_test.go b/vms/platformvm/txs/executor/staker_tx_verification_test.go index 3c40de677c46..eb72c9312a6c 100644 --- a/vms/platformvm/txs/executor/staker_tx_verification_test.go +++ b/vms/platformvm/txs/executor/staker_tx_verification_test.go @@ -127,13 +127,19 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - return nil + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx }, txF: func() *txs.AddPermissionlessValidatorTx { - return nil + return &verifiedTx }, expectedErr: nil, }, @@ -151,10 +157,15 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - state := state.NewMockChain(ctrl) - state.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) - state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) - return state + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + s.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) + s.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx @@ -180,10 +191,15 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - state := state.NewMockChain(ctrl) - state.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) - state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) - return state + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + s.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) + s.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx @@ -209,10 +225,15 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - state := state.NewMockChain(ctrl) - state.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) - state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) - return state + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + s.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) + s.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx @@ -239,10 +260,15 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - state := state.NewMockChain(ctrl) - state.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) - state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) - return state + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + s.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) + s.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx @@ -272,10 +298,15 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - state := state.NewMockChain(ctrl) - state.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) - state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) - return state + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + s.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) + s.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx @@ -305,10 +336,15 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - state := state.NewMockChain(ctrl) - state.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) - state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) - return state + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + s.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) + s.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx @@ -340,12 +376,17 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { } }, stateF: func(ctrl *gomock.Controller) state.Chain { - state := state.NewMockChain(ctrl) - state.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) - state.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) + s := state.NewMockChain(ctrl) + primaryValidatorMock := &state.Staker{ + SubnetID: constants.PrimaryNetworkID, + EndTime: mockable.MaxTime, + } + s.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, gomock.Any()).Return(primaryValidatorMock, nil) + s.EXPECT().GetTimestamp().Return(time.Unix(0, 0)) + s.EXPECT().GetSubnetTransformation(subnetID).Return(&transformTx, nil) // State says validator exists - state.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, nil) - return state + s.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, nil) + return s }, sTxF: func() *txs.Tx { return &verifiedSignedTx @@ -376,8 +417,9 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { mockState.EXPECT().GetPendingValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound) // Validator time isn't subset of primary network validator time primaryNetworkVdr := &state.Staker{ - StartTime: verifiedTx.StartTime(), - EndTime: verifiedTx.EndTime().Add(-1 * time.Second), + StartTime: verifiedTx.StartTime(), + StakingPeriod: verifiedTx.StakingPeriod() - 1, + EndTime: verifiedTx.EndTime().Add(-1 * time.Second), } mockState.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, verifiedTx.NodeID()).Return(primaryNetworkVdr, nil) return mockState @@ -423,8 +465,9 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { mockState.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound) mockState.EXPECT().GetPendingValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound) primaryNetworkVdr := &state.Staker{ - StartTime: verifiedTx.StartTime(), - EndTime: verifiedTx.EndTime(), + StartTime: verifiedTx.StartTime(), + StakingPeriod: verifiedTx.StakingPeriod(), + EndTime: verifiedTx.EndTime(), } mockState.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, verifiedTx.NodeID()).Return(primaryNetworkVdr, nil) return mockState @@ -470,8 +513,9 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { mockState.EXPECT().GetCurrentValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound) mockState.EXPECT().GetPendingValidator(subnetID, verifiedTx.NodeID()).Return(nil, database.ErrNotFound) primaryNetworkVdr := &state.Staker{ - StartTime: time.Unix(0, 0), - EndTime: mockable.MaxTime, + StartTime: time.Unix(0, 0), + StakingPeriod: txs.StakerMaxDuration, + EndTime: mockable.MaxTime, } mockState.EXPECT().GetCurrentValidator(constants.PrimaryNetworkID, verifiedTx.NodeID()).Return(primaryNetworkVdr, nil) return mockState @@ -498,7 +542,7 @@ func TestVerifyAddPermissionlessValidatorTx(t *testing.T) { tx = tt.txF() ) - err := verifyAddPermissionlessValidatorTx(backend, state, sTx, tx) + _, err := verifyAddPermissionlessValidatorTx(backend, state, sTx, tx) require.ErrorIs(t, err, tt.expectedErr) }) } diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index a3be9b9c69d2..08a8056557a5 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/reward" @@ -284,7 +285,7 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { chainTime = e.State.GetTimestamp() ) - staker, err := e.addStakerFromStakerTx(tx, chainTime) + staker, err := e.addStakerFromStakerTx(tx, chainTime, mockable.MaxTime) if err != nil { return err } @@ -301,12 +302,13 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { } func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { - if err := verifyAddSubnetValidatorTx( + primaryValidatorEndTime, err := verifyAddSubnetValidatorTx( e.Backend, e.State, e.Tx, tx, - ); err != nil { + ) + if err != nil { return err } @@ -315,7 +317,7 @@ func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) chainTime = e.State.GetTimestamp() ) - staker, err := e.addStakerFromStakerTx(tx, chainTime) + staker, err := e.addStakerFromStakerTx(tx, chainTime, primaryValidatorEndTime) if err != nil { return err } @@ -331,12 +333,13 @@ func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) } func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { - if _, err := verifyAddDelegatorTx( + _, primaryValidatorEndTime, err := verifyAddDelegatorTx( e.Backend, e.State, e.Tx, tx, - ); err != nil { + ) + if err != nil { return err } @@ -345,7 +348,7 @@ func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { chainTime = e.State.GetTimestamp() ) - staker, err := e.addStakerFromStakerTx(tx, chainTime) + staker, err := e.addStakerFromStakerTx(tx, chainTime, primaryValidatorEndTime) if err != nil { return err } @@ -439,12 +442,13 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error } func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - if err := verifyAddPermissionlessValidatorTx( + primaryValidatorEndTime, err := verifyAddPermissionlessValidatorTx( e.Backend, e.State, e.Tx, tx, - ); err != nil { + ) + if err != nil { return err } @@ -453,7 +457,7 @@ func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionl chainTime = e.State.GetTimestamp() ) - staker, err := e.addStakerFromStakerTx(tx, chainTime) + staker, err := e.addStakerFromStakerTx(tx, chainTime, primaryValidatorEndTime) if err != nil { return err } @@ -470,12 +474,13 @@ func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionl } func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - if err := verifyAddPermissionlessDelegatorTx( + primaryValidatorEndTime, err := verifyAddPermissionlessDelegatorTx( e.Backend, e.State, e.Tx, tx, - ); err != nil { + ) + if err != nil { return err } @@ -484,7 +489,7 @@ func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionl chainTime = e.State.GetTimestamp() ) - staker, err := e.addStakerFromStakerTx(tx, chainTime) + staker, err := e.addStakerFromStakerTx(tx, chainTime, primaryValidatorEndTime) if err != nil { return err } @@ -501,9 +506,40 @@ func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionl return nil } +func (e *StandardTxExecutor) StopStakerTx(tx *txs.StopStakerTx) error { + stakers, stopTime, err := verifyStopStakerTx( + e.Backend, + e.State, + e.Tx, + tx, + ) + if err != nil { + return err + } + + for _, toStop := range stakers { + state.MarkStakerForRemovalInPlaceBeforeTime(toStop, stopTime) + if toStop.Priority.IsValidator() { + err = e.State.UpdateCurrentValidator(toStop) + } else { + err = e.State.UpdateCurrentDelegator(toStop) + } + if err != nil { + return err + } + } + + txID := e.Tx.ID() + avax.Consume(e.State, tx.Ins) + avax.Produce(e.State, txID, tx.Outs) + + return nil +} + func (e *StandardTxExecutor) addStakerFromStakerTx( stakerTx txs.Staker, chainTime time.Time, + endTimeBound time.Time, ) (*state.Staker, error) { // Pre Continuous Staking fork, stakers are added as pending first, them promoted // to current when chainTime reach their start time. @@ -517,7 +553,7 @@ func (e *StandardTxExecutor) addStakerFromStakerTx( var ( potentialReward = uint64(0) - stakeDuration = stakerTx.Duration() + stakeDuration = stakerTx.StakingPeriod() ) if stakerTx.CurrentPriority() != txs.SubnetPermissionedValidatorCurrentPriority { currentSupply, err := e.State.GetCurrentSupply(stakerTx.SubnetID()) @@ -540,5 +576,11 @@ func (e *StandardTxExecutor) addStakerFromStakerTx( updatedSupply := currentSupply + potentialReward e.State.SetCurrentSupply(stakerTx.SubnetID(), updatedSupply) } - return state.NewCurrentStaker(txID, stakerTx, chainTime, potentialReward) + staker, err := state.NewCurrentStaker(txID, stakerTx, chainTime, potentialReward) + if err != nil { + return nil, err + } + + state.MarkStakerForRemovalInPlaceBeforeTime(staker, endTimeBound) + return staker, nil } diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index d389c07dd07b..da7c78728942 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/hashing" + "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/config" @@ -1035,7 +1036,9 @@ func TestStandardTxExecutorContinuousAddValidator(t *testing.T) { require.Equal(addValTx.ID(), val.TxID) require.Equal(env.state.GetTimestamp(), val.StartTime) - require.Equal(val.StartTime.Add(validatorDuration), val.EndTime) + require.Equal(validatorDuration, val.StakingPeriod) + require.Equal(val.StartTime.Add(val.StakingPeriod), val.NextTime) + require.Equal(mockable.MaxTime, val.EndTime) } // Returns a RemoveSubnetValidatorTx that passes syntactic verification. diff --git a/vms/platformvm/txs/executor/state_changes.go b/vms/platformvm/txs/executor/state_changes.go index c63c51310107..72f52ff6b7c0 100644 --- a/vms/platformvm/txs/executor/state_changes.go +++ b/vms/platformvm/txs/executor/state_changes.go @@ -158,7 +158,7 @@ func AdvanceTimeTo( rewards := reward.NewCalculator(rewardsCfg) potentialReward := rewards.Calculate( - stakerToRemove.EndTime.Sub(stakerToRemove.StartTime), + stakerToRemove.StakingPeriod, stakerToRemove.Weight, supply, ) diff --git a/vms/platformvm/txs/executor/tx_mempool_verifier.go b/vms/platformvm/txs/executor/tx_mempool_verifier.go index 3f6676c813c6..060f7a94f254 100644 --- a/vms/platformvm/txs/executor/tx_mempool_verifier.go +++ b/vms/platformvm/txs/executor/tx_mempool_verifier.go @@ -74,6 +74,10 @@ func (v *MempoolTxVerifier) AddPermissionlessDelegatorTx(tx *txs.AddPermissionle return v.standardTx(tx) } +func (v *MempoolTxVerifier) StopStakerTx(tx *txs.StopStakerTx) error { + return v.standardTx(tx) +} + func (v *MempoolTxVerifier) standardTx(tx txs.UnsignedTx) error { baseState, err := v.standardBaseState() if err != nil { diff --git a/vms/platformvm/txs/mempool/issuer.go b/vms/platformvm/txs/mempool/issuer.go index 97a47cbcbaf5..2190d3b8b9d5 100644 --- a/vms/platformvm/txs/mempool/issuer.go +++ b/vms/platformvm/txs/mempool/issuer.go @@ -105,3 +105,8 @@ func (i *issuer) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) } return nil } + +func (i *issuer) StopStakerTx(*txs.StopStakerTx) error { + i.m.addDecisionTx(i.tx) + return nil +} diff --git a/vms/platformvm/txs/mempool/remover.go b/vms/platformvm/txs/mempool/remover.go index 4f66d08ce3e5..fad60e55fff1 100644 --- a/vms/platformvm/txs/mempool/remover.go +++ b/vms/platformvm/txs/mempool/remover.go @@ -3,7 +3,9 @@ package mempool -import "github.com/ava-labs/avalanchego/vms/platformvm/txs" +import ( + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) var _ txs.Visitor = (*remover)(nil) @@ -83,3 +85,8 @@ func (*remover) RewardValidatorTx(*txs.RewardValidatorTx) error { // this tx is never in mempool return nil } + +func (r *remover) StopStakerTx(*txs.StopStakerTx) error { + r.m.removeDecisionTxs([]*txs.Tx{r.tx}) + return nil +} diff --git a/vms/platformvm/txs/mock_staker.go b/vms/platformvm/txs/mock_staker.go index 4cb717f808df..39a01fe9111b 100644 --- a/vms/platformvm/txs/mock_staker.go +++ b/vms/platformvm/txs/mock_staker.go @@ -68,17 +68,17 @@ func (mr *MockStakerMockRecorder) EndTime() *gomock.Call { } // Duration mocks base method. -func (m *MockStaker) Duration() time.Duration { +func (m *MockStaker) StakingPeriod() time.Duration { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Duration") + ret := m.ctrl.Call(m, "StakingPeriod") ret0, _ := ret[0].(time.Duration) return ret0 } // EndTime indicates an expected call of EndTime. -func (mr *MockStakerMockRecorder) Duration() *gomock.Call { +func (mr *MockStakerMockRecorder) StakingPeriod() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Duration", reflect.TypeOf((*MockStaker)(nil).Duration)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StakingPeriod", reflect.TypeOf((*MockStaker)(nil).StakingPeriod)) } // NodeID mocks base method. diff --git a/vms/platformvm/txs/staker_tx.go b/vms/platformvm/txs/staker_tx.go index 870743f5317c..2458cc2d3334 100644 --- a/vms/platformvm/txs/staker_tx.go +++ b/vms/platformvm/txs/staker_tx.go @@ -4,6 +4,7 @@ package txs import ( + "math" "time" "github.com/ava-labs/avalanchego/ids" @@ -12,6 +13,11 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/fx" ) +var ( + StakerZeroTime = time.Unix(0, 0) + StakerMaxDuration time.Duration = math.MaxInt64 // time.Duration underlying type is currently int64 +) + // ValidatorTx defines the interface for a validator transaction that supports // delegation. type ValidatorTx interface { @@ -50,7 +56,7 @@ type Staker interface { PublicKey() (*bls.PublicKey, bool, error) StartTime() time.Time EndTime() time.Time - Duration() time.Duration + StakingPeriod() time.Duration Weight() uint64 PendingPriority() Priority CurrentPriority() Priority diff --git a/vms/platformvm/txs/stop_staker_tx.go b/vms/platformvm/txs/stop_staker_tx.go new file mode 100644 index 000000000000..dc4c1bdae02d --- /dev/null +++ b/vms/platformvm/txs/stop_staker_tx.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/vms/components/verify" +) + +var _ UnsignedTx = (*StopStakerTx)(nil) + +type StopStakerTx struct { + BaseTx `serialize:"true"` + + // ID of the tx that created the staker being removed + TxID ids.ID `serialize:"true" json:"txID"` + + // Proves that the issuer has the right to stop staking. + StakerAuth verify.Verifiable `serialize:"true" json:"stakerAuthorization"` +} + +func (tx *StopStakerTx) SyntacticVerify(ctx *snow.Context) error { + switch { + case tx == nil: + return ErrNilTx + case tx.SyntacticallyVerified: + // already passed syntactic verification + return nil + } + + if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { + return err + } + if err := tx.StakerAuth.Verify(); err != nil { + return err + } + + tx.SyntacticallyVerified = true + return nil +} + +func (tx *StopStakerTx) Visit(visitor Visitor) error { + return visitor.StopStakerTx(tx) +} diff --git a/vms/platformvm/txs/validator.go b/vms/platformvm/txs/validator.go index 79163392fee7..6d0fa6811aff 100644 --- a/vms/platformvm/txs/validator.go +++ b/vms/platformvm/txs/validator.go @@ -8,11 +8,13 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/timer/mockable" ) var ( - ErrWeightTooSmall = errors.New("weight of this validator is too low") - errBadSubnetID = errors.New("subnet ID can't be primary network ID") + ErrWeightTooSmall = errors.New("weight of this validator is too low") + ErrBadValidatorDuration = errors.New("validator duration too large") + errBadSubnetID = errors.New("subnet ID can't be primary network ID") ) // Validator is a validator. @@ -37,11 +39,17 @@ func (v *Validator) StartTime() time.Time { // EndTime is the time that this validator will leave the validator set func (v *Validator) EndTime() time.Time { + if v.Start == 0 { + return mockable.MaxTime + } return time.Unix(int64(v.End), 0) } -// Duration is the amount of time that this validator will be in the validator set -func (v *Validator) Duration() time.Duration { +// StakingPeriod is the amount of time that this validator will be in the validator set +func (v *Validator) StakingPeriod() time.Duration { + if v.Start == 0 { + return time.Duration(v.End) * time.Second + } return v.EndTime().Sub(v.StartTime()) } @@ -55,6 +63,9 @@ func (v *Validator) Verify() error { switch { case v.Wght == 0: // Ensure the validator has some weight return ErrWeightTooSmall + case v.Start == 0 && v.StakingPeriod() > StakerMaxDuration: + // Ensure proper encoding when v.End is used to encode a duration + return ErrBadValidatorDuration default: return nil } diff --git a/vms/platformvm/txs/validator_test.go b/vms/platformvm/txs/validator_test.go index 047c7180193b..8720167222f9 100644 --- a/vms/platformvm/txs/validator_test.go +++ b/vms/platformvm/txs/validator_test.go @@ -21,8 +21,8 @@ func TestValidatorBoundedBy(t *testing.T) { require := require.New(t) // case 1: a starts, a finishes, b starts, b finishes - aStartTime := uint64(0) - aEndTIme := uint64(1) + aStartTime := uint64(1) + aEndTIme := uint64(2) a := &Validator{ NodeID: ids.NodeID(keys[0].PublicKey().Address()), Start: aStartTime, @@ -30,8 +30,8 @@ func TestValidatorBoundedBy(t *testing.T) { Wght: defaultWeight, } - bStartTime := uint64(2) - bEndTime := uint64(3) + bStartTime := uint64(3) + bEndTime := uint64(4) b := &Validator{ NodeID: ids.NodeID(keys[0].PublicKey().Address()), Start: bStartTime, @@ -42,50 +42,50 @@ func TestValidatorBoundedBy(t *testing.T) { require.False(b.BoundedBy(a.StartTime(), a.EndTime())) // case 2: a starts, b starts, a finishes, b finishes - a.Start = 0 - b.Start = 1 - a.End = 2 - b.End = 3 + a.Start = 1 + b.Start = 2 + a.End = 3 + b.End = 4 require.False(a.BoundedBy(b.StartTime(), b.EndTime())) require.False(b.BoundedBy(a.StartTime(), a.EndTime())) // case 3: a starts, b starts, b finishes, a finishes - a.Start = 0 - b.Start = 1 - b.End = 2 - a.End = 3 + a.Start = 1 + b.Start = 2 + b.End = 3 + a.End = 4 require.False(a.BoundedBy(b.StartTime(), b.EndTime())) require.True(b.BoundedBy(a.StartTime(), a.EndTime())) // case 4: b starts, a starts, a finishes, b finishes - b.Start = 0 - a.Start = 1 - a.End = 2 - b.End = 3 + b.Start = 1 + a.Start = 2 + a.End = 3 + b.End = 4 require.True(a.BoundedBy(b.StartTime(), b.EndTime())) require.False(b.BoundedBy(a.StartTime(), a.EndTime())) // case 5: b starts, b finishes, a starts, a finishes - b.Start = 0 - b.End = 1 - a.Start = 2 - a.End = 3 + b.Start = 1 + b.End = 2 + a.Start = 3 + a.End = 4 require.False(a.BoundedBy(b.StartTime(), b.EndTime())) require.False(b.BoundedBy(a.StartTime(), a.EndTime())) // case 6: b starts, a starts, b finishes, a finishes - b.Start = 0 - a.Start = 1 - b.End = 2 - a.End = 3 + b.Start = 1 + a.Start = 2 + b.End = 3 + a.End = 4 require.False(a.BoundedBy(b.StartTime(), b.EndTime())) require.False(b.BoundedBy(a.StartTime(), a.EndTime())) // case 3: a starts, b starts, b finishes, a finishes - a.Start = 0 - b.Start = 0 - b.End = 1 - a.End = 1 + a.Start = 1 + b.Start = 1 + b.End = 2 + a.End = 2 require.True(a.BoundedBy(b.StartTime(), b.EndTime())) require.True(b.BoundedBy(a.StartTime(), a.EndTime())) } diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 18455d814358..7e20b78d56ed 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -18,4 +18,5 @@ type Visitor interface { TransformSubnetTx(*TransformSubnetTx) error AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error + StopStakerTx(*StopStakerTx) error } diff --git a/vms/platformvm/utxo/handler.go b/vms/platformvm/utxo/handler.go index 71cd3616e08a..6723d5a9a0ad 100644 --- a/vms/platformvm/utxo/handler.go +++ b/vms/platformvm/utxo/handler.go @@ -80,6 +80,16 @@ type Spender interface { []*secp256k1.PrivateKey, // Keys that prove ownership error, ) + + AuthorizeStopStaking( + stakerTxID ids.ID, + state state.Chain, + keys []*secp256k1.PrivateKey, + ) ( + verify.Verifiable, // Input that names owners + []*secp256k1.PrivateKey, // Keys that prove ownership + error, + ) } type Verifier interface { @@ -433,6 +443,70 @@ func (h *handler) Authorize( return &secp256k1fx.Input{SigIndices: indices}, signers, nil } +func (h *handler) AuthorizeStopStaking( + stakerTxID ids.ID, + state state.Chain, + keys []*secp256k1.PrivateKey, +) ( + verify.Verifiable, // Input that names owners + []*secp256k1.PrivateKey, // Keys that prove ownership + error, +) { + stakerTx, _, err := state.GetTx(stakerTxID) + if err != nil { + return nil, nil, fmt.Errorf( + "failed to fetch tx %s: %w", + stakerTxID, + err, + ) + } + var stakerOwner fx.Owner + switch uStakerTx := stakerTx.Unsigned.(type) { + case txs.ValidatorTx: + stakerOwner = uStakerTx.ValidationRewardsOwner() + case txs.DelegatorTx: + stakerOwner = uStakerTx.RewardsOwner() + case *txs.AddSubnetValidatorTx: + signedSubnetTx, _, err := state.GetTx(uStakerTx.Subnet) + if err != nil { + return nil, nil, fmt.Errorf( + "tx creating subnet not found %q: %v", + uStakerTx.Subnet, + err, + ) + } + subnetTx, ok := signedSubnetTx.Unsigned.(*txs.CreateSubnetTx) + if !ok { + return nil, nil, fmt.Errorf("expected *txs.CreateSubnetTx but got %T", signedSubnetTx.Unsigned) + } + stakerOwner = subnetTx.Owner + default: + return nil, nil, fmt.Errorf( + "unhandled staker type: %t", + uStakerTx, + ) + } + // Make sure the owners of the subnet match the provided keys + owner, ok := stakerOwner.(*secp256k1fx.OutputOwners) + if !ok { + return nil, nil, fmt.Errorf("expected *secp256k1fx.OutputOwners but got %T", stakerOwner) + } + + // Add the keys to a keychain + kc := secp256k1fx.NewKeychain(keys...) + + // Make sure that the operation is valid after a minimum time + now := uint64(h.clk.Time().Unix()) + + // Attempt to prove ownership of the subnet + indices, signers, matches := kc.Match(owner, now) + if !matches { + return nil, nil, errCantSign + } + + return &secp256k1fx.Input{SigIndices: indices}, signers, nil +} + func (h *handler) VerifySpend( tx txs.UnsignedTx, utxoDB avax.UTXOGetter, diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index a70473ecfb8d..022075c8d8cf 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -1206,7 +1206,7 @@ func TestCreateChain(t *testing.T) { // test where we: // 1) Create a subnet // 2) Add a validator to the subnet's pending validator set -// 3) Advance timestamp to validator's start time (moving the validator from pending to current) +// 3) Stop the subnet validator at the end of its first staking period // 4) Advance timestamp to validator's end time (removing validator from current) func TestCreateSubnet(t *testing.T) { require := require.New(t) @@ -1257,11 +1257,10 @@ func TestCreateSubnet(t *testing.T) { require.True(found) // Now that we've created a new subnet, add a validator to that subnet - // [startTime, endTime] is subset of time keys[0] validates default subnet so tx is valid addValidatorTx, err := vm.txBuilder.NewAddSubnetValidatorTx( defaultWeight, - 0, - uint64(time.Unix(0, 0).Add(defaultMinStakingDuration).Unix()), + uint64(0), + uint64(time.Unix(0, 0).Add(defaultMinStakingDuration).Unix()), // seconds in defaultMinStakingDuration nodeID, createSubnetTx.ID(), []*secp256k1.PrivateKey{keys[0]}, @@ -1275,7 +1274,7 @@ func TestCreateSubnet(t *testing.T) { require.NoError(err) require.NoError(blk.Verify(context.Background())) - require.NoError(blk.Accept(context.Background())) // add the validator to pending validator set + require.NoError(blk.Accept(context.Background())) // add the validator to validator set require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) txID := blk.(blocks.Block).Txs()[0].ID() @@ -1289,9 +1288,29 @@ func TestCreateSubnet(t *testing.T) { _, err = vm.state.GetCurrentValidator(createSubnetTx.ID(), nodeID) require.NoError(err) + // Stop the subnet validator + stopTx, err := vm.txBuilder.NewStopStakerTx( + txID, + []*secp256k1.PrivateKey{keys[0]}, + ids.ShortEmpty, // change addr + ) + require.NoError(err) + require.NoError(vm.Builder.AddUnverifiedTx(stopTx)) + blk, err = vm.Builder.BuildBlock(context.Background()) // should make subnet validator as stopping + require.NoError(err) + + require.NoError(blk.Verify(context.Background())) + require.NoError(blk.Accept(context.Background())) // add the validator to validator set + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) + + staker, err := vm.state.GetCurrentValidator(createSubnetTx.ID(), nodeID) + require.NoError(err) + + // there should be a finite end time set, since primary validator has finite end + require.True(staker.EndTime.Before(mockable.MaxTime)) + // fast forward clock to time validator should stop validating - endTime := vm.clock.Time().Add(defaultMinStakingDuration) - vm.clock.Set(endTime) + vm.clock.Set(staker.EndTime) blk, err = vm.Builder.BuildBlock(context.Background()) require.NoError(err) require.NoError(blk.Verify(context.Background())) diff --git a/wallet/chain/p/backend_visitor.go b/wallet/chain/p/backend_visitor.go index 9830d87ade05..f3a609a6be01 100644 --- a/wallet/chain/p/backend_visitor.go +++ b/wallet/chain/p/backend_visitor.go @@ -5,6 +5,7 @@ package p import ( stdcontext "context" + "errors" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/constants" @@ -98,6 +99,10 @@ func (b *backendVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessD return b.baseTx(&tx.BaseTx) } +func (*backendVisitor) StopStakerTx(*txs.StopStakerTx) error { + return errors.New("not yet implemented") +} + func (b *backendVisitor) baseTx(tx *txs.BaseTx) error { return b.b.removeUTXOs( b.ctx, diff --git a/wallet/chain/p/signer_visitor.go b/wallet/chain/p/signer_visitor.go index 52269ee69081..c7590dc63f84 100644 --- a/wallet/chain/p/signer_visitor.go +++ b/wallet/chain/p/signer_visitor.go @@ -164,6 +164,10 @@ func (s *signerVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDe return sign(s.tx, true, txSigners) } +func (*signerVisitor) StopStakerTx(*txs.StopStakerTx) error { + return errors.New("not yet implemented") +} + func (s *signerVisitor) getSigners(sourceChainID ids.ID, ins []*avax.TransferableInput) ([][]keychain.Signer, error) { txSigners := make([][]keychain.Signer, len(ins)) for credIndex, transferInput := range ins {