From df105aff51f223014671cca60699742e28fd5a75 Mon Sep 17 00:00:00 2001 From: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:11:42 -0400 Subject: [PATCH 001/184] Skip Flaky Test (#3495) Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- x/sync/sync_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/sync/sync_test.go b/x/sync/sync_test.go index 41dd5829a7b8..8f065d838e5f 100644 --- a/x/sync/sync_test.go +++ b/x/sync/sync_test.go @@ -1078,6 +1078,8 @@ func Test_Sync_Result_Correct_Root_With_Sync_Restart(t *testing.T) { } func Test_Sync_Result_Correct_Root_Update_Root_During(t *testing.T) { + t.Skip("FLAKY") + require := require.New(t) now := time.Now().UnixNano() From 1865987f1b4ed7782bf36d0406833b4b1f32f3c1 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Thu, 24 Oct 2024 20:59:36 +0300 Subject: [PATCH 002/184] Add request to update `releases.md` in PR template (#3476) Signed-off-by: Ceyhun Onur --- .github/pull_request_template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8200c0597f17..fe4d2395e5e0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,3 +3,5 @@ ## How this works ## How this was tested + +## Need to be documented in RELEASES.md? From 9a0a079a7b9f243cc67c8492de1b29766ab7322b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Oct 2024 14:26:22 -0400 Subject: [PATCH 003/184] ACP-77: Update P-chain state staker tests (#3494) --- vms/platformvm/state/state_test.go | 1205 ++++++++++++---------------- 1 file changed, 514 insertions(+), 691 deletions(-) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 6204540bd615..12214c2060b0 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -5,7 +5,6 @@ package state import ( "context" - "fmt" "math" "math/rand" "sync" @@ -28,6 +27,8 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/maybe" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -109,620 +110,384 @@ func TestStateSyncGenesis(t *testing.T) { ) } -// Whenever we store a staker, a whole bunch a data structures are updated -// This test is meant to capture which updates are carried out -func TestPersistStakers(t *testing.T) { - tests := map[string]struct { - // Insert or delete a staker to state and store it - storeStaker func(*require.Assertions, ids.ID /*=subnetID*/, *state) *Staker +// Whenever we add or remove a staker, a number of on-disk data structures +// should be updated. +// +// This test verifies that the on-disk data structures are updated as expected. +func TestState_writeStakers(t *testing.T) { + const ( + primaryValidatorDuration = 28 * 24 * time.Hour + primaryDelegatorDuration = 14 * 24 * time.Hour + subnetValidatorDuration = 21 * 24 * time.Hour + + primaryValidatorReward = iota + primaryDelegatorReward + subnetValidatorReward + ) + var ( + primaryValidatorStartTime = time.Now().Truncate(time.Second) + primaryValidatorEndTime = primaryValidatorStartTime.Add(primaryValidatorDuration) + primaryValidatorEndTimeUnix = uint64(primaryValidatorEndTime.Unix()) + + primaryDelegatorStartTime = primaryValidatorStartTime + primaryDelegatorEndTime = primaryDelegatorStartTime.Add(primaryDelegatorDuration) + primaryDelegatorEndTimeUnix = uint64(primaryDelegatorEndTime.Unix()) + + subnetValidatorStartTime = primaryValidatorStartTime + subnetValidatorEndTime = subnetValidatorStartTime.Add(subnetValidatorDuration) + subnetValidatorEndTimeUnix = uint64(subnetValidatorEndTime.Unix()) + + primaryValidatorData = txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + End: primaryValidatorEndTimeUnix, + Wght: 1234, + } + primaryDelegatorData = txs.Validator{ + NodeID: primaryValidatorData.NodeID, + End: primaryDelegatorEndTimeUnix, + Wght: 6789, + } + subnetValidatorData = txs.Validator{ + NodeID: primaryValidatorData.NodeID, + End: subnetValidatorEndTimeUnix, + Wght: 9876, + } - // Check that the staker is duly stored/removed in P-chain state - checkStakerInState func(*require.Assertions, *state, *Staker) + subnetID = ids.GenerateTestID() + ) - // Check whether validators are duly reported in the validator set, - // with the right weight and showing the BLS key - checkValidatorsSet func(*require.Assertions, *state, *Staker) + unsignedAddPrimaryNetworkValidator := createPermissionlessValidatorTx(t, constants.PrimaryNetworkID, primaryValidatorData) + addPrimaryNetworkValidator := &txs.Tx{Unsigned: unsignedAddPrimaryNetworkValidator} + require.NoError(t, addPrimaryNetworkValidator.Initialize(txs.Codec)) - // Check that node duly track stakers uptimes - checkValidatorUptimes func(*require.Assertions, *state, *Staker) + primaryNetworkPendingValidatorStaker, err := NewPendingStaker( + addPrimaryNetworkValidator.ID(), + unsignedAddPrimaryNetworkValidator, + ) + require.NoError(t, err) - // Check whether weight/bls keys diffs are duly stored - checkDiffs func(*require.Assertions, *state, *Staker, uint64) - }{ - "add current validator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - var ( - startTime = time.Now().Unix() - endTime = time.Now().Add(14 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - End: uint64(endTime), - Wght: 1234, - } - validatorReward uint64 = 5678 - ) + primaryNetworkCurrentValidatorStaker, err := NewCurrentStaker( + addPrimaryNetworkValidator.ID(), + unsignedAddPrimaryNetworkValidator, + primaryValidatorStartTime, + primaryValidatorReward, + ) + require.NoError(t, err) - utx := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utx} - r.NoError(addPermValTx.Initialize(txs.Codec)) + unsignedAddPrimaryNetworkDelegator := createPermissionlessDelegatorTx(constants.PrimaryNetworkID, primaryDelegatorData) + addPrimaryNetworkDelegator := &txs.Tx{Unsigned: unsignedAddPrimaryNetworkDelegator} + require.NoError(t, addPrimaryNetworkDelegator.Initialize(txs.Codec)) - staker, err := NewCurrentStaker( - addPermValTx.ID(), - utx, - time.Unix(startTime, 0), - validatorReward, - ) - r.NoError(err) + primaryNetworkPendingDelegatorStaker, err := NewPendingStaker( + addPrimaryNetworkDelegator.ID(), + unsignedAddPrimaryNetworkDelegator, + ) + require.NoError(t, err) - r.NoError(s.PutCurrentValidator(staker)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) - return staker - }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - retrievedStaker, err := s.GetCurrentValidator(staker.SubnetID, staker.NodeID) - r.NoError(err) - r.Equal(staker, retrievedStaker) - }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - valsMap := s.validators.GetMap(staker.SubnetID) - r.Contains(valsMap, staker.NodeID) - r.Equal( - &validators.GetValidatorOutput{ - NodeID: staker.NodeID, - PublicKey: staker.PublicKey, - Weight: staker.Weight, - }, - valsMap[staker.NodeID], - ) - }, - checkValidatorUptimes: func(r *require.Assertions, s *state, staker *Staker) { - upDuration, lastUpdated, err := s.GetUptime(staker.NodeID) - if staker.SubnetID != constants.PrimaryNetworkID { - // only primary network validators have uptimes - r.ErrorIs(err, database.ErrNotFound) - } else { - r.NoError(err) - r.Equal(upDuration, time.Duration(0)) - r.Equal(lastUpdated, staker.StartTime) - } - }, - checkDiffs: func(r *require.Assertions, s *state, staker *Staker, height uint64) { - weightDiffBytes, err := s.validatorWeightDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.NoError(err) - weightDiff, err := unmarshalWeightDiff(weightDiffBytes) - r.NoError(err) - r.Equal(&ValidatorWeightDiff{ - Decrease: false, - Amount: staker.Weight, - }, weightDiff) - - blsDiffBytes, err := s.validatorPublicKeyDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - if staker.SubnetID == constants.PrimaryNetworkID { - r.NoError(err) - r.Nil(blsDiffBytes) - } else { - r.ErrorIs(err, database.ErrNotFound) - } - }, - }, - "add current delegator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - // insert the delegator and its validator - var ( - valStartTime = time.Now().Truncate(time.Second).Unix() - delStartTime = time.Unix(valStartTime, 0).Add(time.Hour).Unix() - delEndTime = time.Unix(delStartTime, 0).Add(30 * 24 * time.Hour).Unix() - valEndTime = time.Unix(valStartTime, 0).Add(365 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - End: uint64(valEndTime), - Wght: 1234, - } - validatorReward uint64 = 5678 + primaryNetworkCurrentDelegatorStaker, err := NewCurrentStaker( + addPrimaryNetworkDelegator.ID(), + unsignedAddPrimaryNetworkDelegator, + primaryDelegatorStartTime, + primaryDelegatorReward, + ) + require.NoError(t, err) - delegatorData = txs.Validator{ - NodeID: validatorsData.NodeID, - End: uint64(delEndTime), - Wght: validatorsData.Wght / 2, - } - delegatorReward uint64 = 5432 - ) + unsignedAddSubnetValidator := createPermissionlessValidatorTx(t, subnetID, subnetValidatorData) + addSubnetValidator := &txs.Tx{Unsigned: unsignedAddSubnetValidator} + require.NoError(t, addSubnetValidator.Initialize(txs.Codec)) - utxVal := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utxVal} - r.NoError(addPermValTx.Initialize(txs.Codec)) + subnetCurrentValidatorStaker, err := NewCurrentStaker( + addSubnetValidator.ID(), + unsignedAddSubnetValidator, + subnetValidatorStartTime, + subnetValidatorReward, + ) + require.NoError(t, err) - val, err := NewCurrentStaker( - addPermValTx.ID(), - utxVal, - time.Unix(valStartTime, 0), - validatorReward, - ) - r.NoError(err) + tests := map[string]struct { + initialStakers []*Staker + initialTxs []*txs.Tx - utxDel := createPermissionlessDelegatorTx(subnetID, delegatorData) - addPermDelTx := &txs.Tx{Unsigned: utxDel} - r.NoError(addPermDelTx.Initialize(txs.Codec)) + // Staker to insert or remove + staker *Staker + addStakerTx *txs.Tx // If tx is nil, the staker is being removed - del, err := NewCurrentStaker( - addPermDelTx.ID(), - utxDel, - time.Unix(delStartTime, 0), - delegatorReward, - ) - r.NoError(err) + // Check that the staker is duly stored/removed in P-chain state + expectedCurrentValidator *Staker + expectedPendingValidator *Staker + expectedCurrentDelegators []*Staker + expectedPendingDelegators []*Staker - r.NoError(s.PutCurrentValidator(val)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) + // Check that the validator entry has been set correctly in the + // in-memory validator set. + expectedValidatorSetOutput *validators.GetValidatorOutput - s.PutCurrentDelegator(del) - s.AddTx(addPermDelTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) - return del - }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - delIt, err := s.GetCurrentDelegatorIterator(staker.SubnetID, staker.NodeID) - r.NoError(err) - r.True(delIt.Next()) - retrievedDelegator := delIt.Value() - r.False(delIt.Next()) - delIt.Release() - r.Equal(staker, retrievedDelegator) + // Check whether weight/bls keys diffs are duly stored + expectedWeightDiff *ValidatorWeightDiff + expectedPublicKeyDiff maybe.Maybe[*bls.PublicKey] + }{ + "add current primary network validator": { + staker: primaryNetworkCurrentValidatorStaker, + addStakerTx: addPrimaryNetworkValidator, + expectedCurrentValidator: primaryNetworkCurrentValidatorStaker, + expectedValidatorSetOutput: &validators.GetValidatorOutput{ + NodeID: primaryNetworkCurrentValidatorStaker.NodeID, + PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, + Weight: primaryNetworkCurrentValidatorStaker.Weight, + }, + expectedWeightDiff: &ValidatorWeightDiff{ + Decrease: false, + Amount: primaryNetworkCurrentValidatorStaker.Weight, }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - val, err := s.GetCurrentValidator(staker.SubnetID, staker.NodeID) - r.NoError(err) - - valsMap := s.validators.GetMap(staker.SubnetID) - r.Contains(valsMap, staker.NodeID) - valOut := valsMap[staker.NodeID] - r.Equal(valOut.NodeID, staker.NodeID) - r.Equal(valOut.Weight, val.Weight+staker.Weight) + expectedPublicKeyDiff: maybe.Some[*bls.PublicKey](nil), + }, + "add current primary network delegator": { + initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker}, + initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, + staker: primaryNetworkCurrentDelegatorStaker, + addStakerTx: addPrimaryNetworkDelegator, + expectedCurrentValidator: primaryNetworkCurrentValidatorStaker, + expectedCurrentDelegators: []*Staker{primaryNetworkCurrentDelegatorStaker}, + expectedValidatorSetOutput: &validators.GetValidatorOutput{ + NodeID: primaryNetworkCurrentValidatorStaker.NodeID, + PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, + Weight: primaryNetworkCurrentValidatorStaker.Weight + primaryNetworkCurrentDelegatorStaker.Weight, + }, + expectedWeightDiff: &ValidatorWeightDiff{ + Decrease: false, + Amount: primaryNetworkCurrentDelegatorStaker.Weight, }, - checkValidatorUptimes: func(*require.Assertions, *state, *Staker) {}, - checkDiffs: func(r *require.Assertions, s *state, staker *Staker, height uint64) { - // validator's weight must increase of delegator's weight amount - weightDiffBytes, err := s.validatorWeightDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.NoError(err) - weightDiff, err := unmarshalWeightDiff(weightDiffBytes) - r.NoError(err) - r.Equal(&ValidatorWeightDiff{ - Decrease: false, - Amount: staker.Weight, - }, weightDiff) + }, + "add pending primary network validator": { + staker: primaryNetworkPendingValidatorStaker, + addStakerTx: addPrimaryNetworkValidator, + expectedPendingValidator: primaryNetworkPendingValidatorStaker, + }, + "add pending primary network delegator": { + initialStakers: []*Staker{primaryNetworkPendingValidatorStaker}, + initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, + staker: primaryNetworkPendingDelegatorStaker, + addStakerTx: addPrimaryNetworkDelegator, + expectedPendingValidator: primaryNetworkPendingValidatorStaker, + expectedPendingDelegators: []*Staker{primaryNetworkPendingDelegatorStaker}, + }, + "add current subnet validator": { + initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker}, + initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, + staker: subnetCurrentValidatorStaker, + addStakerTx: addSubnetValidator, + expectedCurrentValidator: subnetCurrentValidatorStaker, + expectedValidatorSetOutput: &validators.GetValidatorOutput{ + NodeID: subnetCurrentValidatorStaker.NodeID, + Weight: subnetCurrentValidatorStaker.Weight, + }, + expectedWeightDiff: &ValidatorWeightDiff{ + Decrease: false, + Amount: subnetCurrentValidatorStaker.Weight, }, }, - "add pending validator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - var ( - startTime = time.Now().Unix() - endTime = time.Now().Add(14 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - Start: uint64(startTime), - End: uint64(endTime), - Wght: 1234, - } - ) - - utx := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utx} - r.NoError(addPermValTx.Initialize(txs.Codec)) - - staker, err := NewPendingStaker( - addPermValTx.ID(), - utx, - ) - r.NoError(err) - - r.NoError(s.PutPendingValidator(staker)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) - return staker + "delete current primary network validator": { + initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker}, + initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, + staker: primaryNetworkCurrentValidatorStaker, + expectedWeightDiff: &ValidatorWeightDiff{ + Decrease: true, + Amount: primaryNetworkCurrentValidatorStaker.Weight, }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - retrievedStaker, err := s.GetPendingValidator(staker.SubnetID, staker.NodeID) - r.NoError(err) - r.Equal(staker, retrievedStaker) + expectedPublicKeyDiff: maybe.Some(primaryNetworkCurrentValidatorStaker.PublicKey), + }, + "delete current primary network delegator": { + initialStakers: []*Staker{ + primaryNetworkCurrentValidatorStaker, + primaryNetworkCurrentDelegatorStaker, + }, + initialTxs: []*txs.Tx{ + addPrimaryNetworkValidator, + addPrimaryNetworkDelegator, + }, + staker: primaryNetworkCurrentDelegatorStaker, + expectedCurrentValidator: primaryNetworkCurrentValidatorStaker, + expectedValidatorSetOutput: &validators.GetValidatorOutput{ + NodeID: primaryNetworkCurrentValidatorStaker.NodeID, + PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, + Weight: primaryNetworkCurrentValidatorStaker.Weight, + }, + expectedWeightDiff: &ValidatorWeightDiff{ + Decrease: true, + Amount: primaryNetworkCurrentDelegatorStaker.Weight, }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - // pending validators are not showed in validators set - valsMap := s.validators.GetMap(staker.SubnetID) - r.NotContains(valsMap, staker.NodeID) + }, + "delete pending primary network validator": { + initialStakers: []*Staker{primaryNetworkPendingValidatorStaker}, + initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, + staker: primaryNetworkPendingValidatorStaker, + }, + "delete pending primary network delegator": { + initialStakers: []*Staker{ + primaryNetworkPendingValidatorStaker, + primaryNetworkPendingDelegatorStaker, }, - checkValidatorUptimes: func(r *require.Assertions, s *state, staker *Staker) { - // pending validators uptime is not tracked - _, _, err := s.GetUptime(staker.NodeID) - r.ErrorIs(err, database.ErrNotFound) + initialTxs: []*txs.Tx{ + addPrimaryNetworkValidator, + addPrimaryNetworkDelegator, }, - checkDiffs: func(r *require.Assertions, s *state, staker *Staker, height uint64) { - // pending validators weight diff and bls diffs are not stored - _, err := s.validatorWeightDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.ErrorIs(err, database.ErrNotFound) - - _, err = s.validatorPublicKeyDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.ErrorIs(err, database.ErrNotFound) + staker: primaryNetworkPendingDelegatorStaker, + expectedPendingValidator: primaryNetworkPendingValidatorStaker, + }, + "delete current subnet validator": { + initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker, subnetCurrentValidatorStaker}, + initialTxs: []*txs.Tx{addPrimaryNetworkValidator, addSubnetValidator}, + staker: subnetCurrentValidatorStaker, + expectedWeightDiff: &ValidatorWeightDiff{ + Decrease: true, + Amount: subnetCurrentValidatorStaker.Weight, }, }, - "add pending delegator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - // insert the delegator and its validator - var ( - valStartTime = time.Now().Truncate(time.Second).Unix() - delStartTime = time.Unix(valStartTime, 0).Add(time.Hour).Unix() - delEndTime = time.Unix(delStartTime, 0).Add(30 * 24 * time.Hour).Unix() - valEndTime = time.Unix(valStartTime, 0).Add(365 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - Start: uint64(valStartTime), - End: uint64(valEndTime), - Wght: 1234, - } + } - delegatorData = txs.Validator{ - NodeID: validatorsData.NodeID, - Start: uint64(delStartTime), - End: uint64(delEndTime), - Wght: validatorsData.Wght / 2, - } - ) + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) - utxVal := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utxVal} - r.NoError(addPermValTx.Initialize(txs.Codec)) + db := memdb.New() + state := newTestState(t, db) + + addOrDeleteStaker := func(staker *Staker, add bool) { + if add { + switch { + case staker.Priority.IsCurrentValidator(): + require.NoError(state.PutCurrentValidator(staker)) + case staker.Priority.IsPendingValidator(): + require.NoError(state.PutPendingValidator(staker)) + case staker.Priority.IsCurrentDelegator(): + state.PutCurrentDelegator(staker) + case staker.Priority.IsPendingDelegator(): + state.PutPendingDelegator(staker) + } + } else { + switch { + case staker.Priority.IsCurrentValidator(): + state.DeleteCurrentValidator(staker) + case staker.Priority.IsPendingValidator(): + state.DeletePendingValidator(staker) + case staker.Priority.IsCurrentDelegator(): + state.DeleteCurrentDelegator(staker) + case staker.Priority.IsPendingDelegator(): + state.DeletePendingDelegator(staker) + } + } + } - val, err := NewPendingStaker(addPermValTx.ID(), utxVal) - r.NoError(err) + // create and store the initial stakers + for _, staker := range test.initialStakers { + addOrDeleteStaker(staker, true) + } + for _, tx := range test.initialTxs { + state.AddTx(tx, status.Committed) + } - utxDel := createPermissionlessDelegatorTx(subnetID, delegatorData) - addPermDelTx := &txs.Tx{Unsigned: utxDel} - r.NoError(addPermDelTx.Initialize(txs.Codec)) + state.SetHeight(0) + require.NoError(state.Commit()) - del, err := NewPendingStaker(addPermDelTx.ID(), utxDel) - r.NoError(err) + // create and store the staker under test + addOrDeleteStaker(test.staker, test.addStakerTx != nil) + if test.addStakerTx != nil { + state.AddTx(test.addStakerTx, status.Committed) + } - r.NoError(s.PutPendingValidator(val)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) + state.SetHeight(1) + require.NoError(state.Commit()) - s.PutPendingDelegator(del) - s.AddTx(addPermDelTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) + // Perform the checks once immediately after committing to the + // state, and once after re-loading the state from disk. + for i := 0; i < 2; i++ { + currentValidator, err := state.GetCurrentValidator(test.staker.SubnetID, test.staker.NodeID) + if test.expectedCurrentValidator == nil { + require.ErrorIs(err, database.ErrNotFound) - return del - }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - delIt, err := s.GetPendingDelegatorIterator(staker.SubnetID, staker.NodeID) - r.NoError(err) - r.True(delIt.Next()) - retrievedDelegator := delIt.Value() - r.False(delIt.Next()) - delIt.Release() - r.Equal(staker, retrievedDelegator) - }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - valsMap := s.validators.GetMap(staker.SubnetID) - r.NotContains(valsMap, staker.NodeID) - }, - checkValidatorUptimes: func(*require.Assertions, *state, *Staker) {}, - checkDiffs: func(*require.Assertions, *state, *Staker, uint64) {}, - }, - "delete current validator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - // add them remove the validator - var ( - startTime = time.Now().Unix() - endTime = time.Now().Add(14 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - End: uint64(endTime), - Wght: 1234, + if test.staker.SubnetID == constants.PrimaryNetworkID { + // Uptimes are only considered for primary network validators + _, _, err := state.GetUptime(test.staker.NodeID) + require.ErrorIs(err, database.ErrNotFound) } - validatorReward uint64 = 5678 - ) - - utx := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utx} - r.NoError(addPermValTx.Initialize(txs.Codec)) - - staker, err := NewCurrentStaker( - addPermValTx.ID(), - utx, - time.Unix(startTime, 0), - validatorReward, - ) - r.NoError(err) - - r.NoError(s.PutCurrentValidator(staker)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) + } else { + require.NoError(err) + require.Equal(test.expectedCurrentValidator, currentValidator) + + if test.staker.SubnetID == constants.PrimaryNetworkID { + // Uptimes are only considered for primary network validators + upDuration, lastUpdated, err := state.GetUptime(currentValidator.NodeID) + require.NoError(err) + require.Zero(upDuration) + require.Equal(currentValidator.StartTime, lastUpdated) + } + } - s.DeleteCurrentValidator(staker) - r.NoError(s.Commit()) - return staker - }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - _, err := s.GetCurrentValidator(staker.SubnetID, staker.NodeID) - r.ErrorIs(err, database.ErrNotFound) - }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - // deleted validators are not showed in the validators set anymore - valsMap := s.validators.GetMap(staker.SubnetID) - r.NotContains(valsMap, staker.NodeID) - }, - checkValidatorUptimes: func(r *require.Assertions, s *state, staker *Staker) { - // uptimes of delete validators are dropped - _, _, err := s.GetUptime(staker.NodeID) - r.ErrorIs(err, database.ErrNotFound) - }, - checkDiffs: func(r *require.Assertions, s *state, staker *Staker, height uint64) { - weightDiffBytes, err := s.validatorWeightDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.NoError(err) - weightDiff, err := unmarshalWeightDiff(weightDiffBytes) - r.NoError(err) - r.Equal(&ValidatorWeightDiff{ - Decrease: true, - Amount: staker.Weight, - }, weightDiff) - - blsDiffBytes, err := s.validatorPublicKeyDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - if staker.SubnetID == constants.PrimaryNetworkID { - r.NoError(err) - r.Equal(bls.PublicKeyFromValidUncompressedBytes(blsDiffBytes), staker.PublicKey) + pendingValidator, err := state.GetPendingValidator(test.staker.SubnetID, test.staker.NodeID) + if test.expectedPendingValidator == nil { + require.ErrorIs(err, database.ErrNotFound) } else { - r.ErrorIs(err, database.ErrNotFound) + require.NoError(err) + require.Equal(test.expectedPendingValidator, pendingValidator) } - }, - }, - "delete current delegator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - // insert validator and delegator, then remove the delegator - var ( - valStartTime = time.Now().Truncate(time.Second).Unix() - delStartTime = time.Unix(valStartTime, 0).Add(time.Hour).Unix() - delEndTime = time.Unix(delStartTime, 0).Add(30 * 24 * time.Hour).Unix() - valEndTime = time.Unix(valStartTime, 0).Add(365 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - End: uint64(valEndTime), - Wght: 1234, - } - validatorReward uint64 = 5678 - delegatorData = txs.Validator{ - NodeID: validatorsData.NodeID, - End: uint64(delEndTime), - Wght: validatorsData.Wght / 2, - } - delegatorReward uint64 = 5432 + it, err := state.GetCurrentDelegatorIterator(test.staker.SubnetID, test.staker.NodeID) + require.NoError(err) + require.Equal( + test.expectedCurrentDelegators, + iterator.ToSlice(it), ) - utxVal := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utxVal} - r.NoError(addPermValTx.Initialize(txs.Codec)) - - val, err := NewCurrentStaker( - addPermValTx.ID(), - utxVal, - time.Unix(valStartTime, 0), - validatorReward, + it, err = state.GetPendingDelegatorIterator(test.staker.SubnetID, test.staker.NodeID) + require.NoError(err) + require.Equal( + test.expectedPendingDelegators, + iterator.ToSlice(it), ) - r.NoError(err) - - utxDel := createPermissionlessDelegatorTx(subnetID, delegatorData) - addPermDelTx := &txs.Tx{Unsigned: utxDel} - r.NoError(addPermDelTx.Initialize(txs.Codec)) - del, err := NewCurrentStaker( - addPermDelTx.ID(), - utxDel, - time.Unix(delStartTime, 0), - delegatorReward, + require.Equal( + test.expectedValidatorSetOutput, + state.validators.GetMap(test.staker.SubnetID)[test.staker.NodeID], ) - r.NoError(err) - - r.NoError(s.PutCurrentValidator(val)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - s.PutCurrentDelegator(del) - s.AddTx(addPermDelTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) - - s.DeleteCurrentDelegator(del) - r.NoError(s.Commit()) - - return del - }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - delIt, err := s.GetCurrentDelegatorIterator(staker.SubnetID, staker.NodeID) - r.NoError(err) - r.False(delIt.Next()) - delIt.Release() - }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - val, err := s.GetCurrentValidator(staker.SubnetID, staker.NodeID) - r.NoError(err) - - valsMap := s.validators.GetMap(staker.SubnetID) - r.Contains(valsMap, staker.NodeID) - valOut := valsMap[staker.NodeID] - r.Equal(valOut.NodeID, staker.NodeID) - r.Equal(valOut.Weight, val.Weight) - }, - checkValidatorUptimes: func(*require.Assertions, *state, *Staker) {}, - checkDiffs: func(r *require.Assertions, s *state, staker *Staker, height uint64) { - // validator's weight must decrease of delegator's weight amount - weightDiffBytes, err := s.validatorWeightDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.NoError(err) - weightDiff, err := unmarshalWeightDiff(weightDiffBytes) - r.NoError(err) - r.Equal(&ValidatorWeightDiff{ - Decrease: true, - Amount: staker.Weight, - }, weightDiff) - }, - }, - "delete pending validator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - var ( - startTime = time.Now().Unix() - endTime = time.Now().Add(14 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - Start: uint64(startTime), - End: uint64(endTime), - Wght: 1234, - } - ) - - utx := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utx} - r.NoError(addPermValTx.Initialize(txs.Codec)) - - staker, err := NewPendingStaker( - addPermValTx.ID(), - utx, - ) - r.NoError(err) - - r.NoError(s.PutPendingValidator(staker)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) - - s.DeletePendingValidator(staker) - r.NoError(s.Commit()) + diffKey := marshalDiffKey(test.staker.SubnetID, 1, test.staker.NodeID) + weightDiffBytes, err := state.validatorWeightDiffsDB.Get(diffKey) + if test.expectedWeightDiff == nil { + require.ErrorIs(err, database.ErrNotFound) + } else { + require.NoError(err) - return staker - }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - _, err := s.GetPendingValidator(staker.SubnetID, staker.NodeID) - r.ErrorIs(err, database.ErrNotFound) - }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - valsMap := s.validators.GetMap(staker.SubnetID) - r.NotContains(valsMap, staker.NodeID) - }, - checkValidatorUptimes: func(r *require.Assertions, s *state, staker *Staker) { - _, _, err := s.GetUptime(staker.NodeID) - r.ErrorIs(err, database.ErrNotFound) - }, - checkDiffs: func(r *require.Assertions, s *state, staker *Staker, height uint64) { - _, err := s.validatorWeightDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.ErrorIs(err, database.ErrNotFound) + weightDiff, err := unmarshalWeightDiff(weightDiffBytes) + require.NoError(err) + require.Equal(test.expectedWeightDiff, weightDiff) + } - _, err = s.validatorPublicKeyDiffsDB.Get(marshalDiffKey(staker.SubnetID, height, staker.NodeID)) - r.ErrorIs(err, database.ErrNotFound) - }, - }, - "delete pending delegator": { - storeStaker: func(r *require.Assertions, subnetID ids.ID, s *state) *Staker { - // insert validator and delegator the remove the validator - var ( - valStartTime = time.Now().Truncate(time.Second).Unix() - delStartTime = time.Unix(valStartTime, 0).Add(time.Hour).Unix() - delEndTime = time.Unix(delStartTime, 0).Add(30 * 24 * time.Hour).Unix() - valEndTime = time.Unix(valStartTime, 0).Add(365 * 24 * time.Hour).Unix() - - validatorsData = txs.Validator{ - NodeID: ids.GenerateTestNodeID(), - Start: uint64(valStartTime), - End: uint64(valEndTime), - Wght: 1234, - } + publicKeyDiffBytes, err := state.validatorPublicKeyDiffsDB.Get(diffKey) + if test.expectedPublicKeyDiff.IsNothing() { + require.ErrorIs(err, database.ErrNotFound) + } else { + require.NoError(err) - delegatorData = txs.Validator{ - NodeID: validatorsData.NodeID, - Start: uint64(delStartTime), - End: uint64(delEndTime), - Wght: validatorsData.Wght / 2, + expectedPublicKeyDiff := test.expectedPublicKeyDiff.Value() + if expectedPublicKeyDiff != nil { + require.Equal(expectedPublicKeyDiff, bls.PublicKeyFromValidUncompressedBytes(publicKeyDiffBytes)) + } else { + require.Empty(publicKeyDiffBytes) } - ) - - utxVal := createPermissionlessValidatorTx(r, subnetID, validatorsData) - addPermValTx := &txs.Tx{Unsigned: utxVal} - r.NoError(addPermValTx.Initialize(txs.Codec)) - - val, err := NewPendingStaker(addPermValTx.ID(), utxVal) - r.NoError(err) - - utxDel := createPermissionlessDelegatorTx(subnetID, delegatorData) - addPermDelTx := &txs.Tx{Unsigned: utxDel} - r.NoError(addPermDelTx.Initialize(txs.Codec)) - - del, err := NewPendingStaker(addPermDelTx.ID(), utxDel) - r.NoError(err) - - r.NoError(s.PutPendingValidator(val)) - s.AddTx(addPermValTx, status.Committed) // this is currently needed to reload the staker - - s.PutPendingDelegator(del) - s.AddTx(addPermDelTx, status.Committed) // this is currently needed to reload the staker - r.NoError(s.Commit()) - - s.DeletePendingDelegator(del) - r.NoError(s.Commit()) - return del - }, - checkStakerInState: func(r *require.Assertions, s *state, staker *Staker) { - delIt, err := s.GetPendingDelegatorIterator(staker.SubnetID, staker.NodeID) - r.NoError(err) - r.False(delIt.Next()) - delIt.Release() - }, - checkValidatorsSet: func(r *require.Assertions, s *state, staker *Staker) { - valsMap := s.validators.GetMap(staker.SubnetID) - r.NotContains(valsMap, staker.NodeID) - }, - checkValidatorUptimes: func(*require.Assertions, *state, *Staker) {}, - checkDiffs: func(*require.Assertions, *state, *Staker, uint64) {}, - }, - } - - subnetIDs := []ids.ID{constants.PrimaryNetworkID, ids.GenerateTestID()} - for _, subnetID := range subnetIDs { - for name, test := range tests { - t.Run(fmt.Sprintf("%s - subnetID %s", name, subnetID), func(t *testing.T) { - require := require.New(t) - - db := memdb.New() - state := newTestState(t, db) - - // create and store the staker - staker := test.storeStaker(require, subnetID, state) - - // check all relevant data are stored - test.checkStakerInState(require, state, staker) - test.checkValidatorsSet(require, state, staker) - test.checkValidatorUptimes(require, state, staker) - test.checkDiffs(require, state, staker, 0 /*height*/) - - // rebuild the state - rebuiltState := newTestState(t, db) + } - // check again that all relevant data are still available in rebuilt state - test.checkStakerInState(require, rebuiltState, staker) - test.checkValidatorsSet(require, rebuiltState, staker) - test.checkValidatorUptimes(require, rebuiltState, staker) - test.checkDiffs(require, rebuiltState, staker, 0 /*height*/) - }) - } + // re-load the state from disk for the second iteration + state = newTestState(t, db) + } + }) } } -func createPermissionlessValidatorTx(r *require.Assertions, subnetID ids.ID, validatorsData txs.Validator) *txs.AddPermissionlessValidatorTx { +func createPermissionlessValidatorTx(t testing.TB, subnetID ids.ID, validatorsData txs.Validator) *txs.AddPermissionlessValidatorTx { var sig signer.Signer = &signer.Empty{} if subnetID == constants.PrimaryNetworkID { sk, err := bls.NewSecretKey() - r.NoError(err) + require.NoError(t, err) sig = signer.NewProofOfPossession(sk) } @@ -988,43 +753,50 @@ func TestValidatorWeightDiff(t *testing.T) { } } -// Tests PutCurrentValidator, DeleteCurrentValidator, GetCurrentValidator, -// ApplyValidatorWeightDiffs, ApplyValidatorPublicKeyDiffs -func TestStateAddRemoveValidator(t *testing.T) { +func TestState_ApplyValidatorDiffs(t *testing.T) { require := require.New(t) state := newTestState(t, memdb.New()) var ( - numNodes = 3 - subnetID = ids.GenerateTestID() - startTime = time.Now() - endTime = startTime.Add(24 * time.Hour) - stakers = make([]Staker, numNodes) + numNodes = 5 + subnetID = ids.GenerateTestID() + startTime = time.Now() + endTime = startTime.Add(24 * time.Hour) + primaryStakers = make([]Staker, numNodes) + subnetStakers = make([]Staker, numNodes) ) - for i := 0; i < numNodes; i++ { - stakers[i] = Staker{ + for i := range primaryStakers { + sk, err := bls.NewSecretKey() + require.NoError(err) + + timeOffset := time.Duration(i) * time.Second + primaryStakers[i] = Staker{ TxID: ids.GenerateTestID(), NodeID: ids.GenerateTestNodeID(), + PublicKey: bls.PublicFromSecretKey(sk), + SubnetID: constants.PrimaryNetworkID, Weight: uint64(i + 1), - StartTime: startTime.Add(time.Duration(i) * time.Second), - EndTime: endTime.Add(time.Duration(i) * time.Second), + StartTime: startTime.Add(timeOffset), + EndTime: endTime.Add(timeOffset), PotentialReward: uint64(i + 1), } - if i%2 == 0 { - stakers[i].SubnetID = subnetID - } else { - sk, err := bls.NewSecretKey() - require.NoError(err) - stakers[i].PublicKey = bls.PublicFromSecretKey(sk) - stakers[i].SubnetID = constants.PrimaryNetworkID + } + for i, primaryStaker := range primaryStakers { + subnetStakers[i] = Staker{ + TxID: ids.GenerateTestID(), + NodeID: primaryStaker.NodeID, + PublicKey: nil, // Key is inherited from the primary network + SubnetID: subnetID, + Weight: uint64(i + 1), + StartTime: primaryStaker.StartTime, + EndTime: primaryStaker.EndTime, + PotentialReward: uint64(i + 1), } } type diff struct { addedValidators []Staker - addedDelegators []Staker - removedDelegators []Staker removedValidators []Staker expectedPrimaryValidatorSet map[ids.NodeID]*validators.GetValidatorOutput @@ -1037,101 +809,172 @@ func TestStateAddRemoveValidator(t *testing.T) { expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, }, { - // Add a subnet validator - addedValidators: []Staker{stakers[0]}, - expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, - expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ - stakers[0].NodeID: { - NodeID: stakers[0].NodeID, - Weight: stakers[0].Weight, + // Add primary validator 0 + addedValidators: []Staker{primaryStakers[0]}, + expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ + primaryStakers[0].NodeID: { + NodeID: primaryStakers[0].NodeID, + PublicKey: primaryStakers[0].PublicKey, + Weight: primaryStakers[0].Weight, }, }, + expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, }, { - // Remove a subnet validator - removedValidators: []Staker{stakers[0]}, - expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, - expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, + // Add subnet validator 0 + addedValidators: []Staker{subnetStakers[0]}, + expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ + primaryStakers[0].NodeID: { + NodeID: primaryStakers[0].NodeID, + PublicKey: primaryStakers[0].PublicKey, + Weight: primaryStakers[0].Weight, + }, + }, + expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ + subnetStakers[0].NodeID: { + NodeID: subnetStakers[0].NodeID, + Weight: subnetStakers[0].Weight, + }, + }, }, - { // Add a primary network validator - addedValidators: []Staker{stakers[1]}, + { + // Remove subnet validator 0 + removedValidators: []Staker{subnetStakers[0]}, expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ - stakers[1].NodeID: { - NodeID: stakers[1].NodeID, - PublicKey: stakers[1].PublicKey, - Weight: stakers[1].Weight, + primaryStakers[0].NodeID: { + NodeID: primaryStakers[0].NodeID, + PublicKey: primaryStakers[0].PublicKey, + Weight: primaryStakers[0].Weight, }, }, expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, }, { - // Do nothing + // Add primary network validator 1, and subnet validator 1 + addedValidators: []Staker{primaryStakers[1], subnetStakers[1]}, + // Remove primary network validator 0, and subnet validator 1 + removedValidators: []Staker{primaryStakers[0], subnetStakers[1]}, expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ - stakers[1].NodeID: { - NodeID: stakers[1].NodeID, - PublicKey: stakers[1].PublicKey, - Weight: stakers[1].Weight, + primaryStakers[1].NodeID: { + NodeID: primaryStakers[1].NodeID, + PublicKey: primaryStakers[1].PublicKey, + Weight: primaryStakers[1].Weight, }, }, expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, }, - { // Remove a primary network validator - removedValidators: []Staker{stakers[1]}, - expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, - expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, + { + // Add primary network validator 2, and subnet validator 2 + addedValidators: []Staker{primaryStakers[2], subnetStakers[2]}, + // Remove primary network validator 1 + removedValidators: []Staker{primaryStakers[1]}, + expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ + primaryStakers[2].NodeID: { + NodeID: primaryStakers[2].NodeID, + PublicKey: primaryStakers[2].PublicKey, + Weight: primaryStakers[2].Weight, + }, + }, + expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ + subnetStakers[2].NodeID: { + NodeID: subnetStakers[2].NodeID, + Weight: subnetStakers[2].Weight, + }, + }, }, { - // Add 2 subnet validators and a primary network validator - addedValidators: []Staker{stakers[0], stakers[1], stakers[2]}, + // Add primary network and subnet validators 3 & 4 + addedValidators: []Staker{primaryStakers[3], primaryStakers[4], subnetStakers[3], subnetStakers[4]}, expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ - stakers[1].NodeID: { - NodeID: stakers[1].NodeID, - PublicKey: stakers[1].PublicKey, - Weight: stakers[1].Weight, + primaryStakers[2].NodeID: { + NodeID: primaryStakers[2].NodeID, + PublicKey: primaryStakers[2].PublicKey, + Weight: primaryStakers[2].Weight, + }, + primaryStakers[3].NodeID: { + NodeID: primaryStakers[3].NodeID, + PublicKey: primaryStakers[3].PublicKey, + Weight: primaryStakers[3].Weight, + }, + primaryStakers[4].NodeID: { + NodeID: primaryStakers[4].NodeID, + PublicKey: primaryStakers[4].PublicKey, + Weight: primaryStakers[4].Weight, }, }, expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ - stakers[0].NodeID: { - NodeID: stakers[0].NodeID, - Weight: stakers[0].Weight, + subnetStakers[2].NodeID: { + NodeID: subnetStakers[2].NodeID, + Weight: subnetStakers[2].Weight, + }, + subnetStakers[3].NodeID: { + NodeID: subnetStakers[3].NodeID, + Weight: subnetStakers[3].Weight, }, - stakers[2].NodeID: { - NodeID: stakers[2].NodeID, - Weight: stakers[2].Weight, + subnetStakers[4].NodeID: { + NodeID: subnetStakers[4].NodeID, + Weight: subnetStakers[4].Weight, }, }, }, { - // Remove 2 subnet validators and a primary network validator. - removedValidators: []Staker{stakers[0], stakers[1], stakers[2]}, + // Remove primary network and subnet validators 2 & 3 & 4 + removedValidators: []Staker{ + primaryStakers[2], primaryStakers[3], primaryStakers[4], + subnetStakers[2], subnetStakers[3], subnetStakers[4], + }, + expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, + expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, + }, + { + // Do nothing expectedPrimaryValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{}, }, } for currentIndex, diff := range diffs { - for _, added := range diff.addedValidators { - added := added - require.NoError(state.PutCurrentValidator(&added)) - } - for _, added := range diff.addedDelegators { - added := added - state.PutCurrentDelegator(&added) + d, err := NewDiffOn(state) + require.NoError(err) + + type subnetIDNodeID struct { + subnetID ids.ID + nodeID ids.NodeID } - for _, removed := range diff.removedDelegators { - removed := removed - state.DeleteCurrentDelegator(&removed) + var expectedValidators set.Set[subnetIDNodeID] + for _, added := range diff.addedValidators { + require.NoError(d.PutCurrentValidator(&added)) + + expectedValidators.Add(subnetIDNodeID{ + subnetID: added.SubnetID, + nodeID: added.NodeID, + }) } for _, removed := range diff.removedValidators { - removed := removed - state.DeleteCurrentValidator(&removed) + d.DeleteCurrentValidator(&removed) + + expectedValidators.Remove(subnetIDNodeID{ + subnetID: removed.SubnetID, + nodeID: removed.NodeID, + }) } + require.NoError(d.Apply(state)) + currentHeight := uint64(currentIndex + 1) state.SetHeight(currentHeight) require.NoError(state.Commit()) + // Verify that the current state is as expected. for _, added := range diff.addedValidators { + subnetNodeID := subnetIDNodeID{ + subnetID: added.SubnetID, + nodeID: added.NodeID, + } + if !expectedValidators.Contains(subnetNodeID) { + continue + } + gotValidator, err := state.GetCurrentValidator(added.SubnetID, added.NodeID) require.NoError(err) require.Equal(added, *gotValidator) @@ -1142,37 +985,47 @@ func TestStateAddRemoveValidator(t *testing.T) { require.ErrorIs(err, database.ErrNotFound) } + primaryValidatorSet := state.validators.GetMap(constants.PrimaryNetworkID) + delete(primaryValidatorSet, defaultValidatorNodeID) // Ignore the genesis validator + require.Equal(diff.expectedPrimaryValidatorSet, primaryValidatorSet) + + require.Equal(diff.expectedSubnetValidatorSet, state.validators.GetMap(subnetID)) + + // Verify that applying diffs against the current state results in the + // expected state. for i := 0; i < currentIndex; i++ { prevDiff := diffs[i] prevHeight := uint64(i + 1) - primaryValidatorSet := copyValidatorSet(diff.expectedPrimaryValidatorSet) - require.NoError(state.ApplyValidatorWeightDiffs( - context.Background(), - primaryValidatorSet, - currentHeight, - prevHeight+1, - constants.PrimaryNetworkID, - )) - requireEqualWeightsValidatorSet(require, prevDiff.expectedPrimaryValidatorSet, primaryValidatorSet) - - require.NoError(state.ApplyValidatorPublicKeyDiffs( - context.Background(), - primaryValidatorSet, - currentHeight, - prevHeight+1, - )) - requireEqualPublicKeysValidatorSet(require, prevDiff.expectedPrimaryValidatorSet, primaryValidatorSet) - - subnetValidatorSet := copyValidatorSet(diff.expectedSubnetValidatorSet) - require.NoError(state.ApplyValidatorWeightDiffs( - context.Background(), - subnetValidatorSet, - currentHeight, - prevHeight+1, - subnetID, - )) - requireEqualWeightsValidatorSet(require, prevDiff.expectedSubnetValidatorSet, subnetValidatorSet) + { + primaryValidatorSet := copyValidatorSet(diff.expectedPrimaryValidatorSet) + require.NoError(state.ApplyValidatorWeightDiffs( + context.Background(), + primaryValidatorSet, + currentHeight, + prevHeight+1, + constants.PrimaryNetworkID, + )) + require.NoError(state.ApplyValidatorPublicKeyDiffs( + context.Background(), + primaryValidatorSet, + currentHeight, + prevHeight+1, + )) + require.Equal(prevDiff.expectedPrimaryValidatorSet, primaryValidatorSet) + } + + { + subnetValidatorSet := copyValidatorSet(diff.expectedSubnetValidatorSet) + require.NoError(state.ApplyValidatorWeightDiffs( + context.Background(), + subnetValidatorSet, + currentHeight, + prevHeight+1, + subnetID, + )) + require.Equal(prevDiff.expectedSubnetValidatorSet, subnetValidatorSet) + } } } } @@ -1188,36 +1041,6 @@ func copyValidatorSet( return result } -func requireEqualWeightsValidatorSet( - require *require.Assertions, - expected map[ids.NodeID]*validators.GetValidatorOutput, - actual map[ids.NodeID]*validators.GetValidatorOutput, -) { - require.Len(actual, len(expected)) - for nodeID, expectedVdr := range expected { - require.Contains(actual, nodeID) - - actualVdr := actual[nodeID] - require.Equal(expectedVdr.NodeID, actualVdr.NodeID) - require.Equal(expectedVdr.Weight, actualVdr.Weight) - } -} - -func requireEqualPublicKeysValidatorSet( - require *require.Assertions, - expected map[ids.NodeID]*validators.GetValidatorOutput, - actual map[ids.NodeID]*validators.GetValidatorOutput, -) { - require.Len(actual, len(expected)) - for nodeID, expectedVdr := range expected { - require.Contains(actual, nodeID) - - actualVdr := actual[nodeID] - require.Equal(expectedVdr.NodeID, actualVdr.NodeID) - require.Equal(expectedVdr.PublicKey, actualVdr.PublicKey) - } -} - func TestParsedStateBlock(t *testing.T) { var ( require = require.New(t) From e8b21ec88d26f3bc2d2cf3899470cf4b65e2fc44 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Oct 2024 17:10:34 -0400 Subject: [PATCH 004/184] Verify no SoV + legacy overlap --- vms/platformvm/state/diff_test.go | 15 +++++++++++++++ vms/platformvm/state/subnet_only_validator.go | 17 ++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/state/diff_test.go b/vms/platformvm/state/diff_test.go index 3e1ca3b2cdce..9cc67ff8d9ef 100644 --- a/vms/platformvm/state/diff_test.go +++ b/vms/platformvm/state/diff_test.go @@ -315,6 +315,15 @@ func TestDiffSubnetOnlyValidatorsErrors(t *testing.T) { }, expectedErr: ErrMutatedSubnetOnlyValidator, }, + { + name: "conflicting legacy subnetID and nodeID pair", + initialEndAccumulatedFee: 1, + sov: SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + NodeID: defaultValidatorNodeID, + }, + expectedErr: ErrConflictingSubnetOnlyValidator, + }, { name: "duplicate active subnetID and nodeID pair", initialEndAccumulatedFee: 1, @@ -341,6 +350,12 @@ func TestDiffSubnetOnlyValidatorsErrors(t *testing.T) { state := newTestState(t, memdb.New()) + require.NoError(state.PutCurrentValidator(&Staker{ + TxID: ids.GenerateTestID(), + SubnetID: sov.SubnetID, + NodeID: defaultValidatorNodeID, + })) + sov.EndAccumulatedFee = test.initialEndAccumulatedFee require.NoError(state.PutSubnetOnlyValidator(sov)) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 978cb78c2727..9c58a60bf2e2 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -26,8 +26,9 @@ var ( _ btree.LessFunc[SubnetOnlyValidator] = SubnetOnlyValidator.Less _ utils.Sortable[SubnetOnlyValidator] = SubnetOnlyValidator{} - ErrMutatedSubnetOnlyValidator = errors.New("subnet only validator contains mutated constant fields") - ErrDuplicateSubnetOnlyValidator = errors.New("subnet only validator contains conflicting subnetID + nodeID pair") + ErrMutatedSubnetOnlyValidator = errors.New("subnet only validator contains mutated constant fields") + ErrConflictingSubnetOnlyValidator = errors.New("subnet only validator contains conflicting subnetID + nodeID pair") + ErrDuplicateSubnetOnlyValidator = errors.New("subnet only validator contains duplicate subnetID + nodeID pair") errUnexpectedSubnetIDNodeIDLength = fmt.Errorf("expected subnetID+nodeID entry length %d", subnetIDNodeIDEntryLength) ) @@ -233,7 +234,7 @@ func (d *subnetOnlyValidatorsDiff) hasSubnetOnlyValidator(subnetID ids.ID, nodeI return has, modified } -func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state SubnetOnlyValidators, sov SubnetOnlyValidator) error { +func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov SubnetOnlyValidator) error { var ( prevWeight uint64 prevActive bool @@ -248,6 +249,16 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state SubnetOnlyValida prevWeight = priorSOV.Weight prevActive = priorSOV.EndAccumulatedFee != 0 case database.ErrNotFound: + // Verify that there is not a legacy subnet validator with the same + // subnetID+nodeID as this L1 validator. + _, err := state.GetCurrentValidator(sov.SubnetID, sov.NodeID) + if err == nil { + return ErrConflictingSubnetOnlyValidator + } + if err != database.ErrNotFound { + return err + } + has, err := state.HasSubnetOnlyValidator(sov.SubnetID, sov.NodeID) if err != nil { return err From 7f517c7ced96bbcaf7a929939acf3eebb5f2b644 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 24 Oct 2024 18:40:16 -0400 Subject: [PATCH 005/184] Fix legacy validator migration --- vms/platformvm/state/state.go | 627 +++++++++++++++-------------- vms/platformvm/state/state_test.go | 41 ++ 2 files changed, 359 insertions(+), 309 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 948dede49f1e..448f7414d78c 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1952,10 +1952,11 @@ func (s *state) write(updateValidators bool, height uint64) error { return errors.Join( s.writeBlocks(), s.writeExpiry(), - s.writeSubnetOnlyValidators(updateValidators, height), - s.writeCurrentStakers(updateValidators, height, codecVersion), + s.writeValidatorDiffs(height), + s.writeCurrentStakers(updateValidators, codecVersion), s.writePendingStakers(), s.WriteValidatorMetadata(s.currentValidatorList, s.currentSubnetValidatorList, codecVersion), // Must be called after writeCurrentStakers + s.writeSubnetOnlyValidators(updateValidators), s.writeTXs(), s.writeRewardUTXOs(), s.writeUTXOs(), @@ -2206,248 +2207,78 @@ func (s *state) writeExpiry() error { return nil } -// TODO: Add caching -func (s *state) writeSubnetOnlyValidators(updateValidators bool, height uint64) error { - // Write modified weights: - for subnetID, weight := range s.sovDiff.modifiedTotalWeight { - if err := database.PutUInt64(s.weightsDB, subnetID[:], weight); err != nil { - return err - } - } - maps.Clear(s.sovDiff.modifiedTotalWeight) - - historicalDiffs, err := s.makeSubnetOnlyValidatorHistoricalDiffs() - if err != nil { - return err +func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) { + if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is still present. + return vdr.validator.PublicKey, nil } - for subnetIDNodeID, diff := range historicalDiffs { - diffKey := marshalDiffKey(subnetIDNodeID.subnetID, height, subnetIDNodeID.nodeID) - if diff.weightDiff.Amount != 0 { - err := s.validatorWeightDiffsDB.Put( - diffKey, - marshalWeightDiff(&diff.weightDiff), - ) - if err != nil { - return err - } - } - if !bytes.Equal(diff.prevPublicKey, diff.newPublicKey) { - err := s.validatorPublicKeyDiffsDB.Put( - diffKey, - diff.prevPublicKey, - ) - if err != nil { - return err - } - } + if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is being removed. + return vdr.validator.PublicKey, nil } - sovChanges := s.sovDiff.modified - // Perform deletions: - for validationID, sov := range sovChanges { - if sov.Weight != 0 { - // Additions and modifications are handled in the next loops. - continue - } - - // The next loops shouldn't consider this change. - delete(sovChanges, validationID) - - priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - if err == database.ErrNotFound { - // Deleting a non-existent validator is a noop. This can happen if - // the validator was added and then immediately removed. - continue - } - if err != nil { - return err - } - - subnetIDNodeID := subnetIDNodeID{ - subnetID: sov.SubnetID, - nodeID: sov.NodeID, - } - subnetIDNodeIDKey := subnetIDNodeID.Marshal() - if err := s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey); err != nil { - return err - } - - if priorSOV.isActive() { - delete(s.activeSOVLookup, validationID) - s.activeSOVs.Delete(priorSOV) - err = deleteSubnetOnlyValidator(s.activeDB, validationID) - } else { - err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) - } - if err != nil { - return err - } - - // TODO: Move the validator set management out of the state package - if !updateValidators { - continue - } + // This should never happen as the primary network diffs are + // written last and subnet validator times must be a subset + // of the primary network validator times. + return nil, fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) +} - nodeID := ids.EmptyNodeID - if priorSOV.isActive() { - nodeID = priorSOV.NodeID - } - if err := s.validators.RemoveWeight(priorSOV.SubnetID, nodeID, priorSOV.Weight); err != nil { - return fmt.Errorf("failed to delete SoV validator: %w", err) - } +func (s *state) writeValidatorDiffs(height uint64) error { + type validatorChanges struct { + weightDiff ValidatorWeightDiff + prevPublicKey []byte + newPublicKey []byte } + changes := make(map[subnetIDNodeID]*validatorChanges, len(s.sovDiff.modified)) - // Perform modifications: - for validationID, sov := range sovChanges { - priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - if err == database.ErrNotFound { - // New additions are handled in the next loop. - continue - } - if err != nil { - return err - } - - if priorSOV.isActive() { - delete(s.activeSOVLookup, validationID) - s.activeSOVs.Delete(priorSOV) - err = deleteSubnetOnlyValidator(s.activeDB, validationID) - } else { - err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) - } - if err != nil { - return err - } - - if sov.isActive() { - s.activeSOVLookup[validationID] = sov - s.activeSOVs.ReplaceOrInsert(sov) - err = putSubnetOnlyValidator(s.activeDB, sov) - } else { - err = putSubnetOnlyValidator(s.inactiveDB, sov) - } - if err != nil { - return err - } - - // The next loop shouldn't consider this change. - delete(sovChanges, validationID) - - // TODO: Move the validator set management out of the state package - if !updateValidators { - continue - } - - switch { - case !priorSOV.isActive() && sov.isActive(): - // This validator is being activated. - pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) - err = errors.Join( - s.validators.RemoveWeight(sov.SubnetID, ids.EmptyNodeID, priorSOV.Weight), - s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight), - ) - case priorSOV.isActive() && !sov.isActive(): - // This validator is being deactivated. - inactiveWeight := s.validators.GetWeight(sov.SubnetID, ids.EmptyNodeID) - if inactiveWeight == 0 { - err = s.validators.AddStaker(sov.SubnetID, ids.EmptyNodeID, nil, ids.Empty, sov.Weight) - } else { - err = s.validators.AddWeight(sov.SubnetID, ids.EmptyNodeID, sov.Weight) - } - err = errors.Join( - err, - s.validators.RemoveWeight(sov.SubnetID, sov.NodeID, priorSOV.Weight), - ) - default: - // This validator's active status isn't changing. - nodeID := ids.EmptyNodeID - if sov.isActive() { - nodeID = sov.NodeID + for subnetID, subnetDiffs := range s.currentStakers.validatorDiffs { + for nodeID, diff := range subnetDiffs { + change := &validatorChanges{ + weightDiff: ValidatorWeightDiff{ + Decrease: diff.validatorStatus == deleted, + }, } - if priorSOV.Weight < sov.Weight { - err = s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight-priorSOV.Weight) - } else if priorSOV.Weight > sov.Weight { - err = s.validators.RemoveWeight(sov.SubnetID, nodeID, priorSOV.Weight-sov.Weight) + if diff.validatorStatus != unmodified { + change.weightDiff.Amount = diff.validator.Weight } - } - if err != nil { - return err - } - } - - // Perform additions: - for validationID, sov := range sovChanges { - validationID := validationID - - subnetIDNodeID := subnetIDNodeID{ - subnetID: sov.SubnetID, - nodeID: sov.NodeID, - } - subnetIDNodeIDKey := subnetIDNodeID.Marshal() - if err := s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]); err != nil { - return err - } - isActive := sov.isActive() - if isActive { - s.activeSOVLookup[validationID] = sov - s.activeSOVs.ReplaceOrInsert(sov) - err = putSubnetOnlyValidator(s.activeDB, sov) - } else { - err = putSubnetOnlyValidator(s.inactiveDB, sov) - } - if err != nil { - return err - } + for _, staker := range diff.deletedDelegators { + if err := change.weightDiff.Add(true, staker.Weight); err != nil { + return fmt.Errorf("failed to decrease node weight diff: %w", err) + } + } - // TODO: Move the validator set management out of the state package - if !updateValidators { - continue - } + addedDelegatorIterator := iterator.FromTree(diff.addedDelegators) + for addedDelegatorIterator.Next() { + staker := addedDelegatorIterator.Value() + if err := change.weightDiff.Add(false, staker.Weight); err != nil { + addedDelegatorIterator.Release() + return fmt.Errorf("failed to increase node weight diff: %w", err) + } + } + addedDelegatorIterator.Release() - if isActive { - pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) - if err := s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight); err != nil { - return fmt.Errorf("failed to add SoV validator: %w", err) + pk, err := s.getInheritedPublicKey(nodeID) + if err != nil { + return err + } + if pk != nil { + switch diff.validatorStatus { + case added: + change.newPublicKey = bls.PublicKeyToUncompressedBytes(pk) + case deleted: + change.prevPublicKey = bls.PublicKeyToUncompressedBytes(pk) + } } - continue - } - // This validator is inactive - inactiveWeight := s.validators.GetWeight(sov.SubnetID, ids.EmptyNodeID) - if inactiveWeight == 0 { - err = s.validators.AddStaker(sov.SubnetID, ids.EmptyNodeID, nil, ids.Empty, sov.Weight) - } else { - err = s.validators.AddWeight(sov.SubnetID, ids.EmptyNodeID, sov.Weight) - } - if err != nil { - return err + subnetIDNodeID := subnetIDNodeID{ + subnetID: subnetID, + nodeID: nodeID, + } + changes[subnetIDNodeID] = change } } - s.sovDiff = newSubnetOnlyValidatorsDiff() - return nil -} - -type validatorChanges struct { - weightDiff ValidatorWeightDiff - prevPublicKey []byte - newPublicKey []byte -} - -func getOrDefault[K comparable, V any](m map[K]*V, k K) *V { - if v, ok := m[k]; ok { - return v - } - - v := new(V) - m[k] = v - return v -} - -func (s *state) makeSubnetOnlyValidatorHistoricalDiffs() (map[subnetIDNodeID]*validatorChanges, error) { - changes := make(map[subnetIDNodeID]*validatorChanges, len(s.sovDiff.modified)) - // Perform deletions: for validationID := range s.sovDiff.modified { priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) @@ -2455,7 +2286,7 @@ func (s *state) makeSubnetOnlyValidatorHistoricalDiffs() (map[subnetIDNodeID]*va continue } if err != nil { - return nil, err + return err } var ( @@ -2474,7 +2305,7 @@ func (s *state) makeSubnetOnlyValidatorHistoricalDiffs() (map[subnetIDNodeID]*va } if err := diff.weightDiff.Add(true, priorSOV.Weight); err != nil { - return nil, err + return err } } @@ -2501,14 +2332,45 @@ func (s *state) makeSubnetOnlyValidatorHistoricalDiffs() (map[subnetIDNodeID]*va } if err := diff.weightDiff.Add(false, sov.Weight); err != nil { - return nil, err + return err + } + } + + for subnetIDNodeID, diff := range changes { + diffKey := marshalDiffKey(subnetIDNodeID.subnetID, height, subnetIDNodeID.nodeID) + if diff.weightDiff.Amount != 0 { + err := s.validatorWeightDiffsDB.Put( + diffKey, + marshalWeightDiff(&diff.weightDiff), + ) + if err != nil { + return err + } + } + if !bytes.Equal(diff.prevPublicKey, diff.newPublicKey) { + err := s.validatorPublicKeyDiffsDB.Put( + diffKey, + diff.prevPublicKey, + ) + if err != nil { + return err + } } } + return nil +} + +func getOrDefault[K comparable, V any](m map[K]*V, k K) *V { + if v, ok := m[k]; ok { + return v + } - return changes, nil + v := new(V) + m[k] = v + return v } -func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecVersion uint16) error { +func (s *state) writeCurrentStakers(updateValidators bool, codecVersion uint16) error { for subnetID, validatorDiffs := range s.currentStakers.validatorDiffs { // We must write the primary network stakers last because writing subnet // validator diffs may depend on the primary network validator diffs to @@ -2517,33 +2379,31 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecV continue } - delete(s.currentStakers.validatorDiffs, subnetID) - err := s.writeCurrentStakersSubnetDiff( subnetID, validatorDiffs, updateValidators, - height, codecVersion, ) if err != nil { return err } + + delete(s.currentStakers.validatorDiffs, subnetID) } if validatorDiffs, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID]; ok { - delete(s.currentStakers.validatorDiffs, constants.PrimaryNetworkID) - err := s.writeCurrentStakersSubnetDiff( constants.PrimaryNetworkID, validatorDiffs, updateValidators, - height, codecVersion, ) if err != nil { return err } + + delete(s.currentStakers.validatorDiffs, constants.PrimaryNetworkID) } // TODO: Move validator set management out of the state package @@ -2566,7 +2426,6 @@ func (s *state) writeCurrentStakersSubnetDiff( subnetID ids.ID, validatorDiffs map[ids.NodeID]*diffValidator, updateValidators bool, - height uint64, codecVersion uint16, ) error { // Select db to write to @@ -2579,52 +2438,13 @@ func (s *state) writeCurrentStakersSubnetDiff( // Record the change in weight and/or public key for each validator. for nodeID, validatorDiff := range validatorDiffs { - var ( - staker *Staker - pk *bls.PublicKey - weightDiff = &ValidatorWeightDiff{ - Decrease: validatorDiff.validatorStatus == deleted, - } - ) - if validatorDiff.validatorStatus != unmodified { - staker = validatorDiff.validator - - pk = staker.PublicKey - // For non-primary network validators, the public key is inherited - // from the primary network. - if subnetID != constants.PrimaryNetworkID { - if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { - // The primary network validator is still present after - // writing. - pk = vdr.validator.PublicKey - } else if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { - // The primary network validator is being removed during - // writing. - pk = vdr.validator.PublicKey - } else { - // This should never happen as the primary network diffs are - // written last and subnet validator times must be a subset - // of the primary network validator times. - return fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) - } - } - - weightDiff.Amount = staker.Weight - } - + weightDiff := &ValidatorWeightDiff{} switch validatorDiff.validatorStatus { case added: - if pk != nil { - // Record that the public key for the validator is being added. - // This means the prior value for the public key was nil. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - nil, - ) - if err != nil { - return err - } - } + staker := validatorDiff.validator + + weightDiff.Decrease = false + weightDiff.Amount = staker.Weight // The validator is being added. // @@ -2653,23 +2473,10 @@ func (s *state) writeCurrentStakersSubnetDiff( s.validatorState.LoadValidatorMetadata(nodeID, subnetID, metadata) case deleted: - if pk != nil { - // Record that the public key for the validator is being - // removed. This means we must record the prior value of the - // public key. - // - // Note: We store the uncompressed public key here as it is - // significantly more efficient to parse when applying diffs. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - bls.PublicKeyToUncompressedBytes(pk), - ) - if err != nil { - return err - } - } + weightDiff.Decrease = true + weightDiff.Amount = validatorDiff.validator.Weight - if err := validatorDB.Delete(staker.TxID[:]); err != nil { + if err := validatorDB.Delete(validatorDiff.validator.TxID[:]); err != nil { return fmt.Errorf("failed to delete current staker: %w", err) } @@ -2691,14 +2498,6 @@ func (s *state) writeCurrentStakersSubnetDiff( continue } - err = s.validatorWeightDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - marshalWeightDiff(weightDiff), - ) - if err != nil { - return err - } - // TODO: Move the validator set management out of the state package if !updateValidators { continue @@ -2708,11 +2507,17 @@ func (s *state) writeCurrentStakersSubnetDiff( err = s.validators.RemoveWeight(subnetID, nodeID, weightDiff.Amount) } else { if validatorDiff.validatorStatus == added { + var pk *bls.PublicKey + pk, err = s.getInheritedPublicKey(nodeID) + if err != nil { + return err + } + err = s.validators.AddStaker( subnetID, nodeID, pk, - staker.TxID, + validatorDiff.validator.TxID, weightDiff.Amount, ) } else { @@ -2824,6 +2629,210 @@ func writePendingDiff( return nil } +// TODO: Add caching +// +// writeSubnetOnlyValidators must be called after writeCurrentStakers to ensure +// any legacy validators that were removed and then re-added as SoVs are +// correctly written +func (s *state) writeSubnetOnlyValidators(updateValidators bool) error { + // Write modified weights: + for subnetID, weight := range s.sovDiff.modifiedTotalWeight { + if err := database.PutUInt64(s.weightsDB, subnetID[:], weight); err != nil { + return err + } + } + maps.Clear(s.sovDiff.modifiedTotalWeight) + + sovChanges := s.sovDiff.modified + // Perform deletions: + for validationID, sov := range sovChanges { + if sov.Weight != 0 { + // Additions and modifications are handled in the next loops. + continue + } + + // The next loops shouldn't consider this change. + delete(sovChanges, validationID) + + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) + if err == database.ErrNotFound { + // Deleting a non-existent validator is a noop. This can happen if + // the validator was added and then immediately removed. + continue + } + if err != nil { + return err + } + + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + subnetIDNodeIDKey := subnetIDNodeID.Marshal() + if err := s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey); err != nil { + return err + } + + if priorSOV.isActive() { + delete(s.activeSOVLookup, validationID) + s.activeSOVs.Delete(priorSOV) + err = deleteSubnetOnlyValidator(s.activeDB, validationID) + } else { + err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) + } + if err != nil { + return err + } + + // TODO: Move the validator set management out of the state package + if !updateValidators { + continue + } + + nodeID := ids.EmptyNodeID + if priorSOV.isActive() { + nodeID = priorSOV.NodeID + } + if err := s.validators.RemoveWeight(priorSOV.SubnetID, nodeID, priorSOV.Weight); err != nil { + return fmt.Errorf("failed to delete SoV validator: %w", err) + } + } + + // Perform modifications: + for validationID, sov := range sovChanges { + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) + if err == database.ErrNotFound { + // New additions are handled in the next loop. + continue + } + if err != nil { + return err + } + + if priorSOV.isActive() { + delete(s.activeSOVLookup, validationID) + s.activeSOVs.Delete(priorSOV) + err = deleteSubnetOnlyValidator(s.activeDB, validationID) + } else { + err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) + } + if err != nil { + return err + } + + if sov.isActive() { + s.activeSOVLookup[validationID] = sov + s.activeSOVs.ReplaceOrInsert(sov) + err = putSubnetOnlyValidator(s.activeDB, sov) + } else { + err = putSubnetOnlyValidator(s.inactiveDB, sov) + } + if err != nil { + return err + } + + // The next loop shouldn't consider this change. + delete(sovChanges, validationID) + + // TODO: Move the validator set management out of the state package + if !updateValidators { + continue + } + + switch { + case !priorSOV.isActive() && sov.isActive(): + // This validator is being activated. + pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) + err = errors.Join( + s.validators.RemoveWeight(sov.SubnetID, ids.EmptyNodeID, priorSOV.Weight), + s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight), + ) + case priorSOV.isActive() && !sov.isActive(): + // This validator is being deactivated. + inactiveWeight := s.validators.GetWeight(sov.SubnetID, ids.EmptyNodeID) + if inactiveWeight == 0 { + err = s.validators.AddStaker(sov.SubnetID, ids.EmptyNodeID, nil, ids.Empty, sov.Weight) + } else { + err = s.validators.AddWeight(sov.SubnetID, ids.EmptyNodeID, sov.Weight) + } + err = errors.Join( + err, + s.validators.RemoveWeight(sov.SubnetID, sov.NodeID, priorSOV.Weight), + ) + default: + // This validator's active status isn't changing. + nodeID := ids.EmptyNodeID + if sov.isActive() { + nodeID = sov.NodeID + } + if priorSOV.Weight < sov.Weight { + err = s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight-priorSOV.Weight) + } else if priorSOV.Weight > sov.Weight { + err = s.validators.RemoveWeight(sov.SubnetID, nodeID, priorSOV.Weight-sov.Weight) + } + } + if err != nil { + return err + } + } + + // Perform additions: + for validationID, sov := range sovChanges { + validationID := validationID + + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + subnetIDNodeIDKey := subnetIDNodeID.Marshal() + if err := s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]); err != nil { + return err + } + + var ( + isActive = sov.isActive() + err error + ) + if isActive { + s.activeSOVLookup[validationID] = sov + s.activeSOVs.ReplaceOrInsert(sov) + err = putSubnetOnlyValidator(s.activeDB, sov) + } else { + err = putSubnetOnlyValidator(s.inactiveDB, sov) + } + if err != nil { + return err + } + + // TODO: Move the validator set management out of the state package + if !updateValidators { + continue + } + + if isActive { + pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) + if err := s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight); err != nil { + return fmt.Errorf("failed to add SoV validator: %w", err) + } + continue + } + + // This validator is inactive + inactiveWeight := s.validators.GetWeight(sov.SubnetID, ids.EmptyNodeID) + if inactiveWeight == 0 { + err = s.validators.AddStaker(sov.SubnetID, ids.EmptyNodeID, nil, ids.Empty, sov.Weight) + } else { + err = s.validators.AddWeight(sov.SubnetID, ids.EmptyNodeID, sov.Weight) + } + if err != nil { + return err + } + } + + s.sovDiff = newSubnetOnlyValidatorsDiff() + return nil +} + func (s *state) writeTXs() error { for txID, txStatus := range s.addedTxs { txID := txID diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index b5ebc572fea8..f4f50ebdf05d 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1889,3 +1889,44 @@ func TestSubnetOnlyValidators(t *testing.T) { }) } } + +func TestSubnetOnlyValidatorAfterLegacyRemoval(t *testing.T) { + require := require.New(t) + + db := memdb.New() + state := newTestState(t, db) + + legacyStaker := &Staker{ + TxID: ids.GenerateTestID(), + NodeID: defaultValidatorNodeID, + PublicKey: nil, + SubnetID: ids.GenerateTestID(), + Weight: 1, + StartTime: genesistest.DefaultValidatorStartTime, + EndTime: genesistest.DefaultValidatorEndTime, + PotentialReward: 0, + } + require.NoError(state.PutCurrentValidator(legacyStaker)) + + state.SetHeight(1) + require.NoError(state.Commit()) + + state.DeleteCurrentValidator(legacyStaker) + + sov := SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: legacyStaker.SubnetID, + NodeID: legacyStaker.NodeID, + PublicKey: utils.RandomBytes(bls.PublicKeyLen), + RemainingBalanceOwner: utils.RandomBytes(32), + DeactivationOwner: utils.RandomBytes(32), + StartTime: 1, + Weight: 2, + MinNonce: 3, + EndAccumulatedFee: 4, + } + require.NoError(state.PutSubnetOnlyValidator(sov)) + + state.SetHeight(2) + require.NoError(state.Commit()) +} From f212c3e84e5a5921f570438f9caaaa3839a9e53f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 25 Oct 2024 11:34:56 -0400 Subject: [PATCH 006/184] ACP-77: Write subnet public key diffs to state (#3487) --- vms/platformvm/state/mock_state.go | 8 +- vms/platformvm/state/state.go | 345 ++++++++++++++++----------- vms/platformvm/state/state_test.go | 71 +++++- vms/platformvm/validators/manager.go | 8 +- 4 files changed, 277 insertions(+), 155 deletions(-) diff --git a/vms/platformvm/state/mock_state.go b/vms/platformvm/state/mock_state.go index cb05f54fc6f7..5eab1b07ee99 100644 --- a/vms/platformvm/state/mock_state.go +++ b/vms/platformvm/state/mock_state.go @@ -149,17 +149,17 @@ func (mr *MockStateMockRecorder) AddUTXO(utxo any) *gomock.Call { } // ApplyValidatorPublicKeyDiffs mocks base method. -func (m *MockState) ApplyValidatorPublicKeyDiffs(ctx context.Context, validators map[ids.NodeID]*validators.GetValidatorOutput, startHeight, endHeight uint64) error { +func (m *MockState) ApplyValidatorPublicKeyDiffs(ctx context.Context, validators map[ids.NodeID]*validators.GetValidatorOutput, startHeight, endHeight uint64, subnetID ids.ID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ApplyValidatorPublicKeyDiffs", ctx, validators, startHeight, endHeight) + ret := m.ctrl.Call(m, "ApplyValidatorPublicKeyDiffs", ctx, validators, startHeight, endHeight, subnetID) ret0, _ := ret[0].(error) return ret0 } // ApplyValidatorPublicKeyDiffs indicates an expected call of ApplyValidatorPublicKeyDiffs. -func (mr *MockStateMockRecorder) ApplyValidatorPublicKeyDiffs(ctx, validators, startHeight, endHeight any) *gomock.Call { +func (mr *MockStateMockRecorder) ApplyValidatorPublicKeyDiffs(ctx, validators, startHeight, endHeight, subnetID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyValidatorPublicKeyDiffs", reflect.TypeOf((*MockState)(nil).ApplyValidatorPublicKeyDiffs), ctx, validators, startHeight, endHeight) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyValidatorPublicKeyDiffs", reflect.TypeOf((*MockState)(nil).ApplyValidatorPublicKeyDiffs), ctx, validators, startHeight, endHeight, subnetID) } // ApplyValidatorWeightDiffs mocks base method. diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 53b109c23249..c70547265cf1 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -59,8 +59,9 @@ const ( var ( _ State = (*state)(nil) - errValidatorSetAlreadyPopulated = errors.New("validator set already populated") - errIsNotSubnet = errors.New("is not a subnet") + errValidatorSetAlreadyPopulated = errors.New("validator set already populated") + errIsNotSubnet = errors.New("is not a subnet") + errMissingPrimaryNetworkValidator = errors.New("missing primary network validator") BlockIDPrefix = []byte("blockID") BlockPrefix = []byte("block") @@ -193,6 +194,7 @@ type State interface { validators map[ids.NodeID]*validators.GetValidatorOutput, startHeight uint64, endHeight uint64, + subnetID ids.ID, ) error SetHeight(height uint64) @@ -1257,10 +1259,11 @@ func (s *state) ApplyValidatorPublicKeyDiffs( validators map[ids.NodeID]*validators.GetValidatorOutput, startHeight uint64, endHeight uint64, + subnetID ids.ID, ) error { diffIter := s.validatorPublicKeyDiffsDB.NewIteratorWithStartAndPrefix( - marshalStartDiffKey(constants.PrimaryNetworkID, startHeight), - constants.PrimaryNetworkID[:], + marshalStartDiffKey(subnetID, startHeight), + subnetID[:], ) defer diffIter.Release() @@ -1736,19 +1739,30 @@ func (s *state) loadPendingValidators() error { // Invariant: initValidatorSets requires loadCurrentValidators to have already // been called. func (s *state) initValidatorSets() error { - for subnetID, validators := range s.currentStakers.validators { + primaryNetworkValidators := s.currentStakers.validators[constants.PrimaryNetworkID] + for subnetID, subnetValidators := range s.currentStakers.validators { if s.validators.Count(subnetID) != 0 { // Enforce the invariant that the validator set is empty here. return fmt.Errorf("%w: %s", errValidatorSetAlreadyPopulated, subnetID) } - for nodeID, validator := range validators { - validatorStaker := validator.validator - if err := s.validators.AddStaker(subnetID, nodeID, validatorStaker.PublicKey, validatorStaker.TxID, validatorStaker.Weight); err != nil { + for nodeID, subnetValidator := range subnetValidators { + // The subnet validator's Public Key is inherited from the + // corresponding primary network validator. + primaryValidator, ok := primaryNetworkValidators[nodeID] + if !ok { + return fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) + } + + var ( + primaryStaker = primaryValidator.validator + subnetStaker = subnetValidator.validator + ) + if err := s.validators.AddStaker(subnetID, nodeID, primaryStaker.PublicKey, subnetStaker.TxID, subnetStaker.Weight); err != nil { return err } - delegatorIterator := iterator.FromTree(validator.delegators) + delegatorIterator := iterator.FromTree(subnetValidator.delegators) for delegatorIterator.Next() { delegatorStaker := delegatorIterator.Value() if err := s.validators.AddWeight(subnetID, nodeID, delegatorStaker.Weight); err != nil { @@ -2028,164 +2042,219 @@ func (s *state) writeExpiry() error { func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecVersion uint16) error { for subnetID, validatorDiffs := range s.currentStakers.validatorDiffs { + // We must write the primary network stakers last because writing subnet + // validator diffs may depend on the primary network validator diffs to + // inherit the public keys. + if subnetID == constants.PrimaryNetworkID { + continue + } + delete(s.currentStakers.validatorDiffs, subnetID) - // Select db to write to - validatorDB := s.currentSubnetValidatorList - delegatorDB := s.currentSubnetDelegatorList - if subnetID == constants.PrimaryNetworkID { - validatorDB = s.currentValidatorList - delegatorDB = s.currentDelegatorList + err := s.writeCurrentStakersSubnetDiff( + subnetID, + validatorDiffs, + updateValidators, + height, + codecVersion, + ) + if err != nil { + return err } + } - // Record the change in weight and/or public key for each validator. - for nodeID, validatorDiff := range validatorDiffs { - // Copy [nodeID] so it doesn't get overwritten next iteration. - nodeID := nodeID + if validatorDiffs, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID]; ok { + delete(s.currentStakers.validatorDiffs, constants.PrimaryNetworkID) - weightDiff := &ValidatorWeightDiff{ - Decrease: validatorDiff.validatorStatus == deleted, - } - switch validatorDiff.validatorStatus { - case added: - staker := validatorDiff.validator - weightDiff.Amount = staker.Weight - - // Invariant: Only the Primary Network contains non-nil public - // keys. - if staker.PublicKey != nil { - // Record that the public key for the validator is being - // added. This means the prior value for the public key was - // nil. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(constants.PrimaryNetworkID, height, nodeID), - nil, - ) - if err != nil { - return err - } - } + err := s.writeCurrentStakersSubnetDiff( + constants.PrimaryNetworkID, + validatorDiffs, + updateValidators, + height, + codecVersion, + ) + if err != nil { + return err + } + } - // The validator is being added. - // - // Invariant: It's impossible for a delegator to have been - // rewarded in the same block that the validator was added. - startTime := uint64(staker.StartTime.Unix()) - metadata := &validatorMetadata{ - txID: staker.TxID, - lastUpdated: staker.StartTime, - - UpDuration: 0, - LastUpdated: startTime, - StakerStartTime: startTime, - PotentialReward: staker.PotentialReward, - PotentialDelegateeReward: 0, - } + // TODO: Move validator set management out of the state package + if !updateValidators { + return nil + } - metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata) - if err != nil { - return fmt.Errorf("failed to serialize current validator: %w", err) - } + // Update the stake metrics + totalWeight, err := s.validators.TotalWeight(constants.PrimaryNetworkID) + if err != nil { + return fmt.Errorf("failed to get total weight of primary network: %w", err) + } - if err = validatorDB.Put(staker.TxID[:], metadataBytes); err != nil { - return fmt.Errorf("failed to write current validator to list: %w", err) - } + s.metrics.SetLocalStake(s.validators.GetWeight(constants.PrimaryNetworkID, s.ctx.NodeID)) + s.metrics.SetTotalStake(totalWeight) + return nil +} - s.validatorState.LoadValidatorMetadata(nodeID, subnetID, metadata) - case deleted: - staker := validatorDiff.validator - weightDiff.Amount = staker.Weight - - // Invariant: Only the Primary Network contains non-nil public - // keys. - if staker.PublicKey != nil { - // Record that the public key for the validator is being - // removed. This means we must record the prior value of the - // public key. - // - // Note: We store the uncompressed public key here as it is - // significantly more efficient to parse when applying - // diffs. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(constants.PrimaryNetworkID, height, nodeID), - bls.PublicKeyToUncompressedBytes(staker.PublicKey), - ) - if err != nil { - return err - } - } +func (s *state) writeCurrentStakersSubnetDiff( + subnetID ids.ID, + validatorDiffs map[ids.NodeID]*diffValidator, + updateValidators bool, + height uint64, + codecVersion uint16, +) error { + // Select db to write to + validatorDB := s.currentSubnetValidatorList + delegatorDB := s.currentSubnetDelegatorList + if subnetID == constants.PrimaryNetworkID { + validatorDB = s.currentValidatorList + delegatorDB = s.currentDelegatorList + } - if err := validatorDB.Delete(staker.TxID[:]); err != nil { - return fmt.Errorf("failed to delete current staker: %w", err) + // Record the change in weight and/or public key for each validator. + for nodeID, validatorDiff := range validatorDiffs { + var ( + staker *Staker + pk *bls.PublicKey + weightDiff = &ValidatorWeightDiff{ + Decrease: validatorDiff.validatorStatus == deleted, + } + ) + if validatorDiff.validatorStatus != unmodified { + staker = validatorDiff.validator + + pk = staker.PublicKey + // For non-primary network validators, the public key is inherited + // from the primary network. + if subnetID != constants.PrimaryNetworkID { + if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is still present after + // writing. + pk = vdr.validator.PublicKey + } else if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is being removed during + // writing. + pk = vdr.validator.PublicKey + } else { + // This should never happen as the primary network diffs are + // written last and subnet validator times must be a subset + // of the primary network validator times. + return fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) } - - s.validatorState.DeleteValidatorMetadata(nodeID, subnetID) } - err := writeCurrentDelegatorDiff( - delegatorDB, - weightDiff, - validatorDiff, - codecVersion, - ) - if err != nil { - return err + weightDiff.Amount = staker.Weight + } + + switch validatorDiff.validatorStatus { + case added: + if pk != nil { + // Record that the public key for the validator is being added. + // This means the prior value for the public key was nil. + err := s.validatorPublicKeyDiffsDB.Put( + marshalDiffKey(subnetID, height, nodeID), + nil, + ) + if err != nil { + return err + } } - if weightDiff.Amount == 0 { - // No weight change to record; go to next validator. - continue + // The validator is being added. + // + // Invariant: It's impossible for a delegator to have been rewarded + // in the same block that the validator was added. + startTime := uint64(staker.StartTime.Unix()) + metadata := &validatorMetadata{ + txID: staker.TxID, + lastUpdated: staker.StartTime, + + UpDuration: 0, + LastUpdated: startTime, + StakerStartTime: startTime, + PotentialReward: staker.PotentialReward, + PotentialDelegateeReward: 0, } - err = s.validatorWeightDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - marshalWeightDiff(weightDiff), - ) + metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata) if err != nil { - return err + return fmt.Errorf("failed to serialize current validator: %w", err) } - // TODO: Move the validator set management out of the state package - if !updateValidators { - continue + if err = validatorDB.Put(staker.TxID[:], metadataBytes); err != nil { + return fmt.Errorf("failed to write current validator to list: %w", err) } - if weightDiff.Decrease { - err = s.validators.RemoveWeight(subnetID, nodeID, weightDiff.Amount) - } else { - if validatorDiff.validatorStatus == added { - staker := validatorDiff.validator - err = s.validators.AddStaker( - subnetID, - nodeID, - staker.PublicKey, - staker.TxID, - weightDiff.Amount, - ) - } else { - err = s.validators.AddWeight(subnetID, nodeID, weightDiff.Amount) + s.validatorState.LoadValidatorMetadata(nodeID, subnetID, metadata) + case deleted: + if pk != nil { + // Record that the public key for the validator is being + // removed. This means we must record the prior value of the + // public key. + // + // Note: We store the uncompressed public key here as it is + // significantly more efficient to parse when applying diffs. + err := s.validatorPublicKeyDiffsDB.Put( + marshalDiffKey(subnetID, height, nodeID), + bls.PublicKeyToUncompressedBytes(pk), + ) + if err != nil { + return err } } - if err != nil { - return fmt.Errorf("failed to update validator weight: %w", err) + + if err := validatorDB.Delete(staker.TxID[:]); err != nil { + return fmt.Errorf("failed to delete current staker: %w", err) } + + s.validatorState.DeleteValidatorMetadata(nodeID, subnetID) } - } - // TODO: Move validator set management out of the state package - // - // Attempt to update the stake metrics - if !updateValidators { - return nil - } + err := writeCurrentDelegatorDiff( + delegatorDB, + weightDiff, + validatorDiff, + codecVersion, + ) + if err != nil { + return err + } - totalWeight, err := s.validators.TotalWeight(constants.PrimaryNetworkID) - if err != nil { - return fmt.Errorf("failed to get total weight of primary network: %w", err) - } + if weightDiff.Amount == 0 { + // No weight change to record; go to next validator. + continue + } - s.metrics.SetLocalStake(s.validators.GetWeight(constants.PrimaryNetworkID, s.ctx.NodeID)) - s.metrics.SetTotalStake(totalWeight) + err = s.validatorWeightDiffsDB.Put( + marshalDiffKey(subnetID, height, nodeID), + marshalWeightDiff(weightDiff), + ) + if err != nil { + return err + } + + // TODO: Move the validator set management out of the state package + if !updateValidators { + continue + } + + if weightDiff.Decrease { + err = s.validators.RemoveWeight(subnetID, nodeID, weightDiff.Amount) + } else { + if validatorDiff.validatorStatus == added { + err = s.validators.AddStaker( + subnetID, + nodeID, + pk, + staker.TxID, + weightDiff.Amount, + ) + } else { + err = s.validators.AddWeight(subnetID, nodeID, weightDiff.Amount) + } + } + if err != nil { + return fmt.Errorf("failed to update validator weight: %w", err) + } + } return nil } diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 12214c2060b0..b965af1531eb 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -278,13 +278,15 @@ func TestState_writeStakers(t *testing.T) { addStakerTx: addSubnetValidator, expectedCurrentValidator: subnetCurrentValidatorStaker, expectedValidatorSetOutput: &validators.GetValidatorOutput{ - NodeID: subnetCurrentValidatorStaker.NodeID, - Weight: subnetCurrentValidatorStaker.Weight, + NodeID: subnetCurrentValidatorStaker.NodeID, + PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, + Weight: subnetCurrentValidatorStaker.Weight, }, expectedWeightDiff: &ValidatorWeightDiff{ Decrease: false, Amount: subnetCurrentValidatorStaker.Weight, }, + expectedPublicKeyDiff: maybe.Some[*bls.PublicKey](nil), }, "delete current primary network validator": { initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker}, @@ -342,6 +344,7 @@ func TestState_writeStakers(t *testing.T) { Decrease: true, Amount: subnetCurrentValidatorStaker.Weight, }, + expectedPublicKeyDiff: maybe.Some[*bls.PublicKey](primaryNetworkCurrentValidatorStaker.PublicKey), }, } @@ -832,8 +835,9 @@ func TestState_ApplyValidatorDiffs(t *testing.T) { }, expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ subnetStakers[0].NodeID: { - NodeID: subnetStakers[0].NodeID, - Weight: subnetStakers[0].Weight, + NodeID: subnetStakers[0].NodeID, + PublicKey: primaryStakers[0].PublicKey, + Weight: subnetStakers[0].Weight, }, }, }, @@ -877,8 +881,9 @@ func TestState_ApplyValidatorDiffs(t *testing.T) { }, expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ subnetStakers[2].NodeID: { - NodeID: subnetStakers[2].NodeID, - Weight: subnetStakers[2].Weight, + NodeID: subnetStakers[2].NodeID, + PublicKey: primaryStakers[2].PublicKey, + Weight: subnetStakers[2].Weight, }, }, }, @@ -904,16 +909,19 @@ func TestState_ApplyValidatorDiffs(t *testing.T) { }, expectedSubnetValidatorSet: map[ids.NodeID]*validators.GetValidatorOutput{ subnetStakers[2].NodeID: { - NodeID: subnetStakers[2].NodeID, - Weight: subnetStakers[2].Weight, + NodeID: subnetStakers[2].NodeID, + PublicKey: primaryStakers[2].PublicKey, + Weight: subnetStakers[2].Weight, }, subnetStakers[3].NodeID: { - NodeID: subnetStakers[3].NodeID, - Weight: subnetStakers[3].Weight, + NodeID: subnetStakers[3].NodeID, + PublicKey: primaryStakers[3].PublicKey, + Weight: subnetStakers[3].Weight, }, subnetStakers[4].NodeID: { - NodeID: subnetStakers[4].NodeID, - Weight: subnetStakers[4].Weight, + NodeID: subnetStakers[4].NodeID, + PublicKey: primaryStakers[4].PublicKey, + Weight: subnetStakers[4].Weight, }, }, }, @@ -1011,10 +1019,41 @@ func TestState_ApplyValidatorDiffs(t *testing.T) { primaryValidatorSet, currentHeight, prevHeight+1, + constants.PrimaryNetworkID, )) require.Equal(prevDiff.expectedPrimaryValidatorSet, primaryValidatorSet) } + { + legacySubnetValidatorSet := copyValidatorSet(diff.expectedSubnetValidatorSet) + require.NoError(state.ApplyValidatorWeightDiffs( + context.Background(), + legacySubnetValidatorSet, + currentHeight, + prevHeight+1, + subnetID, + )) + + // Update the public keys of the subnet validators with the current + // primary network validator public keys + for nodeID, vdr := range legacySubnetValidatorSet { + if primaryVdr, ok := diff.expectedPrimaryValidatorSet[nodeID]; ok { + vdr.PublicKey = primaryVdr.PublicKey + } else { + vdr.PublicKey = nil + } + } + + require.NoError(state.ApplyValidatorPublicKeyDiffs( + context.Background(), + legacySubnetValidatorSet, + currentHeight, + prevHeight+1, + constants.PrimaryNetworkID, + )) + require.Equal(prevDiff.expectedSubnetValidatorSet, legacySubnetValidatorSet) + } + { subnetValidatorSet := copyValidatorSet(diff.expectedSubnetValidatorSet) require.NoError(state.ApplyValidatorWeightDiffs( @@ -1024,6 +1063,14 @@ func TestState_ApplyValidatorDiffs(t *testing.T) { prevHeight+1, subnetID, )) + + require.NoError(state.ApplyValidatorPublicKeyDiffs( + context.Background(), + subnetValidatorSet, + currentHeight, + prevHeight+1, + subnetID, + )) require.Equal(prevDiff.expectedSubnetValidatorSet, subnetValidatorSet) } } diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index 7f1ea5ea6407..142db3e7635c 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -85,6 +85,7 @@ type State interface { validators map[ids.NodeID]*validators.GetValidatorOutput, startHeight uint64, endHeight uint64, + subnetID ids.ID, ) error } @@ -271,7 +272,7 @@ func (m *manager) makePrimaryNetworkValidatorSet( validatorSet, currentHeight, lastDiffHeight, - constants.PlatformChainID, + constants.PrimaryNetworkID, ) if err != nil { return nil, 0, err @@ -282,6 +283,7 @@ func (m *manager) makePrimaryNetworkValidatorSet( validatorSet, currentHeight, lastDiffHeight, + constants.PrimaryNetworkID, ) return validatorSet, currentHeight, err } @@ -348,6 +350,10 @@ func (m *manager) makeSubnetValidatorSet( subnetValidatorSet, currentHeight, lastDiffHeight, + // TODO: Etna introduces L1s whose validators specify their own public + // keys, rather than inheriting them from the primary network. + // Therefore, this will need to use the subnetID after Etna. + constants.PrimaryNetworkID, ) return subnetValidatorSet, currentHeight, err } From 08bd9e3ae82cb0f8e4008d38d5d7ecbff232dbe4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 25 Oct 2024 16:31:17 -0400 Subject: [PATCH 007/184] ACP-77: Add subnetIDNodeID struct --- vms/platformvm/state/state_test.go | 4 -- vms/platformvm/state/subnet_id_node_id.go | 37 +++++++++++ .../state/subnet_id_node_id_test.go | 66 +++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 vms/platformvm/state/subnet_id_node_id.go create mode 100644 vms/platformvm/state/subnet_id_node_id_test.go diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index b965af1531eb..85df176ce42a 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -944,10 +944,6 @@ func TestState_ApplyValidatorDiffs(t *testing.T) { d, err := NewDiffOn(state) require.NoError(err) - type subnetIDNodeID struct { - subnetID ids.ID - nodeID ids.NodeID - } var expectedValidators set.Set[subnetIDNodeID] for _, added := range diff.addedValidators { require.NoError(d.PutCurrentValidator(&added)) diff --git a/vms/platformvm/state/subnet_id_node_id.go b/vms/platformvm/state/subnet_id_node_id.go new file mode 100644 index 000000000000..208c1cf8f447 --- /dev/null +++ b/vms/platformvm/state/subnet_id_node_id.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" +) + +// subnetIDNodeID = [subnetID] + [nodeID] +const subnetIDNodeIDEntryLength = ids.IDLen + ids.NodeIDLen + +var errUnexpectedSubnetIDNodeIDLength = fmt.Errorf("expected subnetID+nodeID entry length %d", subnetIDNodeIDEntryLength) + +type subnetIDNodeID struct { + subnetID ids.ID + nodeID ids.NodeID +} + +func (s *subnetIDNodeID) Marshal() []byte { + data := make([]byte, subnetIDNodeIDEntryLength) + copy(data, s.subnetID[:]) + copy(data[ids.IDLen:], s.nodeID[:]) + return data +} + +func (s *subnetIDNodeID) Unmarshal(data []byte) error { + if len(data) != subnetIDNodeIDEntryLength { + return errUnexpectedSubnetIDNodeIDLength + } + + copy(s.subnetID[:], data) + copy(s.nodeID[:], data[ids.IDLen:]) + return nil +} diff --git a/vms/platformvm/state/subnet_id_node_id_test.go b/vms/platformvm/state/subnet_id_node_id_test.go new file mode 100644 index 000000000000..4ed720b95a8e --- /dev/null +++ b/vms/platformvm/state/subnet_id_node_id_test.go @@ -0,0 +1,66 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + "github.com/thepudds/fzgen/fuzzer" +) + +func FuzzSubnetIDNodeIDMarshal(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + require := require.New(t) + + var v subnetIDNodeID + fz := fuzzer.NewFuzzer(data) + fz.Fill(&v) + + marshalledData := v.Marshal() + + var parsed subnetIDNodeID + require.NoError(parsed.Unmarshal(marshalledData)) + require.Equal(v, parsed) + }) +} + +func FuzzSubnetIDNodeIDUnmarshal(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + require := require.New(t) + + var v subnetIDNodeID + if err := v.Unmarshal(data); err != nil { + require.ErrorIs(err, errUnexpectedSubnetIDNodeIDLength) + return + } + + marshalledData := v.Marshal() + require.Equal(data, marshalledData) + }) +} + +func FuzzSubnetIDNodeIDMarshalOrdering(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + var ( + v0 subnetIDNodeID + v1 subnetIDNodeID + ) + fz := fuzzer.NewFuzzer(data) + fz.Fill(&v0, &v1) + + if v0.subnetID == v1.subnetID { + return + } + + key0 := v0.Marshal() + key1 := v1.Marshal() + require.Equal( + t, + v0.subnetID.Compare(v1.subnetID), + bytes.Compare(key0, key1), + ) + }) +} From acb3d7d102a0ad397b6b88ffc00dd882483c47e4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 17:47:39 -0400 Subject: [PATCH 008/184] Add `Deregister` to `metrics.MultiGatherer` interface (#3498) --- api/metrics/label_gatherer.go | 14 ++-- api/metrics/label_gatherer_test.go | 100 +++++++++++++++++++++++++---- api/metrics/multi_gatherer.go | 32 +++++++-- api/metrics/prefix_gatherer.go | 12 ++-- utils/slice.go | 18 ++++++ utils/slice_test.go | 50 +++++++++++++++ 6 files changed, 197 insertions(+), 29 deletions(-) create mode 100644 utils/slice.go create mode 100644 utils/slice_test.go diff --git a/api/metrics/label_gatherer.go b/api/metrics/label_gatherer.go index 3b8951a75b77..0a4488dd1fd4 100644 --- a/api/metrics/label_gatherer.go +++ b/api/metrics/label_gatherer.go @@ -45,12 +45,14 @@ func (g *labelGatherer) Register(labelValue string, gatherer prometheus.Gatherer ) } - g.names = append(g.names, labelValue) - g.gatherers = append(g.gatherers, &labeledGatherer{ - labelName: g.labelName, - labelValue: labelValue, - gatherer: gatherer, - }) + g.register( + labelValue, + &labeledGatherer{ + labelName: g.labelName, + labelValue: labelValue, + gatherer: gatherer, + }, + ) return nil } diff --git a/api/metrics/label_gatherer_test.go b/api/metrics/label_gatherer_test.go index d5f30fd6529b..59873aa56173 100644 --- a/api/metrics/label_gatherer_test.go +++ b/api/metrics/label_gatherer_test.go @@ -138,9 +138,13 @@ func TestLabelGatherer_Gather(t *testing.T) { } } -func TestLabelGatherer_Register(t *testing.T) { +func TestLabelGatherer_Registration(t *testing.T) { + const ( + firstName = "first" + secondName = "second" + ) firstLabeledGatherer := &labeledGatherer{ - labelValue: "first", + labelValue: firstName, gatherer: &testGatherer{}, } firstLabelGatherer := func() *labelGatherer { @@ -154,25 +158,37 @@ func TestLabelGatherer_Register(t *testing.T) { } } secondLabeledGatherer := &labeledGatherer{ - labelValue: "second", + labelValue: secondName, gatherer: &testGatherer{ mfs: []*dto.MetricFamily{{}}, }, } - secondLabelGatherer := &labelGatherer{ + secondLabelGatherer := func() *labelGatherer { + return &labelGatherer{ + multiGatherer: multiGatherer{ + names: []string{ + firstLabeledGatherer.labelValue, + secondLabeledGatherer.labelValue, + }, + gatherers: prometheus.Gatherers{ + firstLabeledGatherer, + secondLabeledGatherer, + }, + }, + } + } + onlySecondLabeledGatherer := &labelGatherer{ multiGatherer: multiGatherer{ names: []string{ - firstLabeledGatherer.labelValue, secondLabeledGatherer.labelValue, }, gatherers: prometheus.Gatherers{ - firstLabeledGatherer, secondLabeledGatherer, }, }, } - tests := []struct { + registerTests := []struct { name string labelGatherer *labelGatherer labelValue string @@ -183,7 +199,7 @@ func TestLabelGatherer_Register(t *testing.T) { { name: "first registration", labelGatherer: &labelGatherer{}, - labelValue: "first", + labelValue: firstName, gatherer: firstLabeledGatherer.gatherer, expectedErr: nil, expectedLabelGatherer: firstLabelGatherer(), @@ -191,21 +207,21 @@ func TestLabelGatherer_Register(t *testing.T) { { name: "second registration", labelGatherer: firstLabelGatherer(), - labelValue: "second", + labelValue: secondName, gatherer: secondLabeledGatherer.gatherer, expectedErr: nil, - expectedLabelGatherer: secondLabelGatherer, + expectedLabelGatherer: secondLabelGatherer(), }, { name: "conflicts with previous registration", labelGatherer: firstLabelGatherer(), - labelValue: "first", + labelValue: firstName, gatherer: secondLabeledGatherer.gatherer, expectedErr: errDuplicateGatherer, expectedLabelGatherer: firstLabelGatherer(), }, } - for _, test := range tests { + for _, test := range registerTests { t.Run(test.name, func(t *testing.T) { require := require.New(t) @@ -214,4 +230,64 @@ func TestLabelGatherer_Register(t *testing.T) { require.Equal(test.expectedLabelGatherer, test.labelGatherer) }) } + + deregisterTests := []struct { + name string + labelGatherer *labelGatherer + labelValue string + expectedRemoved bool + expectedLabelGatherer *labelGatherer + }{ + { + name: "remove from nothing", + labelGatherer: &labelGatherer{}, + labelValue: firstName, + expectedRemoved: false, + expectedLabelGatherer: &labelGatherer{}, + }, + { + name: "remove unknown name", + labelGatherer: firstLabelGatherer(), + labelValue: secondName, + expectedRemoved: false, + expectedLabelGatherer: firstLabelGatherer(), + }, + { + name: "remove first name", + labelGatherer: firstLabelGatherer(), + labelValue: firstName, + expectedRemoved: true, + expectedLabelGatherer: &labelGatherer{ + multiGatherer: multiGatherer{ + // We must populate with empty slices rather than nil slices + // to pass the equality check. + names: []string{}, + gatherers: prometheus.Gatherers{}, + }, + }, + }, + { + name: "remove second name", + labelGatherer: secondLabelGatherer(), + labelValue: secondName, + expectedRemoved: true, + expectedLabelGatherer: firstLabelGatherer(), + }, + { + name: "remove only first name", + labelGatherer: secondLabelGatherer(), + labelValue: firstName, + expectedRemoved: true, + expectedLabelGatherer: onlySecondLabeledGatherer, + }, + } + for _, test := range deregisterTests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + removed := test.labelGatherer.Deregister(test.labelValue) + require.Equal(test.expectedRemoved, removed) + require.Equal(test.expectedLabelGatherer, test.labelGatherer) + }) + } } diff --git a/api/metrics/multi_gatherer.go b/api/metrics/multi_gatherer.go index b2fede55643c..d5e7464a87ab 100644 --- a/api/metrics/multi_gatherer.go +++ b/api/metrics/multi_gatherer.go @@ -5,10 +5,13 @@ package metrics import ( "fmt" + "slices" "sync" "github.com/prometheus/client_golang/prometheus" + "github.com/ava-labs/avalanchego/utils" + dto "github.com/prometheus/client_model/go" ) @@ -20,13 +23,11 @@ type MultiGatherer interface { // Register adds the outputs of [gatherer] to the results of future calls to // Gather with the provided [name] added to the metrics. Register(name string, gatherer prometheus.Gatherer) error -} -// Deprecated: Use NewPrefixGatherer instead. -// -// TODO: Remove once coreth is updated. -func NewMultiGatherer() MultiGatherer { - return NewPrefixGatherer() + // Deregister removes the outputs of a gatherer with [name] from the results + // of future calls to Gather. Returns true if a gatherer with [name] was + // found. + Deregister(name string) bool } type multiGatherer struct { @@ -42,6 +43,25 @@ func (g *multiGatherer) Gather() ([]*dto.MetricFamily, error) { return g.gatherers.Gather() } +func (g *multiGatherer) register(name string, gatherer prometheus.Gatherer) { + g.names = append(g.names, name) + g.gatherers = append(g.gatherers, gatherer) +} + +func (g *multiGatherer) Deregister(name string) bool { + g.lock.Lock() + defer g.lock.Unlock() + + index := slices.Index(g.names, name) + if index == -1 { + return false + } + + g.names = utils.DeleteIndex(g.names, index) + g.gatherers = utils.DeleteIndex(g.gatherers, index) + return true +} + func MakeAndRegister(gatherer MultiGatherer, name string) (*prometheus.Registry, error) { reg := prometheus.NewRegistry() if err := gatherer.Register(name, reg); err != nil { diff --git a/api/metrics/prefix_gatherer.go b/api/metrics/prefix_gatherer.go index fae7adb26e84..02e4858346eb 100644 --- a/api/metrics/prefix_gatherer.go +++ b/api/metrics/prefix_gatherer.go @@ -45,11 +45,13 @@ func (g *prefixGatherer) Register(prefix string, gatherer prometheus.Gatherer) e } } - g.names = append(g.names, prefix) - g.gatherers = append(g.gatherers, &prefixedGatherer{ - prefix: prefix, - gatherer: gatherer, - }) + g.register( + prefix, + &prefixedGatherer{ + prefix: prefix, + gatherer: gatherer, + }, + ) return nil } diff --git a/utils/slice.go b/utils/slice.go new file mode 100644 index 000000000000..abe7a57c725a --- /dev/null +++ b/utils/slice.go @@ -0,0 +1,18 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package utils + +// DeleteIndex moves the last element in the slice to index [i] and shrinks the +// size of the slice by 1. +// +// This is an O(1) operation that allows the removal of an element from a slice +// when the order of the slice is not important. +// +// If [i] is out of bounds, this function will panic. +func DeleteIndex[S ~[]E, E any](s S, i int) S { + newSize := len(s) - 1 + s[i] = s[newSize] + s[newSize] = Zero[E]() + return s[:newSize] +} diff --git a/utils/slice_test.go b/utils/slice_test.go new file mode 100644 index 000000000000..cecc0c3ab5d1 --- /dev/null +++ b/utils/slice_test.go @@ -0,0 +1,50 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package utils + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDeleteIndex(t *testing.T) { + tests := []struct { + name string + s []int + i int + expected []int + }{ + { + name: "delete only element", + s: []int{0}, + i: 0, + expected: []int{}, + }, + { + name: "delete first element", + s: []int{0, 1}, + i: 0, + expected: []int{1}, + }, + { + name: "delete middle element", + s: []int{0, 1, 2, 3}, + i: 1, + expected: []int{0, 3, 2}, + }, + { + name: "delete last element", + s: []int{0, 1, 2, 3}, + i: 3, + expected: []int{0, 1, 2}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s := DeleteIndex(test.s, test.i) + require.Equal(t, test.expected, s) + }) + } +} From 78c1c3de58bf69e26f2764ee5b3e840413a32997 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 17:50:18 -0400 Subject: [PATCH 009/184] nit --- vms/platformvm/state/subnet_id_node_id_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/state/subnet_id_node_id_test.go b/vms/platformvm/state/subnet_id_node_id_test.go index 4ed720b95a8e..848893170e2b 100644 --- a/vms/platformvm/state/subnet_id_node_id_test.go +++ b/vms/platformvm/state/subnet_id_node_id_test.go @@ -42,7 +42,7 @@ func FuzzSubnetIDNodeIDUnmarshal(f *testing.F) { }) } -func FuzzSubnetIDNodeIDMarshalOrdering(f *testing.F) { +func FuzzSubnetIDNodeIDOrdering(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { var ( v0 subnetIDNodeID From d2137ef6d9a0b4de0d9badb9cf2e352fd75e54b9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 18:16:45 -0400 Subject: [PATCH 010/184] fix merge --- vms/platformvm/state/subnet_only_validator.go | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 9c58a60bf2e2..6d2a0f6afb9e 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -19,9 +19,6 @@ import ( safemath "github.com/ava-labs/avalanchego/utils/math" ) -// subnetIDNodeID = [subnetID] + [nodeID] -const subnetIDNodeIDEntryLength = ids.IDLen + ids.NodeIDLen - var ( _ btree.LessFunc[SubnetOnlyValidator] = SubnetOnlyValidator.Less _ utils.Sortable[SubnetOnlyValidator] = SubnetOnlyValidator{} @@ -29,8 +26,6 @@ var ( ErrMutatedSubnetOnlyValidator = errors.New("subnet only validator contains mutated constant fields") ErrConflictingSubnetOnlyValidator = errors.New("subnet only validator contains conflicting subnetID + nodeID pair") ErrDuplicateSubnetOnlyValidator = errors.New("subnet only validator contains duplicate subnetID + nodeID pair") - - errUnexpectedSubnetIDNodeIDLength = fmt.Errorf("expected subnetID+nodeID entry length %d", subnetIDNodeIDEntryLength) ) type SubnetOnlyValidators interface { @@ -175,28 +170,6 @@ func deleteSubnetOnlyValidator(db database.KeyValueDeleter, validationID ids.ID) return db.Delete(validationID[:]) } -type subnetIDNodeID struct { - subnetID ids.ID - nodeID ids.NodeID -} - -func (s *subnetIDNodeID) Marshal() []byte { - data := make([]byte, subnetIDNodeIDEntryLength) - copy(data, s.subnetID[:]) - copy(data[ids.IDLen:], s.nodeID[:]) - return data -} - -func (s *subnetIDNodeID) Unmarshal(data []byte) error { - if len(data) != subnetIDNodeIDEntryLength { - return errUnexpectedSubnetIDNodeIDLength - } - - copy(s.subnetID[:], data) - copy(s.nodeID[:], data[ids.IDLen:]) - return nil -} - type subnetOnlyValidatorsDiff struct { numAddedActive int // May be negative modifiedTotalWeight map[ids.ID]uint64 // subnetID -> totalWeight From 5845d11eed4895c72890e43a5c5a50c8ac0d9c1d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 19:36:20 -0400 Subject: [PATCH 011/184] Split writeCurrentStakers into multiple functions --- vms/platformvm/state/stakers.go | 29 +++ vms/platformvm/state/state.go | 362 ++++++++++++++++---------------- 2 files changed, 208 insertions(+), 183 deletions(-) diff --git a/vms/platformvm/state/stakers.go b/vms/platformvm/state/stakers.go index 658796855958..14e4dcf7b1ef 100644 --- a/vms/platformvm/state/stakers.go +++ b/vms/platformvm/state/stakers.go @@ -5,6 +5,7 @@ package state import ( "errors" + "fmt" "github.com/google/btree" @@ -273,6 +274,34 @@ type diffValidator struct { deletedDelegators map[ids.ID]*Staker } +func (d *diffValidator) WeightDiff() (ValidatorWeightDiff, error) { + weightDiff := ValidatorWeightDiff{ + Decrease: d.validatorStatus == deleted, + } + if d.validatorStatus != unmodified { + weightDiff.Amount = d.validator.Weight + } + + for _, staker := range d.deletedDelegators { + if err := weightDiff.Add(true, staker.Weight); err != nil { + return ValidatorWeightDiff{}, fmt.Errorf("failed to decrease node weight diff: %w", err) + } + } + + addedDelegatorIterator := iterator.FromTree(d.addedDelegators) + defer addedDelegatorIterator.Release() + + for addedDelegatorIterator.Next() { + staker := addedDelegatorIterator.Value() + + if err := weightDiff.Add(false, staker.Weight); err != nil { + return ValidatorWeightDiff{}, fmt.Errorf("failed to increase node weight diff: %w", err) + } + } + + return weightDiff, nil +} + // GetValidator attempts to fetch the validator with the given subnetID and // nodeID. // Invariant: Assumes that the validator will never be removed and then added. diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index c70547265cf1..33ae0e61e2ee 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -4,6 +4,7 @@ package state import ( + "bytes" "context" "errors" "fmt" @@ -14,6 +15,7 @@ import ( "github.com/google/btree" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" + "golang.org/x/exp/maps" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/metercacher" @@ -1792,7 +1794,9 @@ func (s *state) write(updateValidators bool, height uint64) error { return errors.Join( s.writeBlocks(), s.writeExpiry(), - s.writeCurrentStakers(updateValidators, height, codecVersion), + s.updateValidatorManager(updateValidators), + s.writeValidatorDiffs(height), + s.writeCurrentStakers(codecVersion), s.writePendingStakers(), s.WriteValidatorMetadata(s.currentValidatorList, s.currentSubnetValidatorList, codecVersion), // Must be called after writeCurrentStakers s.writeTXs(), @@ -2040,47 +2044,75 @@ func (s *state) writeExpiry() error { return nil } -func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecVersion uint16) error { +// getInheritedPublicKey returns the primary network validator's public key. +// +// Note: This function may return a nil public key and no error if the primary +// network validator does not have a public key. +func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) { + if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is present. + return vdr.validator.PublicKey, nil + } + if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is being removed. + return vdr.validator.PublicKey, nil + } + return nil, fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) +} + +// updateValidatorManager updates the validator manager with the pending +// validator set changes. +// +// This function must be called prior to writeCurrentStakers. +func (s *state) updateValidatorManager(updateValidators bool) error { + if !updateValidators { + return nil + } + for subnetID, validatorDiffs := range s.currentStakers.validatorDiffs { - // We must write the primary network stakers last because writing subnet - // validator diffs may depend on the primary network validator diffs to - // inherit the public keys. - if subnetID == constants.PrimaryNetworkID { - continue - } + // Record the change in weight and/or public key for each validator. + for nodeID, diff := range validatorDiffs { + weightDiff, err := diff.WeightDiff() + if err != nil { + return err + } - delete(s.currentStakers.validatorDiffs, subnetID) + if weightDiff.Amount == 0 { + continue // No weight change; go to the next validator. + } - err := s.writeCurrentStakersSubnetDiff( - subnetID, - validatorDiffs, - updateValidators, - height, - codecVersion, - ) - if err != nil { - return err - } - } + if weightDiff.Decrease { + if err := s.validators.RemoveWeight(subnetID, nodeID, weightDiff.Amount); err != nil { + return fmt.Errorf("failed to reduce validator weight: %w", err) + } + continue + } - if validatorDiffs, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID]; ok { - delete(s.currentStakers.validatorDiffs, constants.PrimaryNetworkID) + if diff.validatorStatus != added { + if err := s.validators.AddWeight(subnetID, nodeID, weightDiff.Amount); err != nil { + return fmt.Errorf("failed to increase validator weight: %w", err) + } + continue + } - err := s.writeCurrentStakersSubnetDiff( - constants.PrimaryNetworkID, - validatorDiffs, - updateValidators, - height, - codecVersion, - ) - if err != nil { - return err - } - } + pk, err := s.getInheritedPublicKey(nodeID) + if err != nil { + // This should never happen as there should always be a primary + // network validator corresponding to a subnet validator. + return err + } - // TODO: Move validator set management out of the state package - if !updateValidators { - return nil + err = s.validators.AddStaker( + subnetID, + nodeID, + pk, + diff.validator.TxID, + weightDiff.Amount, + ) + if err != nil { + return fmt.Errorf("failed to add validator: %w", err) + } + } } // Update the stake metrics @@ -2094,185 +2126,153 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecV return nil } -func (s *state) writeCurrentStakersSubnetDiff( - subnetID ids.ID, - validatorDiffs map[ids.NodeID]*diffValidator, - updateValidators bool, - height uint64, - codecVersion uint16, -) error { - // Select db to write to - validatorDB := s.currentSubnetValidatorList - delegatorDB := s.currentSubnetDelegatorList - if subnetID == constants.PrimaryNetworkID { - validatorDB = s.currentValidatorList - delegatorDB = s.currentDelegatorList - } - - // Record the change in weight and/or public key for each validator. - for nodeID, validatorDiff := range validatorDiffs { - var ( - staker *Staker - pk *bls.PublicKey - weightDiff = &ValidatorWeightDiff{ - Decrease: validatorDiff.validatorStatus == deleted, - } - ) - if validatorDiff.validatorStatus != unmodified { - staker = validatorDiff.validator - - pk = staker.PublicKey - // For non-primary network validators, the public key is inherited - // from the primary network. - if subnetID != constants.PrimaryNetworkID { - if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { - // The primary network validator is still present after - // writing. - pk = vdr.validator.PublicKey - } else if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { - // The primary network validator is being removed during - // writing. - pk = vdr.validator.PublicKey - } else { - // This should never happen as the primary network diffs are - // written last and subnet validator times must be a subset - // of the primary network validator times. - return fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) - } +// writeValidatorDiffs writes the validator set diff contained by the pending +// validator set changes to disk. +// +// This function must be called prior to writeCurrentStakers. +func (s *state) writeValidatorDiffs(height uint64) error { + type validatorChanges struct { + weightDiff ValidatorWeightDiff + prevPublicKey []byte + newPublicKey []byte + } + changes := make(map[subnetIDNodeID]*validatorChanges) + + // Calculate the changes to the pre-ACP-77 validator set + for subnetID, subnetDiffs := range s.currentStakers.validatorDiffs { + for nodeID, diff := range subnetDiffs { + weightDiff, err := diff.WeightDiff() + if err != nil { + return err } - weightDiff.Amount = staker.Weight - } + pk, err := s.getInheritedPublicKey(nodeID) + if err != nil { + // This should never happen as there should always be a primary + // network validator corresponding to a subnet validator. + return err + } - switch validatorDiff.validatorStatus { - case added: + change := &validatorChanges{ + weightDiff: weightDiff, + } if pk != nil { - // Record that the public key for the validator is being added. - // This means the prior value for the public key was nil. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - nil, - ) - if err != nil { - return err + switch diff.validatorStatus { + case added: + change.newPublicKey = bls.PublicKeyToUncompressedBytes(pk) + case deleted: + change.prevPublicKey = bls.PublicKeyToUncompressedBytes(pk) } } - // The validator is being added. - // - // Invariant: It's impossible for a delegator to have been rewarded - // in the same block that the validator was added. - startTime := uint64(staker.StartTime.Unix()) - metadata := &validatorMetadata{ - txID: staker.TxID, - lastUpdated: staker.StartTime, - - UpDuration: 0, - LastUpdated: startTime, - StakerStartTime: startTime, - PotentialReward: staker.PotentialReward, - PotentialDelegateeReward: 0, + subnetIDNodeID := subnetIDNodeID{ + subnetID: subnetID, + nodeID: nodeID, } + changes[subnetIDNodeID] = change + } + } - metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata) + // Write the changes to the database + for subnetIDNodeID, diff := range changes { + diffKey := marshalDiffKey(subnetIDNodeID.subnetID, height, subnetIDNodeID.nodeID) + if diff.weightDiff.Amount != 0 { + err := s.validatorWeightDiffsDB.Put( + diffKey, + marshalWeightDiff(&diff.weightDiff), + ) if err != nil { - return fmt.Errorf("failed to serialize current validator: %w", err) + return err } - - if err = validatorDB.Put(staker.TxID[:], metadataBytes); err != nil { - return fmt.Errorf("failed to write current validator to list: %w", err) + } + if !bytes.Equal(diff.prevPublicKey, diff.newPublicKey) { + err := s.validatorPublicKeyDiffsDB.Put( + diffKey, + diff.prevPublicKey, + ) + if err != nil { + return err } + } + } + return nil +} - s.validatorState.LoadValidatorMetadata(nodeID, subnetID, metadata) - case deleted: - if pk != nil { - // Record that the public key for the validator is being - // removed. This means we must record the prior value of the - // public key. - // - // Note: We store the uncompressed public key here as it is - // significantly more efficient to parse when applying diffs. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - bls.PublicKeyToUncompressedBytes(pk), - ) - if err != nil { - return err - } - } +func (s *state) writeCurrentStakers(codecVersion uint16) error { + for subnetID, validatorDiffs := range s.currentStakers.validatorDiffs { + // Select db to write to + validatorDB := s.currentSubnetValidatorList + delegatorDB := s.currentSubnetDelegatorList + if subnetID == constants.PrimaryNetworkID { + validatorDB = s.currentValidatorList + delegatorDB = s.currentDelegatorList + } - if err := validatorDB.Delete(staker.TxID[:]); err != nil { - return fmt.Errorf("failed to delete current staker: %w", err) - } + // Record the change in weight and/or public key for each validator. + for nodeID, validatorDiff := range validatorDiffs { + switch validatorDiff.validatorStatus { + case added: + staker := validatorDiff.validator - s.validatorState.DeleteValidatorMetadata(nodeID, subnetID) - } + // The validator is being added. + // + // Invariant: It's impossible for a delegator to have been rewarded + // in the same block that the validator was added. + startTime := uint64(staker.StartTime.Unix()) + metadata := &validatorMetadata{ + txID: staker.TxID, + lastUpdated: staker.StartTime, + + UpDuration: 0, + LastUpdated: startTime, + StakerStartTime: startTime, + PotentialReward: staker.PotentialReward, + PotentialDelegateeReward: 0, + } - err := writeCurrentDelegatorDiff( - delegatorDB, - weightDiff, - validatorDiff, - codecVersion, - ) - if err != nil { - return err - } + metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata) + if err != nil { + return fmt.Errorf("failed to serialize current validator: %w", err) + } - if weightDiff.Amount == 0 { - // No weight change to record; go to next validator. - continue - } + if err = validatorDB.Put(staker.TxID[:], metadataBytes); err != nil { + return fmt.Errorf("failed to write current validator to list: %w", err) + } - err = s.validatorWeightDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - marshalWeightDiff(weightDiff), - ) - if err != nil { - return err - } + s.validatorState.LoadValidatorMetadata(nodeID, subnetID, metadata) + case deleted: + if err := validatorDB.Delete(validatorDiff.validator.TxID[:]); err != nil { + return fmt.Errorf("failed to delete current staker: %w", err) + } - // TODO: Move the validator set management out of the state package - if !updateValidators { - continue - } + s.validatorState.DeleteValidatorMetadata(nodeID, subnetID) + } - if weightDiff.Decrease { - err = s.validators.RemoveWeight(subnetID, nodeID, weightDiff.Amount) - } else { - if validatorDiff.validatorStatus == added { - err = s.validators.AddStaker( - subnetID, - nodeID, - pk, - staker.TxID, - weightDiff.Amount, - ) - } else { - err = s.validators.AddWeight(subnetID, nodeID, weightDiff.Amount) + err := writeCurrentDelegatorDiff( + delegatorDB, + validatorDiff, + codecVersion, + ) + if err != nil { + return err } } - if err != nil { - return fmt.Errorf("failed to update validator weight: %w", err) - } } + maps.Clear(s.currentStakers.validatorDiffs) return nil } func writeCurrentDelegatorDiff( currentDelegatorList linkeddb.LinkedDB, - weightDiff *ValidatorWeightDiff, validatorDiff *diffValidator, codecVersion uint16, ) error { addedDelegatorIterator := iterator.FromTree(validatorDiff.addedDelegators) defer addedDelegatorIterator.Release() + for addedDelegatorIterator.Next() { staker := addedDelegatorIterator.Value() - if err := weightDiff.Add(false, staker.Weight); err != nil { - return fmt.Errorf("failed to increase node weight diff: %w", err) - } - metadata := &delegatorMetadata{ txID: staker.TxID, PotentialReward: staker.PotentialReward, @@ -2284,10 +2284,6 @@ func writeCurrentDelegatorDiff( } for _, staker := range validatorDiff.deletedDelegators { - if err := weightDiff.Add(true, staker.Weight); err != nil { - return fmt.Errorf("failed to decrease node weight diff: %w", err) - } - if err := currentDelegatorList.Delete(staker.TxID[:]); err != nil { return fmt.Errorf("failed to delete current staker: %w", err) } From d0d1602264c040c6202659454fc299f68d491f55 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 20:09:45 -0400 Subject: [PATCH 012/184] reduce diff --- vms/platformvm/state/state.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index d4353aafb9af..4e34a6bd1cba 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2418,7 +2418,7 @@ func (s *state) writeValidatorDiffs(height uint64) error { } changes := make(map[subnetIDNodeID]*validatorChanges, len(s.sovDiff.modified)) - // Perform pre-ACP-77 validator set changes: + // Calculate the changes to the pre-ACP-77 validator set for subnetID, subnetDiffs := range s.currentStakers.validatorDiffs { for nodeID, diff := range subnetDiffs { weightDiff, err := diff.WeightDiff() @@ -2428,6 +2428,8 @@ func (s *state) writeValidatorDiffs(height uint64) error { pk, err := s.getInheritedPublicKey(nodeID) if err != nil { + // This should never happen as there should always be a primary + // network validator corresponding to a subnet validator. return err } From d397375ea8a4cc8b5312cae64c48cdadecaacddf Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 20:20:08 -0400 Subject: [PATCH 013/184] reduce diff --- vms/platformvm/state/state.go | 85 ++++++++++++----------------------- 1 file changed, 29 insertions(+), 56 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 4e34a6bd1cba..841fe972278b 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2720,49 +2720,18 @@ func (s *state) writeSubnetOnlyValidators() error { // The next loops shouldn't consider this change. delete(sovChanges, validationID) - priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - if err == database.ErrNotFound { - // Deleting a non-existent validator is a noop. This can happen if - // the validator was added and then immediately removed. - continue - } - if err != nil { - return err - } - subnetIDNodeID := subnetIDNodeID{ subnetID: sov.SubnetID, nodeID: sov.NodeID, } subnetIDNodeIDKey := subnetIDNodeID.Marshal() - if err := s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey); err != nil { - return err - } - - if priorSOV.isActive() { - delete(s.activeSOVLookup, validationID) - s.activeSOVs.Delete(priorSOV) - err = deleteSubnetOnlyValidator(s.activeDB, validationID) - } else { - err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) - } - if err != nil { - return err - } - } - - // Perform modifications: - for validationID, sov := range sovChanges { - priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - if err == database.ErrNotFound { - // New additions are handled in the next loop. - continue - } + err := s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey) if err != nil { return err } - if priorSOV.isActive() { + priorSOV, wasActive := s.activeSOVLookup[validationID] + if wasActive { delete(s.activeSOVLookup, validationID) s.activeSOVs.Delete(priorSOV) err = deleteSubnetOnlyValidator(s.activeDB, validationID) @@ -2772,34 +2741,38 @@ func (s *state) writeSubnetOnlyValidators() error { if err != nil { return err } - - if sov.isActive() { - s.activeSOVLookup[validationID] = sov - s.activeSOVs.ReplaceOrInsert(sov) - err = putSubnetOnlyValidator(s.activeDB, sov) - } else { - err = putSubnetOnlyValidator(s.inactiveDB, sov) - } - if err != nil { - return err - } - - // The next loop shouldn't consider this change. - delete(sovChanges, validationID) } - // Perform additions: + // Perform modifications and additions: for validationID, sov := range sovChanges { - subnetIDNodeID := subnetIDNodeID{ - subnetID: sov.SubnetID, - nodeID: sov.NodeID, - } - subnetIDNodeIDKey := subnetIDNodeID.Marshal() - if err := s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]); err != nil { + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) + switch err { + case nil: + // This is modifying an existing validator + if priorSOV.isActive() { + delete(s.activeSOVLookup, validationID) + s.activeSOVs.Delete(priorSOV) + err = deleteSubnetOnlyValidator(s.activeDB, validationID) + } else { + err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) + } + if err != nil { + return err + } + case database.ErrNotFound: + // This is a new validator + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + subnetIDNodeIDKey := subnetIDNodeID.Marshal() + if err := s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]); err != nil { + return err + } + default: return err } - var err error if sov.isActive() { s.activeSOVLookup[validationID] = sov s.activeSOVs.ReplaceOrInsert(sov) From 5f8a09ca115f8b363e1d5a9d9506e3150c10f270 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 20:25:42 -0400 Subject: [PATCH 014/184] reduce diff --- vms/platformvm/state/state.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 841fe972278b..004d273a5edd 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2699,7 +2699,6 @@ func writePendingDiff( return nil } -// TODO: Add caching func (s *state) writeSubnetOnlyValidators() error { // Write modified weights: for subnetID, weight := range s.sovDiff.modifiedTotalWeight { @@ -2756,9 +2755,6 @@ func (s *state) writeSubnetOnlyValidators() error { } else { err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) } - if err != nil { - return err - } case database.ErrNotFound: // This is a new validator subnetIDNodeID := subnetIDNodeID{ @@ -2766,10 +2762,9 @@ func (s *state) writeSubnetOnlyValidators() error { nodeID: sov.NodeID, } subnetIDNodeIDKey := subnetIDNodeID.Marshal() - if err := s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]); err != nil { - return err - } - default: + err = s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]) + } + if err != nil { return err } From 3e9dc01fd1327f151149ae749886d7b8a299db8a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 20:49:51 -0400 Subject: [PATCH 015/184] reduce diff --- vms/platformvm/state/state.go | 6 ++++-- vms/platformvm/state/subnet_only_validator.go | 20 +++---------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 004d273a5edd..07e99d505c9d 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2228,7 +2228,8 @@ func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) // updateValidatorManager updates the validator manager with the pending // validator set changes. // -// This function must be called prior to writeCurrentStakers. +// This function must be called prior to writeCurrentStakers and +// writeSubnetOnlyValidators. func (s *state) updateValidatorManager(updateValidators bool) error { if !updateValidators { return nil @@ -2409,7 +2410,8 @@ func (s *state) updateValidatorManager(updateValidators bool) error { // writeValidatorDiffs writes the validator set diff contained by the pending // validator set changes to disk. // -// This function must be called prior to writeCurrentStakers. +// This function must be called prior to writeCurrentStakers and +// writeSubnetOnlyValidators. func (s *state) writeValidatorDiffs(height uint64) error { type validatorChanges struct { weightDiff ValidatorWeightDiff diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 6d2a0f6afb9e..cb40856efc59 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -243,26 +243,17 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne return err } - switch { - case prevWeight < sov.Weight: + if prevWeight != sov.Weight { weight, err := state.WeightOfSubnetOnlyValidators(sov.SubnetID) if err != nil { return err } - weight, err = safemath.Add(weight, sov.Weight-prevWeight) + weight, err = safemath.Sub(weight, prevWeight) if err != nil { return err } - - d.modifiedTotalWeight[sov.SubnetID] = weight - case prevWeight > sov.Weight: - weight, err := state.WeightOfSubnetOnlyValidators(sov.SubnetID) - if err != nil { - return err - } - - weight, err = safemath.Sub(weight, prevWeight-sov.Weight) + weight, err = safemath.Add(weight, sov.Weight) if err != nil { return err } @@ -278,11 +269,6 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne } if prevSOV, ok := d.modified[sov.ValidationID]; ok { - prevSubnetIDNodeID := subnetIDNodeID{ - subnetID: prevSOV.SubnetID, - nodeID: prevSOV.NodeID, - } - d.modifiedHasNodeIDs[prevSubnetIDNodeID] = false d.active.Delete(prevSOV) } d.modified[sov.ValidationID] = sov From da3a7267c73d69d052db7e2e08005f06817d9ad5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 20:52:54 -0400 Subject: [PATCH 016/184] reduce diff --- vms/platformvm/state/state.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 07e99d505c9d..e0e39a0f14a9 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -773,11 +773,7 @@ func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { } // TODO: Add caching - weight, err := database.GetUInt64(s.weightsDB, subnetID[:]) - if err == database.ErrNotFound { - return 0, nil - } - return weight, err + return database.WithDefault(database.GetUInt64, s.weightsDB, subnetID[:], 0) } func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { From 8483cedaaa4397d1c737d95326ee9cb143990e77 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 26 Oct 2024 21:05:18 -0400 Subject: [PATCH 017/184] cleanup --- vms/platformvm/state/diff.go | 6 +++--- vms/platformvm/state/state.go | 12 +++++------ vms/platformvm/state/subnet_only_validator.go | 21 ++++++++++++------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/vms/platformvm/state/diff.go b/vms/platformvm/state/diff.go index 47d98863106c..4b8922f74492 100644 --- a/vms/platformvm/state/diff.go +++ b/vms/platformvm/state/diff.go @@ -231,7 +231,7 @@ func (d *diff) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { func (d *diff) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { if sov, modified := d.sovDiff.modified[validationID]; modified { - if sov.Weight == 0 { + if sov.isDeleted() { return SubnetOnlyValidator{}, database.ErrNotFound } return sov, nil @@ -577,7 +577,7 @@ func (d *diff) Apply(baseState Chain) error { // a single diff can't get reordered into the addition happening first; // which would return an error. for _, sov := range d.sovDiff.modified { - if sov.Weight != 0 { + if !sov.isDeleted() { continue } if err := baseState.PutSubnetOnlyValidator(sov); err != nil { @@ -585,7 +585,7 @@ func (d *diff) Apply(baseState Chain) error { } } for _, sov := range d.sovDiff.modified { - if sov.Weight == 0 { + if sov.isDeleted() { continue } if err := baseState.PutSubnetOnlyValidator(sov); err != nil { diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index e0e39a0f14a9..421c2355e5a3 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -778,7 +778,7 @@ func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { if sov, modified := s.sovDiff.modified[validationID]; modified { - if sov.Weight == 0 { + if sov.isDeleted() { return SubnetOnlyValidator{}, database.ErrNotFound } return sov, nil @@ -2283,7 +2283,7 @@ func (s *state) updateValidatorManager(updateValidators bool) error { sovChangesApplied set.Set[ids.ID] ) for validationID, sov := range sovChanges { - if sov.Weight != 0 { + if !sov.isDeleted() { // Additions and modifications are handled in the next loops. continue } @@ -2327,14 +2327,14 @@ func (s *state) updateValidatorManager(updateValidators bool) error { sovChangesApplied.Add(validationID) switch { - case !priorSOV.isActive() && sov.isActive(): + case priorSOV.isInactive() && sov.isActive(): // This validator is being activated. pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) err = errors.Join( s.validators.RemoveWeight(sov.SubnetID, ids.EmptyNodeID, priorSOV.Weight), s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight), ) - case priorSOV.isActive() && !sov.isActive(): + case priorSOV.isActive() && sov.isInactive(): // This validator is being deactivated. inactiveWeight := s.validators.GetWeight(sov.SubnetID, ids.EmptyNodeID) if inactiveWeight == 0 { @@ -2484,7 +2484,7 @@ func (s *state) writeValidatorDiffs(height uint64) error { // Perform SoV additions: for _, sov := range s.sovDiff.modified { // If the validator is being removed, we shouldn't work to re-add it. - if sov.Weight == 0 { + if sov.isDeleted() { continue } @@ -2709,7 +2709,7 @@ func (s *state) writeSubnetOnlyValidators() error { sovChanges := s.sovDiff.modified // Perform deletions: for validationID, sov := range sovChanges { - if sov.Weight != 0 { + if !sov.isDeleted() { // Additions and modifications are handled in the next loops. continue } diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index cb40856efc59..932aa1a20cb2 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -139,10 +139,18 @@ func (v SubnetOnlyValidator) constantsAreUnmodified(o SubnetOnlyValidator) bool v.StartTime == o.StartTime } +func (v SubnetOnlyValidator) isDeleted() bool { + return v.Weight == 0 +} + func (v SubnetOnlyValidator) isActive() bool { return v.Weight != 0 && v.EndAccumulatedFee != 0 } +func (v SubnetOnlyValidator) isInactive() bool { + return v.Weight != 0 && v.EndAccumulatedFee == 0 +} + func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (SubnetOnlyValidator, error) { bytes, err := db.Get(validationID[:]) if err != nil { @@ -211,7 +219,7 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne var ( prevWeight uint64 prevActive bool - newActive = sov.Weight != 0 && sov.EndAccumulatedFee != 0 + newActive = sov.isActive() ) switch priorSOV, err := state.GetSubnetOnlyValidator(sov.ValidationID); err { case nil: @@ -220,7 +228,7 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne } prevWeight = priorSOV.Weight - prevActive = priorSOV.EndAccumulatedFee != 0 + prevActive = priorSOV.isActive() case database.ErrNotFound: // Verify that there is not a legacy subnet validator with the same // subnetID+nodeID as this L1 validator. @@ -277,12 +285,9 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne subnetID: sov.SubnetID, nodeID: sov.NodeID, } - isDeleted := sov.Weight == 0 - d.modifiedHasNodeIDs[subnetIDNodeID] = !isDeleted - if isDeleted || sov.EndAccumulatedFee == 0 { - // Validator is being deleted or is inactive - return nil + d.modifiedHasNodeIDs[subnetIDNodeID] = !sov.isDeleted() + if sov.isActive() { + d.active.ReplaceOrInsert(sov) } - d.active.ReplaceOrInsert(sov) return nil } From 0507ce702057b92bbf0b8b14680fd79c02b43fd4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 27 Oct 2024 14:36:10 -0400 Subject: [PATCH 018/184] nit --- vms/platformvm/state/state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 33ae0e61e2ee..ce4869792c61 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2054,7 +2054,7 @@ func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) return vdr.validator.PublicKey, nil } if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { - // The primary network validator is being removed. + // The primary network validator is being modified. return vdr.validator.PublicKey, nil } return nil, fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) From 8bbeee650f5d3e9834ae9f4b8bb809b52b885ae9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 27 Oct 2024 15:49:29 -0400 Subject: [PATCH 019/184] Add comment --- vms/platformvm/state/subnet_only_validator.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 932aa1a20cb2..41014793c2ab 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -195,6 +195,8 @@ func newSubnetOnlyValidatorsDiff() *subnetOnlyValidatorsDiff { } } +// getActiveSubnetOnlyValidatorsIterator takes in the parent iterator, removes +// all modified validators, and then adds all modified active validators. func (d *subnetOnlyValidatorsDiff) getActiveSubnetOnlyValidatorsIterator(parentIterator iterator.Iterator[SubnetOnlyValidator]) iterator.Iterator[SubnetOnlyValidator] { return iterator.Merge( SubnetOnlyValidator.Less, From 08dd776bce7b5a727636f71f5f7f163662a4f023 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 27 Oct 2024 16:00:30 -0400 Subject: [PATCH 020/184] comment --- vms/platformvm/state/state.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index abedb5343cdb..f4ed7f26d7e5 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -787,6 +787,9 @@ func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator return s.getPersistedSubnetOnlyValidator(validationID) } +// getPersistedSubnetOnlyValidator returns the currently persisted +// SubnetOnlyValidator with the given validationID. It is guaranteed that any +// returned validator is either active or inactive. func (s *state) getPersistedSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { if sov, ok := s.activeSOVLookup[validationID]; ok { return sov, nil @@ -2733,6 +2736,9 @@ func (s *state) writeSubnetOnlyValidators() error { s.activeSOVs.Delete(priorSOV) err = deleteSubnetOnlyValidator(s.activeDB, validationID) } else { + // It is technically possible for the validator not to exist on disk + // here, but that's fine as deleting an entry that doesn't exist is + // a noop. err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) } if err != nil { From 255b0bf92c322f41536801d4ac8f41034719ef12 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 27 Oct 2024 16:15:45 -0400 Subject: [PATCH 021/184] nit --- vms/platformvm/state/subnet_only_validator.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 41014793c2ab..ddaa5eb5a50a 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -14,9 +14,8 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/iterator" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/platformvm/block" - - safemath "github.com/ava-labs/avalanchego/utils/math" ) var ( @@ -259,11 +258,11 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne return err } - weight, err = safemath.Sub(weight, prevWeight) + weight, err = math.Sub(weight, prevWeight) if err != nil { return err } - weight, err = safemath.Add(weight, sov.Weight) + weight, err = math.Add(weight, sov.Weight) if err != nil { return err } From 3bc547d7741946ab731e6d29055c2e31deebe0ab Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 27 Oct 2024 18:19:52 -0400 Subject: [PATCH 022/184] reduce diff --- vms/platformvm/validators/manager.go | 94 +++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index 2e67faa63464..142db3e7635c 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -200,7 +200,17 @@ func (m *manager) GetValidatorSet( // get the start time to track metrics startTime := m.clk.Time() - validatorSet, currentHeight, err := m.makeValidatorSet(ctx, targetHeight, subnetID) + + var ( + validatorSet map[ids.NodeID]*validators.GetValidatorOutput + currentHeight uint64 + err error + ) + if subnetID == constants.PrimaryNetworkID { + validatorSet, currentHeight, err = m.makePrimaryNetworkValidatorSet(ctx, targetHeight) + } else { + validatorSet, currentHeight, err = m.makeSubnetValidatorSet(ctx, targetHeight, subnetID) + } if err != nil { return nil, err } @@ -233,12 +243,65 @@ func (m *manager) getValidatorSetCache(subnetID ids.ID) cache.Cacher[uint64, map return validatorSetsCache } -func (m *manager) makeValidatorSet( +func (m *manager) makePrimaryNetworkValidatorSet( + ctx context.Context, + targetHeight uint64, +) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { + validatorSet, currentHeight, err := m.getCurrentPrimaryValidatorSet(ctx) + if err != nil { + return nil, 0, err + } + if currentHeight < targetHeight { + return nil, 0, fmt.Errorf("%w with SubnetID = %s: current P-chain height (%d) < requested P-Chain height (%d)", + errUnfinalizedHeight, + constants.PrimaryNetworkID, + currentHeight, + targetHeight, + ) + } + + // Rebuild primary network validators at [targetHeight] + // + // Note: Since we are attempting to generate the validator set at + // [targetHeight], we want to apply the diffs from + // (targetHeight, currentHeight]. Because the state interface is implemented + // to be inclusive, we apply diffs in [targetHeight + 1, currentHeight]. + lastDiffHeight := targetHeight + 1 + err = m.state.ApplyValidatorWeightDiffs( + ctx, + validatorSet, + currentHeight, + lastDiffHeight, + constants.PrimaryNetworkID, + ) + if err != nil { + return nil, 0, err + } + + err = m.state.ApplyValidatorPublicKeyDiffs( + ctx, + validatorSet, + currentHeight, + lastDiffHeight, + constants.PrimaryNetworkID, + ) + return validatorSet, currentHeight, err +} + +func (m *manager) getCurrentPrimaryValidatorSet( + ctx context.Context, +) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { + primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID) + currentHeight, err := m.getCurrentHeight(ctx) + return primaryMap, currentHeight, err +} + +func (m *manager) makeSubnetValidatorSet( ctx context.Context, targetHeight uint64, subnetID ids.ID, ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { - subnetValidatorSet, currentHeight, err := m.getCurrentValidatorSet(ctx, subnetID) + subnetValidatorSet, primaryValidatorSet, currentHeight, err := m.getCurrentValidatorSets(ctx, subnetID) if err != nil { return nil, 0, err } @@ -269,23 +332,40 @@ func (m *manager) makeValidatorSet( return nil, 0, err } + // Update the subnet validator set to include the public keys at + // [currentHeight]. When we apply the public key diffs, we will convert + // these keys to represent the public keys at [targetHeight]. If the subnet + // validator is not currently a primary network validator, it doesn't have a + // key at [currentHeight]. + for nodeID, vdr := range subnetValidatorSet { + if primaryVdr, ok := primaryValidatorSet[nodeID]; ok { + vdr.PublicKey = primaryVdr.PublicKey + } else { + vdr.PublicKey = nil + } + } + err = m.state.ApplyValidatorPublicKeyDiffs( ctx, subnetValidatorSet, currentHeight, lastDiffHeight, - subnetID, + // TODO: Etna introduces L1s whose validators specify their own public + // keys, rather than inheriting them from the primary network. + // Therefore, this will need to use the subnetID after Etna. + constants.PrimaryNetworkID, ) return subnetValidatorSet, currentHeight, err } -func (m *manager) getCurrentValidatorSet( +func (m *manager) getCurrentValidatorSets( ctx context.Context, subnetID ids.ID, -) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { +) (map[ids.NodeID]*validators.GetValidatorOutput, map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { subnetMap := m.cfg.Validators.GetMap(subnetID) + primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID) currentHeight, err := m.getCurrentHeight(ctx) - return subnetMap, currentHeight, err + return subnetMap, primaryMap, currentHeight, err } func (m *manager) GetSubnetID(_ context.Context, chainID ids.ID) (ids.ID, error) { From 9f0ff018cc1ca2879b59bb3b62c54fdcc1957e9d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 28 Oct 2024 14:27:53 -0400 Subject: [PATCH 023/184] ACP-77: Add subnetIDNodeID struct (#3499) --- vms/platformvm/state/state_test.go | 4 -- vms/platformvm/state/subnet_id_node_id.go | 37 +++++++++++ .../state/subnet_id_node_id_test.go | 66 +++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 vms/platformvm/state/subnet_id_node_id.go create mode 100644 vms/platformvm/state/subnet_id_node_id_test.go diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index b965af1531eb..85df176ce42a 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -944,10 +944,6 @@ func TestState_ApplyValidatorDiffs(t *testing.T) { d, err := NewDiffOn(state) require.NoError(err) - type subnetIDNodeID struct { - subnetID ids.ID - nodeID ids.NodeID - } var expectedValidators set.Set[subnetIDNodeID] for _, added := range diff.addedValidators { require.NoError(d.PutCurrentValidator(&added)) diff --git a/vms/platformvm/state/subnet_id_node_id.go b/vms/platformvm/state/subnet_id_node_id.go new file mode 100644 index 000000000000..208c1cf8f447 --- /dev/null +++ b/vms/platformvm/state/subnet_id_node_id.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" +) + +// subnetIDNodeID = [subnetID] + [nodeID] +const subnetIDNodeIDEntryLength = ids.IDLen + ids.NodeIDLen + +var errUnexpectedSubnetIDNodeIDLength = fmt.Errorf("expected subnetID+nodeID entry length %d", subnetIDNodeIDEntryLength) + +type subnetIDNodeID struct { + subnetID ids.ID + nodeID ids.NodeID +} + +func (s *subnetIDNodeID) Marshal() []byte { + data := make([]byte, subnetIDNodeIDEntryLength) + copy(data, s.subnetID[:]) + copy(data[ids.IDLen:], s.nodeID[:]) + return data +} + +func (s *subnetIDNodeID) Unmarshal(data []byte) error { + if len(data) != subnetIDNodeIDEntryLength { + return errUnexpectedSubnetIDNodeIDLength + } + + copy(s.subnetID[:], data) + copy(s.nodeID[:], data[ids.IDLen:]) + return nil +} diff --git a/vms/platformvm/state/subnet_id_node_id_test.go b/vms/platformvm/state/subnet_id_node_id_test.go new file mode 100644 index 000000000000..848893170e2b --- /dev/null +++ b/vms/platformvm/state/subnet_id_node_id_test.go @@ -0,0 +1,66 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + "github.com/thepudds/fzgen/fuzzer" +) + +func FuzzSubnetIDNodeIDMarshal(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + require := require.New(t) + + var v subnetIDNodeID + fz := fuzzer.NewFuzzer(data) + fz.Fill(&v) + + marshalledData := v.Marshal() + + var parsed subnetIDNodeID + require.NoError(parsed.Unmarshal(marshalledData)) + require.Equal(v, parsed) + }) +} + +func FuzzSubnetIDNodeIDUnmarshal(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + require := require.New(t) + + var v subnetIDNodeID + if err := v.Unmarshal(data); err != nil { + require.ErrorIs(err, errUnexpectedSubnetIDNodeIDLength) + return + } + + marshalledData := v.Marshal() + require.Equal(data, marshalledData) + }) +} + +func FuzzSubnetIDNodeIDOrdering(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + var ( + v0 subnetIDNodeID + v1 subnetIDNodeID + ) + fz := fuzzer.NewFuzzer(data) + fz.Fill(&v0, &v1) + + if v0.subnetID == v1.subnetID { + return + } + + key0 := v0.Marshal() + key1 := v1.Marshal() + require.Equal( + t, + v0.subnetID.Compare(v1.subnetID), + bytes.Compare(key0, key1), + ) + }) +} From f500deec84c2d8db0426c6e1ddd4d783e459f078 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 12:56:16 -0400 Subject: [PATCH 024/184] Use subnet public key diffs after Etna is activated (#3502) --- vms/platformvm/state/mock_state.go | 15 +++ vms/platformvm/state/state.go | 19 ++- vms/platformvm/state/state_interface_test.go | 68 ++++++++++ vms/platformvm/validators/manager.go | 39 +++--- vms/platformvm/validators/manager_test.go | 127 +++++++++++++++++++ 5 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 vms/platformvm/state/state_interface_test.go create mode 100644 vms/platformvm/validators/manager_test.go diff --git a/vms/platformvm/state/mock_state.go b/vms/platformvm/state/mock_state.go index 5eab1b07ee99..5f88902cf9eb 100644 --- a/vms/platformvm/state/mock_state.go +++ b/vms/platformvm/state/mock_state.go @@ -424,6 +424,21 @@ func (mr *MockStateMockRecorder) GetDelegateeReward(subnetID, nodeID any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDelegateeReward", reflect.TypeOf((*MockState)(nil).GetDelegateeReward), subnetID, nodeID) } +// GetEtnaHeight mocks base method. +func (m *MockState) GetEtnaHeight() (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEtnaHeight") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEtnaHeight indicates an expected call of GetEtnaHeight. +func (mr *MockStateMockRecorder) GetEtnaHeight() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEtnaHeight", reflect.TypeOf((*MockState)(nil).GetEtnaHeight)) +} + // GetExpiryIterator mocks base method. func (m *MockState) GetExpiryIterator() (iterator.Iterator[ExpiryEntry], error) { m.ctrl.T.Helper() diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index c70547265cf1..928d069476f3 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -86,6 +86,7 @@ var ( ExpiryReplayProtectionPrefix = []byte("expiryReplayProtection") SingletonPrefix = []byte("singleton") + EtnaHeightKey = []byte("etna height") TimestampKey = []byte("timestamp") FeeStateKey = []byte("fee state") SoVExcessKey = []byte("sov excess") @@ -145,6 +146,9 @@ type State interface { uptime.State avax.UTXOReader + // TODO: Remove after Etna is activated + GetEtnaHeight() (uint64, error) + GetLastAccepted() ids.ID SetLastAccepted(blkID ids.ID) @@ -293,6 +297,7 @@ type stateBlk struct { * '-. singletons * |-- initializedKey -> nil * |-- blocksReindexedKey -> nil + * |-- etnaHeightKey -> height * |-- timestampKey -> timestamp * |-- feeStateKey -> feeState * |-- sovExcessKey -> sovExcess @@ -1083,6 +1088,10 @@ func (s *state) GetStartTime(nodeID ids.NodeID) (time.Time, error) { return staker.StartTime, nil } +func (s *state) GetEtnaHeight() (uint64, error) { + return database.GetUInt64(s.singletonDB, EtnaHeightKey) +} + func (s *state) GetTimestamp() time.Time { return s.timestamp } @@ -1804,7 +1813,7 @@ func (s *state) write(updateValidators bool, height uint64) error { s.writeTransformedSubnets(), s.writeSubnetSupplies(), s.writeChains(), - s.writeMetadata(), + s.writeMetadata(height), ) } @@ -2516,7 +2525,13 @@ func (s *state) writeChains() error { return nil } -func (s *state) writeMetadata() error { +func (s *state) writeMetadata(height uint64) error { + if !s.upgrades.IsEtnaActivated(s.persistedTimestamp) && s.upgrades.IsEtnaActivated(s.timestamp) { + if err := database.PutUInt64(s.singletonDB, EtnaHeightKey, height); err != nil { + return fmt.Errorf("failed to write etna height: %w", err) + } + } + if !s.persistedTimestamp.Equal(s.timestamp) { if err := database.PutTimestamp(s.singletonDB, TimestampKey, s.timestamp); err != nil { return fmt.Errorf("failed to write timestamp: %w", err) diff --git a/vms/platformvm/state/state_interface_test.go b/vms/platformvm/state/state_interface_test.go new file mode 100644 index 000000000000..f0909c67aa7d --- /dev/null +++ b/vms/platformvm/state/state_interface_test.go @@ -0,0 +1,68 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" + "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" +) + +func TestState_GetEtnaHeight_Activation(t *testing.T) { + require := require.New(t) + + upgrades := upgradetest.GetConfig(upgradetest.Durango) + upgrades.EtnaTime = genesistest.DefaultValidatorStartTime.Add(2 * time.Second) + state := statetest.New(t, statetest.Config{ + Upgrades: upgrades, + }) + + // Etna isn't initially active + _, err := state.GetEtnaHeight() + require.ErrorIs(err, database.ErrNotFound) + + // Etna still isn't active after advancing the time + state.SetHeight(1) + state.SetTimestamp(genesistest.DefaultValidatorStartTime.Add(time.Second)) + require.NoError(state.Commit()) + + _, err = state.GetEtnaHeight() + require.ErrorIs(err, database.ErrNotFound) + + // Etna was just activated + const expectedEtnaHeight uint64 = 2 + state.SetHeight(expectedEtnaHeight) + state.SetTimestamp(genesistest.DefaultValidatorStartTime.Add(2 * time.Second)) + require.NoError(state.Commit()) + + etnaHeight, err := state.GetEtnaHeight() + require.NoError(err) + require.Equal(expectedEtnaHeight, etnaHeight) + + // Etna was previously activated + state.SetHeight(3) + state.SetTimestamp(genesistest.DefaultValidatorStartTime.Add(3 * time.Second)) + require.NoError(state.Commit()) + + etnaHeight, err = state.GetEtnaHeight() + require.NoError(err) + require.Equal(expectedEtnaHeight, etnaHeight) +} + +func TestState_GetEtnaHeight_InitiallyActive(t *testing.T) { + require := require.New(t) + + state := statetest.New(t, statetest.Config{}) + + // Etna is initially active + etnaHeight, err := state.GetEtnaHeight() + require.NoError(err) + require.Zero(etnaHeight) +} diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index 142db3e7635c..23ed1ccd8c65 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -10,6 +10,7 @@ import ( "time" "github.com/ava-labs/avalanchego/cache" + "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils/constants" @@ -49,6 +50,9 @@ type Manager interface { type State interface { GetTx(txID ids.ID) (*txs.Tx, status.Status, error) + // TODO: Remove after Etna is activated + GetEtnaHeight() (uint64, error) + GetLastAccepted() ids.ID GetStatelessBlock(blockID ids.ID) (block.Block, error) @@ -198,16 +202,20 @@ func (m *manager) GetValidatorSet( return validatorSet, nil } + etnaHeight, err := m.state.GetEtnaHeight() + if err != nil && err != database.ErrNotFound { + return nil, err + } + // get the start time to track metrics startTime := m.clk.Time() var ( validatorSet map[ids.NodeID]*validators.GetValidatorOutput currentHeight uint64 - err error ) - if subnetID == constants.PrimaryNetworkID { - validatorSet, currentHeight, err = m.makePrimaryNetworkValidatorSet(ctx, targetHeight) + if subnetID == constants.PrimaryNetworkID || (err == nil && targetHeight >= etnaHeight) { + validatorSet, currentHeight, err = m.makeValidatorSet(ctx, targetHeight, subnetID) } else { validatorSet, currentHeight, err = m.makeSubnetValidatorSet(ctx, targetHeight, subnetID) } @@ -243,24 +251,25 @@ func (m *manager) getValidatorSetCache(subnetID ids.ID) cache.Cacher[uint64, map return validatorSetsCache } -func (m *manager) makePrimaryNetworkValidatorSet( +func (m *manager) makeValidatorSet( ctx context.Context, targetHeight uint64, + subnetID ids.ID, ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { - validatorSet, currentHeight, err := m.getCurrentPrimaryValidatorSet(ctx) + validatorSet, currentHeight, err := m.getCurrentValidatorSet(ctx, subnetID) if err != nil { return nil, 0, err } if currentHeight < targetHeight { return nil, 0, fmt.Errorf("%w with SubnetID = %s: current P-chain height (%d) < requested P-Chain height (%d)", errUnfinalizedHeight, - constants.PrimaryNetworkID, + subnetID, currentHeight, targetHeight, ) } - // Rebuild primary network validators at [targetHeight] + // Rebuild subnet validators at [targetHeight] // // Note: Since we are attempting to generate the validator set at // [targetHeight], we want to apply the diffs from @@ -272,7 +281,7 @@ func (m *manager) makePrimaryNetworkValidatorSet( validatorSet, currentHeight, lastDiffHeight, - constants.PrimaryNetworkID, + subnetID, ) if err != nil { return nil, 0, err @@ -283,19 +292,22 @@ func (m *manager) makePrimaryNetworkValidatorSet( validatorSet, currentHeight, lastDiffHeight, - constants.PrimaryNetworkID, + subnetID, ) return validatorSet, currentHeight, err } -func (m *manager) getCurrentPrimaryValidatorSet( +func (m *manager) getCurrentValidatorSet( ctx context.Context, + subnetID ids.ID, ) (map[ids.NodeID]*validators.GetValidatorOutput, uint64, error) { - primaryMap := m.cfg.Validators.GetMap(constants.PrimaryNetworkID) + subnetMap := m.cfg.Validators.GetMap(subnetID) currentHeight, err := m.getCurrentHeight(ctx) - return primaryMap, currentHeight, err + return subnetMap, currentHeight, err } +// TODO: Once Etna has been activated, remove this function and use +// makeValidatorSet for all validator set lookups. func (m *manager) makeSubnetValidatorSet( ctx context.Context, targetHeight uint64, @@ -350,9 +362,6 @@ func (m *manager) makeSubnetValidatorSet( subnetValidatorSet, currentHeight, lastDiffHeight, - // TODO: Etna introduces L1s whose validators specify their own public - // keys, rather than inheriting them from the primary network. - // Therefore, this will need to use the subnetID after Etna. constants.PrimaryNetworkID, ) return subnetValidatorSet, currentHeight, err diff --git a/vms/platformvm/validators/manager_test.go b/vms/platformvm/validators/manager_test.go new file mode 100644 index 000000000000..525d7b3646a4 --- /dev/null +++ b/vms/platformvm/validators/manager_test.go @@ -0,0 +1,127 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package validators_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/config" + "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" + "github.com/ava-labs/avalanchego/vms/platformvm/metrics" + "github.com/ava-labs/avalanchego/vms/platformvm/state" + "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" + + . "github.com/ava-labs/avalanchego/vms/platformvm/validators" +) + +func TestGetValidatorSet_AfterEtna(t *testing.T) { + require := require.New(t) + + vdrs := validators.NewManager() + upgrades := upgradetest.GetConfig(upgradetest.Durango) + upgradeTime := genesistest.DefaultValidatorStartTime.Add(2 * time.Second) + upgrades.EtnaTime = upgradeTime + s := statetest.New(t, statetest.Config{ + Validators: vdrs, + Upgrades: upgrades, + }) + + sk, err := bls.NewSecretKey() + require.NoError(err) + var ( + subnetID = ids.GenerateTestID() + startTime = genesistest.DefaultValidatorStartTime + endTime = startTime.Add(24 * time.Hour) + pk = bls.PublicFromSecretKey(sk) + primaryStaker = &state.Staker{ + TxID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pk, + SubnetID: constants.PrimaryNetworkID, + Weight: 1, + StartTime: startTime, + EndTime: endTime, + PotentialReward: 1, + } + subnetStaker = &state.Staker{ + TxID: ids.GenerateTestID(), + NodeID: primaryStaker.NodeID, + PublicKey: nil, // inherited from primaryStaker + SubnetID: subnetID, + Weight: 1, + StartTime: upgradeTime, + EndTime: endTime, + } + ) + + // Add a subnet staker during the Etna upgrade + { + blk, err := block.NewBanffStandardBlock(upgradeTime, s.GetLastAccepted(), 1, nil) + require.NoError(err) + + s.SetHeight(blk.Height()) + s.SetTimestamp(blk.Timestamp()) + s.AddStatelessBlock(blk) + s.SetLastAccepted(blk.ID()) + + require.NoError(s.PutCurrentValidator(primaryStaker)) + require.NoError(s.PutCurrentValidator(subnetStaker)) + + require.NoError(s.Commit()) + } + + // Remove a subnet staker + { + blk, err := block.NewBanffStandardBlock(s.GetTimestamp(), s.GetLastAccepted(), 2, nil) + require.NoError(err) + + s.SetHeight(blk.Height()) + s.SetTimestamp(blk.Timestamp()) + s.AddStatelessBlock(blk) + s.SetLastAccepted(blk.ID()) + + s.DeleteCurrentValidator(subnetStaker) + + require.NoError(s.Commit()) + } + + m := NewManager( + logging.NoLog{}, + config.Config{ + Validators: vdrs, + }, + s, + metrics.Noop, + new(mockable.Clock), + ) + + expectedValidators := []map[ids.NodeID]*validators.GetValidatorOutput{ + {}, // Subnet staker didn't exist at genesis + { + subnetStaker.NodeID: { + NodeID: subnetStaker.NodeID, + PublicKey: pk, + Weight: subnetStaker.Weight, + }, + }, // Subnet staker was added at height 1 + {}, // Subnet staker was removed at height 2 + } + for height, expected := range expectedValidators { + actual, err := m.GetValidatorSet(context.Background(), uint64(height), subnetID) + require.NoError(err) + require.Equal(expected, actual) + } +} From 801762c73b078bbeada30733bb1753f0d8be7613 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 13:10:27 -0400 Subject: [PATCH 025/184] Split `writeCurrentStakers` into multiple functions (#3500) --- vms/platformvm/state/stakers.go | 29 +++ vms/platformvm/state/state.go | 375 +++++++++++++++-------------- vms/platformvm/state/state_test.go | 150 ++++++++---- 3 files changed, 324 insertions(+), 230 deletions(-) diff --git a/vms/platformvm/state/stakers.go b/vms/platformvm/state/stakers.go index 658796855958..14e4dcf7b1ef 100644 --- a/vms/platformvm/state/stakers.go +++ b/vms/platformvm/state/stakers.go @@ -5,6 +5,7 @@ package state import ( "errors" + "fmt" "github.com/google/btree" @@ -273,6 +274,34 @@ type diffValidator struct { deletedDelegators map[ids.ID]*Staker } +func (d *diffValidator) WeightDiff() (ValidatorWeightDiff, error) { + weightDiff := ValidatorWeightDiff{ + Decrease: d.validatorStatus == deleted, + } + if d.validatorStatus != unmodified { + weightDiff.Amount = d.validator.Weight + } + + for _, staker := range d.deletedDelegators { + if err := weightDiff.Add(true, staker.Weight); err != nil { + return ValidatorWeightDiff{}, fmt.Errorf("failed to decrease node weight diff: %w", err) + } + } + + addedDelegatorIterator := iterator.FromTree(d.addedDelegators) + defer addedDelegatorIterator.Release() + + for addedDelegatorIterator.Next() { + staker := addedDelegatorIterator.Value() + + if err := weightDiff.Add(false, staker.Weight); err != nil { + return ValidatorWeightDiff{}, fmt.Errorf("failed to increase node weight diff: %w", err) + } + } + + return weightDiff, nil +} + // GetValidator attempts to fetch the validator with the given subnetID and // nodeID. // Invariant: Assumes that the validator will never be removed and then added. diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 928d069476f3..be6bee28cf0f 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -4,6 +4,7 @@ package state import ( + "bytes" "context" "errors" "fmt" @@ -14,6 +15,7 @@ import ( "github.com/google/btree" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" + "golang.org/x/exp/maps" "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/cache/metercacher" @@ -1801,7 +1803,9 @@ func (s *state) write(updateValidators bool, height uint64) error { return errors.Join( s.writeBlocks(), s.writeExpiry(), - s.writeCurrentStakers(updateValidators, height, codecVersion), + s.updateValidatorManager(updateValidators), + s.writeValidatorDiffs(height), + s.writeCurrentStakers(codecVersion), s.writePendingStakers(), s.WriteValidatorMetadata(s.currentValidatorList, s.currentSubnetValidatorList, codecVersion), // Must be called after writeCurrentStakers s.writeTXs(), @@ -2049,47 +2053,75 @@ func (s *state) writeExpiry() error { return nil } -func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecVersion uint16) error { +// getInheritedPublicKey returns the primary network validator's public key. +// +// Note: This function may return a nil public key and no error if the primary +// network validator does not have a public key. +func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) { + if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is present. + return vdr.validator.PublicKey, nil + } + if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { + // The primary network validator is being modified. + return vdr.validator.PublicKey, nil + } + return nil, fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) +} + +// updateValidatorManager updates the validator manager with the pending +// validator set changes. +// +// This function must be called prior to writeCurrentStakers. +func (s *state) updateValidatorManager(updateValidators bool) error { + if !updateValidators { + return nil + } + for subnetID, validatorDiffs := range s.currentStakers.validatorDiffs { - // We must write the primary network stakers last because writing subnet - // validator diffs may depend on the primary network validator diffs to - // inherit the public keys. - if subnetID == constants.PrimaryNetworkID { - continue - } + // Record the change in weight and/or public key for each validator. + for nodeID, diff := range validatorDiffs { + weightDiff, err := diff.WeightDiff() + if err != nil { + return err + } - delete(s.currentStakers.validatorDiffs, subnetID) + if weightDiff.Amount == 0 { + continue // No weight change; go to the next validator. + } - err := s.writeCurrentStakersSubnetDiff( - subnetID, - validatorDiffs, - updateValidators, - height, - codecVersion, - ) - if err != nil { - return err - } - } + if weightDiff.Decrease { + if err := s.validators.RemoveWeight(subnetID, nodeID, weightDiff.Amount); err != nil { + return fmt.Errorf("failed to reduce validator weight: %w", err) + } + continue + } - if validatorDiffs, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID]; ok { - delete(s.currentStakers.validatorDiffs, constants.PrimaryNetworkID) + if diff.validatorStatus != added { + if err := s.validators.AddWeight(subnetID, nodeID, weightDiff.Amount); err != nil { + return fmt.Errorf("failed to increase validator weight: %w", err) + } + continue + } - err := s.writeCurrentStakersSubnetDiff( - constants.PrimaryNetworkID, - validatorDiffs, - updateValidators, - height, - codecVersion, - ) - if err != nil { - return err - } - } + pk, err := s.getInheritedPublicKey(nodeID) + if err != nil { + // This should never happen as there should always be a primary + // network validator corresponding to a subnet validator. + return err + } - // TODO: Move validator set management out of the state package - if !updateValidators { - return nil + err = s.validators.AddStaker( + subnetID, + nodeID, + pk, + diff.validator.TxID, + weightDiff.Amount, + ) + if err != nil { + return fmt.Errorf("failed to add validator: %w", err) + } + } } // Update the stake metrics @@ -2103,185 +2135,168 @@ func (s *state) writeCurrentStakers(updateValidators bool, height uint64, codecV return nil } -func (s *state) writeCurrentStakersSubnetDiff( - subnetID ids.ID, - validatorDiffs map[ids.NodeID]*diffValidator, - updateValidators bool, - height uint64, - codecVersion uint16, -) error { - // Select db to write to - validatorDB := s.currentSubnetValidatorList - delegatorDB := s.currentSubnetDelegatorList - if subnetID == constants.PrimaryNetworkID { - validatorDB = s.currentValidatorList - delegatorDB = s.currentDelegatorList - } +type validatorDiff struct { + weightDiff ValidatorWeightDiff + prevPublicKey []byte + newPublicKey []byte +} - // Record the change in weight and/or public key for each validator. - for nodeID, validatorDiff := range validatorDiffs { - var ( - staker *Staker - pk *bls.PublicKey - weightDiff = &ValidatorWeightDiff{ - Decrease: validatorDiff.validatorStatus == deleted, - } - ) - if validatorDiff.validatorStatus != unmodified { - staker = validatorDiff.validator - - pk = staker.PublicKey - // For non-primary network validators, the public key is inherited - // from the primary network. - if subnetID != constants.PrimaryNetworkID { - if vdr, ok := s.currentStakers.validators[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { - // The primary network validator is still present after - // writing. - pk = vdr.validator.PublicKey - } else if vdr, ok := s.currentStakers.validatorDiffs[constants.PrimaryNetworkID][nodeID]; ok && vdr.validator != nil { - // The primary network validator is being removed during - // writing. - pk = vdr.validator.PublicKey - } else { - // This should never happen as the primary network diffs are - // written last and subnet validator times must be a subset - // of the primary network validator times. - return fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) - } +// calculateValidatorDiffs calculates the validator set diff contained by the +// pending validator set changes. +// +// This function must be called prior to writeCurrentStakers. +func (s *state) calculateValidatorDiffs() (map[subnetIDNodeID]*validatorDiff, error) { + changes := make(map[subnetIDNodeID]*validatorDiff) + + // Calculate the changes to the pre-ACP-77 validator set + for subnetID, subnetDiffs := range s.currentStakers.validatorDiffs { + for nodeID, diff := range subnetDiffs { + weightDiff, err := diff.WeightDiff() + if err != nil { + return nil, err } - weightDiff.Amount = staker.Weight - } + pk, err := s.getInheritedPublicKey(nodeID) + if err != nil { + // This should never happen as there should always be a primary + // network validator corresponding to a subnet validator. + return nil, err + } - switch validatorDiff.validatorStatus { - case added: + change := &validatorDiff{ + weightDiff: weightDiff, + } if pk != nil { - // Record that the public key for the validator is being added. - // This means the prior value for the public key was nil. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - nil, - ) - if err != nil { - return err + pkBytes := bls.PublicKeyToUncompressedBytes(pk) + if diff.validatorStatus != added { + change.prevPublicKey = pkBytes + } + if diff.validatorStatus != deleted { + change.newPublicKey = pkBytes } } - // The validator is being added. - // - // Invariant: It's impossible for a delegator to have been rewarded - // in the same block that the validator was added. - startTime := uint64(staker.StartTime.Unix()) - metadata := &validatorMetadata{ - txID: staker.TxID, - lastUpdated: staker.StartTime, - - UpDuration: 0, - LastUpdated: startTime, - StakerStartTime: startTime, - PotentialReward: staker.PotentialReward, - PotentialDelegateeReward: 0, + subnetIDNodeID := subnetIDNodeID{ + subnetID: subnetID, + nodeID: nodeID, } + changes[subnetIDNodeID] = change + } + } - metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata) - if err != nil { - return fmt.Errorf("failed to serialize current validator: %w", err) - } + return changes, nil +} - if err = validatorDB.Put(staker.TxID[:], metadataBytes); err != nil { - return fmt.Errorf("failed to write current validator to list: %w", err) - } +// writeValidatorDiffs writes the validator set diff contained by the pending +// validator set changes to disk. +// +// This function must be called prior to writeCurrentStakers. +func (s *state) writeValidatorDiffs(height uint64) error { + changes, err := s.calculateValidatorDiffs() + if err != nil { + return err + } - s.validatorState.LoadValidatorMetadata(nodeID, subnetID, metadata) - case deleted: - if pk != nil { - // Record that the public key for the validator is being - // removed. This means we must record the prior value of the - // public key. - // - // Note: We store the uncompressed public key here as it is - // significantly more efficient to parse when applying diffs. - err := s.validatorPublicKeyDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - bls.PublicKeyToUncompressedBytes(pk), - ) - if err != nil { - return err - } + // Write the changes to the database + for subnetIDNodeID, diff := range changes { + diffKey := marshalDiffKey(subnetIDNodeID.subnetID, height, subnetIDNodeID.nodeID) + if diff.weightDiff.Amount != 0 { + err := s.validatorWeightDiffsDB.Put( + diffKey, + marshalWeightDiff(&diff.weightDiff), + ) + if err != nil { + return err } - - if err := validatorDB.Delete(staker.TxID[:]); err != nil { - return fmt.Errorf("failed to delete current staker: %w", err) + } + if !bytes.Equal(diff.prevPublicKey, diff.newPublicKey) { + err := s.validatorPublicKeyDiffsDB.Put( + diffKey, + diff.prevPublicKey, + ) + if err != nil { + return err } - - s.validatorState.DeleteValidatorMetadata(nodeID, subnetID) } + } + return nil +} - err := writeCurrentDelegatorDiff( - delegatorDB, - weightDiff, - validatorDiff, - codecVersion, - ) - if err != nil { - return err +func (s *state) writeCurrentStakers(codecVersion uint16) error { + for subnetID, validatorDiffs := range s.currentStakers.validatorDiffs { + // Select db to write to + validatorDB := s.currentSubnetValidatorList + delegatorDB := s.currentSubnetDelegatorList + if subnetID == constants.PrimaryNetworkID { + validatorDB = s.currentValidatorList + delegatorDB = s.currentDelegatorList } - if weightDiff.Amount == 0 { - // No weight change to record; go to next validator. - continue - } + // Record the change in weight and/or public key for each validator. + for nodeID, validatorDiff := range validatorDiffs { + switch validatorDiff.validatorStatus { + case added: + staker := validatorDiff.validator - err = s.validatorWeightDiffsDB.Put( - marshalDiffKey(subnetID, height, nodeID), - marshalWeightDiff(weightDiff), - ) - if err != nil { - return err - } + // The validator is being added. + // + // Invariant: It's impossible for a delegator to have been rewarded + // in the same block that the validator was added. + startTime := uint64(staker.StartTime.Unix()) + metadata := &validatorMetadata{ + txID: staker.TxID, + lastUpdated: staker.StartTime, + + UpDuration: 0, + LastUpdated: startTime, + StakerStartTime: startTime, + PotentialReward: staker.PotentialReward, + PotentialDelegateeReward: 0, + } - // TODO: Move the validator set management out of the state package - if !updateValidators { - continue - } + metadataBytes, err := MetadataCodec.Marshal(codecVersion, metadata) + if err != nil { + return fmt.Errorf("failed to serialize current validator: %w", err) + } - if weightDiff.Decrease { - err = s.validators.RemoveWeight(subnetID, nodeID, weightDiff.Amount) - } else { - if validatorDiff.validatorStatus == added { - err = s.validators.AddStaker( - subnetID, - nodeID, - pk, - staker.TxID, - weightDiff.Amount, - ) - } else { - err = s.validators.AddWeight(subnetID, nodeID, weightDiff.Amount) + if err = validatorDB.Put(staker.TxID[:], metadataBytes); err != nil { + return fmt.Errorf("failed to write current validator to list: %w", err) + } + + s.validatorState.LoadValidatorMetadata(nodeID, subnetID, metadata) + case deleted: + if err := validatorDB.Delete(validatorDiff.validator.TxID[:]); err != nil { + return fmt.Errorf("failed to delete current staker: %w", err) + } + + s.validatorState.DeleteValidatorMetadata(nodeID, subnetID) + } + + err := writeCurrentDelegatorDiff( + delegatorDB, + validatorDiff, + codecVersion, + ) + if err != nil { + return err } - } - if err != nil { - return fmt.Errorf("failed to update validator weight: %w", err) } } + maps.Clear(s.currentStakers.validatorDiffs) return nil } func writeCurrentDelegatorDiff( currentDelegatorList linkeddb.LinkedDB, - weightDiff *ValidatorWeightDiff, validatorDiff *diffValidator, codecVersion uint16, ) error { addedDelegatorIterator := iterator.FromTree(validatorDiff.addedDelegators) defer addedDelegatorIterator.Release() + for addedDelegatorIterator.Next() { staker := addedDelegatorIterator.Value() - if err := weightDiff.Add(false, staker.Weight); err != nil { - return fmt.Errorf("failed to increase node weight diff: %w", err) - } - metadata := &delegatorMetadata{ txID: staker.TxID, PotentialReward: staker.PotentialReward, @@ -2293,10 +2308,6 @@ func writeCurrentDelegatorDiff( } for _, staker := range validatorDiff.deletedDelegators { - if err := weightDiff.Add(true, staker.Weight); err != nil { - return fmt.Errorf("failed to decrease node weight diff: %w", err) - } - if err := currentDelegatorList.Delete(staker.TxID[:]); err != nil { return fmt.Errorf("failed to delete current staker: %w", err) } diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 85df176ce42a..143f673b4e0f 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -4,6 +4,7 @@ package state import ( + "bytes" "context" "math" "math/rand" @@ -27,7 +28,6 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/utils/wrappers" @@ -223,8 +223,7 @@ func TestState_writeStakers(t *testing.T) { expectedValidatorSetOutput *validators.GetValidatorOutput // Check whether weight/bls keys diffs are duly stored - expectedWeightDiff *ValidatorWeightDiff - expectedPublicKeyDiff maybe.Maybe[*bls.PublicKey] + expectedValidatorDiffs map[subnetIDNodeID]*validatorDiff }{ "add current primary network validator": { staker: primaryNetworkCurrentValidatorStaker, @@ -235,11 +234,19 @@ func TestState_writeStakers(t *testing.T) { PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, Weight: primaryNetworkCurrentValidatorStaker.Weight, }, - expectedWeightDiff: &ValidatorWeightDiff{ - Decrease: false, - Amount: primaryNetworkCurrentValidatorStaker.Weight, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{ + { + subnetID: constants.PrimaryNetworkID, + nodeID: primaryNetworkCurrentValidatorStaker.NodeID, + }: { + weightDiff: ValidatorWeightDiff{ + Decrease: false, + Amount: primaryNetworkCurrentValidatorStaker.Weight, + }, + prevPublicKey: nil, + newPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + }, }, - expectedPublicKeyDiff: maybe.Some[*bls.PublicKey](nil), }, "add current primary network delegator": { initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker}, @@ -253,15 +260,25 @@ func TestState_writeStakers(t *testing.T) { PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, Weight: primaryNetworkCurrentValidatorStaker.Weight + primaryNetworkCurrentDelegatorStaker.Weight, }, - expectedWeightDiff: &ValidatorWeightDiff{ - Decrease: false, - Amount: primaryNetworkCurrentDelegatorStaker.Weight, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{ + { + subnetID: constants.PrimaryNetworkID, + nodeID: primaryNetworkCurrentValidatorStaker.NodeID, + }: { + weightDiff: ValidatorWeightDiff{ + Decrease: false, + Amount: primaryNetworkCurrentDelegatorStaker.Weight, + }, + prevPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + newPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + }, }, }, "add pending primary network validator": { staker: primaryNetworkPendingValidatorStaker, addStakerTx: addPrimaryNetworkValidator, expectedPendingValidator: primaryNetworkPendingValidatorStaker, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{}, }, "add pending primary network delegator": { initialStakers: []*Staker{primaryNetworkPendingValidatorStaker}, @@ -270,6 +287,7 @@ func TestState_writeStakers(t *testing.T) { addStakerTx: addPrimaryNetworkDelegator, expectedPendingValidator: primaryNetworkPendingValidatorStaker, expectedPendingDelegators: []*Staker{primaryNetworkPendingDelegatorStaker}, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{}, }, "add current subnet validator": { initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker}, @@ -282,21 +300,37 @@ func TestState_writeStakers(t *testing.T) { PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, Weight: subnetCurrentValidatorStaker.Weight, }, - expectedWeightDiff: &ValidatorWeightDiff{ - Decrease: false, - Amount: subnetCurrentValidatorStaker.Weight, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{ + { + subnetID: subnetID, + nodeID: subnetCurrentValidatorStaker.NodeID, + }: { + weightDiff: ValidatorWeightDiff{ + Decrease: false, + Amount: subnetCurrentValidatorStaker.Weight, + }, + prevPublicKey: nil, + newPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + }, }, - expectedPublicKeyDiff: maybe.Some[*bls.PublicKey](nil), }, "delete current primary network validator": { initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker}, initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, staker: primaryNetworkCurrentValidatorStaker, - expectedWeightDiff: &ValidatorWeightDiff{ - Decrease: true, - Amount: primaryNetworkCurrentValidatorStaker.Weight, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{ + { + subnetID: constants.PrimaryNetworkID, + nodeID: primaryNetworkCurrentValidatorStaker.NodeID, + }: { + weightDiff: ValidatorWeightDiff{ + Decrease: true, + Amount: primaryNetworkCurrentValidatorStaker.Weight, + }, + prevPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + newPublicKey: nil, + }, }, - expectedPublicKeyDiff: maybe.Some(primaryNetworkCurrentValidatorStaker.PublicKey), }, "delete current primary network delegator": { initialStakers: []*Staker{ @@ -314,15 +348,25 @@ func TestState_writeStakers(t *testing.T) { PublicKey: primaryNetworkCurrentValidatorStaker.PublicKey, Weight: primaryNetworkCurrentValidatorStaker.Weight, }, - expectedWeightDiff: &ValidatorWeightDiff{ - Decrease: true, - Amount: primaryNetworkCurrentDelegatorStaker.Weight, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{ + { + subnetID: constants.PrimaryNetworkID, + nodeID: primaryNetworkCurrentValidatorStaker.NodeID, + }: { + weightDiff: ValidatorWeightDiff{ + Decrease: true, + Amount: primaryNetworkCurrentDelegatorStaker.Weight, + }, + prevPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + newPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + }, }, }, "delete pending primary network validator": { - initialStakers: []*Staker{primaryNetworkPendingValidatorStaker}, - initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, - staker: primaryNetworkPendingValidatorStaker, + initialStakers: []*Staker{primaryNetworkPendingValidatorStaker}, + initialTxs: []*txs.Tx{addPrimaryNetworkValidator}, + staker: primaryNetworkPendingValidatorStaker, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{}, }, "delete pending primary network delegator": { initialStakers: []*Staker{ @@ -335,16 +379,25 @@ func TestState_writeStakers(t *testing.T) { }, staker: primaryNetworkPendingDelegatorStaker, expectedPendingValidator: primaryNetworkPendingValidatorStaker, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{}, }, "delete current subnet validator": { initialStakers: []*Staker{primaryNetworkCurrentValidatorStaker, subnetCurrentValidatorStaker}, initialTxs: []*txs.Tx{addPrimaryNetworkValidator, addSubnetValidator}, staker: subnetCurrentValidatorStaker, - expectedWeightDiff: &ValidatorWeightDiff{ - Decrease: true, - Amount: subnetCurrentValidatorStaker.Weight, + expectedValidatorDiffs: map[subnetIDNodeID]*validatorDiff{ + { + subnetID: subnetID, + nodeID: subnetCurrentValidatorStaker.NodeID, + }: { + weightDiff: ValidatorWeightDiff{ + Decrease: true, + Amount: subnetCurrentValidatorStaker.Weight, + }, + prevPublicKey: bls.PublicKeyToUncompressedBytes(primaryNetworkCurrentValidatorStaker.PublicKey), + newPublicKey: nil, + }, }, - expectedPublicKeyDiff: maybe.Some[*bls.PublicKey](primaryNetworkCurrentValidatorStaker.PublicKey), }, } @@ -398,6 +451,10 @@ func TestState_writeStakers(t *testing.T) { state.AddTx(test.addStakerTx, status.Committed) } + validatorDiffs, err := state.calculateValidatorDiffs() + require.NoError(err) + require.Equal(test.expectedValidatorDiffs, validatorDiffs) + state.SetHeight(1) require.NoError(state.Commit()) @@ -453,29 +510,26 @@ func TestState_writeStakers(t *testing.T) { state.validators.GetMap(test.staker.SubnetID)[test.staker.NodeID], ) - diffKey := marshalDiffKey(test.staker.SubnetID, 1, test.staker.NodeID) - weightDiffBytes, err := state.validatorWeightDiffsDB.Get(diffKey) - if test.expectedWeightDiff == nil { - require.ErrorIs(err, database.ErrNotFound) - } else { - require.NoError(err) - - weightDiff, err := unmarshalWeightDiff(weightDiffBytes) - require.NoError(err) - require.Equal(test.expectedWeightDiff, weightDiff) - } + for subnetIDNodeID, expectedDiff := range test.expectedValidatorDiffs { + diffKey := marshalDiffKey(subnetIDNodeID.subnetID, 1, subnetIDNodeID.nodeID) + weightDiffBytes, err := state.validatorWeightDiffsDB.Get(diffKey) + if expectedDiff.weightDiff.Amount == 0 { + require.ErrorIs(err, database.ErrNotFound) + } else { + require.NoError(err) - publicKeyDiffBytes, err := state.validatorPublicKeyDiffsDB.Get(diffKey) - if test.expectedPublicKeyDiff.IsNothing() { - require.ErrorIs(err, database.ErrNotFound) - } else { - require.NoError(err) + weightDiff, err := unmarshalWeightDiff(weightDiffBytes) + require.NoError(err) + require.Equal(&expectedDiff.weightDiff, weightDiff) + } - expectedPublicKeyDiff := test.expectedPublicKeyDiff.Value() - if expectedPublicKeyDiff != nil { - require.Equal(expectedPublicKeyDiff, bls.PublicKeyFromValidUncompressedBytes(publicKeyDiffBytes)) + publicKeyDiffBytes, err := state.validatorPublicKeyDiffsDB.Get(diffKey) + if bytes.Equal(expectedDiff.prevPublicKey, expectedDiff.newPublicKey) { + require.ErrorIs(err, database.ErrNotFound) } else { - require.Empty(publicKeyDiffBytes) + require.NoError(err) + + require.Equal(expectedDiff.prevPublicKey, publicKeyDiffBytes) } } From 41f78f0915f4ffd54e7ca4572b6fe1de21faa6e5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 13:35:31 -0400 Subject: [PATCH 026/184] nit --- vms/platformvm/state/state.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 9372357270e8..1bbb4c1b1e9b 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -809,10 +809,11 @@ func (s *state) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool return has, nil } - // TODO: Add caching - key := make([]byte, len(subnetID)+len(nodeID)) - copy(key, subnetID[:]) - copy(key[len(subnetID):], nodeID[:]) + subnetIDNodeID := subnetIDNodeID{ + subnetID: subnetID, + nodeID: nodeID, + } + key := subnetIDNodeID.Marshal() return s.subnetIDNodeIDDB.Has(key) } From 29cd6badb5c59965b4d18ed8b058a006d29abf66 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 13:38:15 -0400 Subject: [PATCH 027/184] nit --- vms/platformvm/state/state.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 1bbb4c1b1e9b..68021ebebf0a 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -814,6 +814,8 @@ func (s *state) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool nodeID: nodeID, } key := subnetIDNodeID.Marshal() + + // TODO: Add caching return s.subnetIDNodeIDDB.Has(key) } From 8dfcbb15570d7618722c338e541ea139455ce4b9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 14:10:25 -0400 Subject: [PATCH 028/184] Fix initValidatorSets --- snow/validators/manager.go | 10 +++++ vms/platformvm/state/state.go | 10 ++--- vms/platformvm/state/state_test.go | 67 ++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/snow/validators/manager.go b/snow/validators/manager.go index 45ba32c0e261..6fd8a1b5f7c5 100644 --- a/snow/validators/manager.go +++ b/snow/validators/manager.go @@ -80,6 +80,9 @@ type Manager interface { // If an error is returned, the set will be unmodified. RemoveWeight(subnetID ids.ID, nodeID ids.NodeID, weight uint64) error + // NumSubnets returns the number of subnets with non-zero weight. + NumSubnets() int + // Count returns the number of validators currently in the subnet. Count(subnetID ids.ID) int @@ -227,6 +230,13 @@ func (m *manager) RemoveWeight(subnetID ids.ID, nodeID ids.NodeID, weight uint64 return nil } +func (m *manager) NumSubnets() int { + m.lock.RLock() + defer m.lock.RUnlock() + + return len(m.subnetToVdrs) +} + func (m *manager) Count(subnetID ids.ID) int { m.lock.RLock() set, exists := m.subnetToVdrs[subnetID] diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 68021ebebf0a..56cb2d85920e 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1867,6 +1867,11 @@ func (s *state) loadPendingValidators() error { // Invariant: initValidatorSets requires loadActiveSubnetOnlyValidators and // loadCurrentValidators to have already been called. func (s *state) initValidatorSets() error { + if s.validators.NumSubnets() != 0 { + // Enforce the invariant that the validator set is empty here. + return errValidatorSetAlreadyPopulated + } + // Load ACP77 validators for validationID, sov := range s.activeSOVLookup { pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) @@ -1913,11 +1918,6 @@ func (s *state) initValidatorSets() error { // Load primary network and non-ACP77 validators primaryNetworkValidators := s.currentStakers.validators[constants.PrimaryNetworkID] for subnetID, subnetValidators := range s.currentStakers.validators { - if s.validators.Count(subnetID) != 0 { - // Enforce the invariant that the validator set is empty here. - return fmt.Errorf("%w: %s", errValidatorSetAlreadyPopulated, subnetID) - } - for nodeID, subnetValidator := range subnetValidators { // The subnet validator's Public Key is inherited from the // corresponding primary network validator. diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 0ff795aefbdd..5dbf976e3270 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1944,6 +1944,73 @@ func TestSubnetOnlyValidators(t *testing.T) { } } +// TestLoadSubnetOnlyValidatorAndLegacy tests that the state can be loaded when +// there is a mix of legacy validators and subnet only validators in the same +// subnet. +func TestLoadSubnetOnlyValidatorAndLegacy(t *testing.T) { + var ( + require = require.New(t) + db = memdb.New() + state = newTestState(t, db) + subnetID = ids.GenerateTestID() + weight uint64 = 1 + ) + + unsignedAddSubnetValidator := createPermissionlessValidatorTx( + t, + subnetID, + txs.Validator{ + NodeID: defaultValidatorNodeID, + End: genesistest.DefaultValidatorEndTimeUnix, + Wght: weight, + }, + ) + addSubnetValidator := &txs.Tx{Unsigned: unsignedAddSubnetValidator} + require.NoError(addSubnetValidator.Initialize(txs.Codec)) + state.AddTx(addSubnetValidator, status.Committed) + + legacyStaker := &Staker{ + TxID: addSubnetValidator.ID(), + NodeID: defaultValidatorNodeID, + PublicKey: nil, + SubnetID: subnetID, + Weight: weight, + StartTime: genesistest.DefaultValidatorStartTime, + EndTime: genesistest.DefaultValidatorEndTime, + PotentialReward: 0, + } + require.NoError(state.PutCurrentValidator(legacyStaker)) + + sk, err := bls.NewSecretKey() + require.NoError(err) + pk := bls.PublicFromSecretKey(sk) + pkBytes := bls.PublicKeyToUncompressedBytes(pk) + + sov := SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: legacyStaker.SubnetID, + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + RemainingBalanceOwner: utils.RandomBytes(32), + DeactivationOwner: utils.RandomBytes(32), + StartTime: 1, + Weight: 2, + MinNonce: 3, + EndAccumulatedFee: 4, + } + require.NoError(state.PutSubnetOnlyValidator(sov)) + + state.SetHeight(1) + require.NoError(state.Commit()) + + expectedValidatorSet := state.validators.GetMap(subnetID) + + state = newTestState(t, db) + + validatorSet := state.validators.GetMap(subnetID) + require.Equal(expectedValidatorSet, validatorSet) +} + func TestSubnetOnlyValidatorAfterLegacyRemoval(t *testing.T) { require := require.New(t) From ef295480177969f4c4079073e54651bb1ef413be Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 14:35:25 -0400 Subject: [PATCH 029/184] nit --- node/overridden_manager.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node/overridden_manager.go b/node/overridden_manager.go index 484fe05da758..e961d2bd2d46 100644 --- a/node/overridden_manager.go +++ b/node/overridden_manager.go @@ -56,6 +56,10 @@ func (o *overriddenManager) RemoveWeight(_ ids.ID, nodeID ids.NodeID, weight uin return o.manager.RemoveWeight(o.subnetID, nodeID, weight) } +func (o *overriddenManager) NumSubnets() int { + return 1 +} + func (o *overriddenManager) Count(ids.ID) int { return o.manager.Count(o.subnetID) } From a77fb3c517811e9ee451c9aa3e4e5196fcb117af Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 14:35:48 -0400 Subject: [PATCH 030/184] test subnetIDNodeIDDB --- vms/platformvm/state/state_test.go | 43 +++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 5dbf976e3270..50ef6b4919df 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1832,7 +1832,7 @@ func TestSubnetOnlyValidators(t *testing.T) { verifyChain := func(chain Chain) { for _, expectedSOV := range expectedSOVs { - if expectedSOV.Weight != 0 { + if !expectedSOV.isDeleted() { continue } @@ -1846,7 +1846,7 @@ func TestSubnetOnlyValidators(t *testing.T) { expectedActive []SubnetOnlyValidator ) for _, expectedSOV := range expectedSOVs { - if expectedSOV.Weight == 0 { + if expectedSOV.isDeleted() { continue } @@ -1893,13 +1893,50 @@ func TestSubnetOnlyValidators(t *testing.T) { verifyChain(state) assertChainsEqual(t, state, d) + // Verify that the subnetID+nodeID -> validationID mapping is correct. + var populatedSubnetIDNodeIDs set.Set[subnetIDNodeID] + for _, sov := range expectedSOVs { + if sov.isDeleted() { + continue + } + + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + populatedSubnetIDNodeIDs.Add(subnetIDNodeID) + + subnetIDNodeIDKey := subnetIDNodeID.Marshal() + validatorID, err := database.GetID(state.subnetIDNodeIDDB, subnetIDNodeIDKey) + require.NoError(err) + require.Equal(sov.ValidationID, validatorID) + } + for _, sov := range expectedSOVs { + if !sov.isDeleted() { + continue + } + + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + if populatedSubnetIDNodeIDs.Contains(subnetIDNodeID) { + continue + } + + subnetIDNodeIDKey := subnetIDNodeID.Marshal() + has, err := state.subnetIDNodeIDDB.Has(subnetIDNodeIDKey) + require.NoError(err) + require.False(has) + } + sovsToValidatorSet := func( sovs map[ids.ID]SubnetOnlyValidator, subnetID ids.ID, ) map[ids.NodeID]*validators.GetValidatorOutput { validatorSet := make(map[ids.NodeID]*validators.GetValidatorOutput) for _, sov := range sovs { - if sov.SubnetID != subnetID || sov.Weight == 0 { + if sov.SubnetID != subnetID || sov.isDeleted() { continue } From ce05dc86a64624e348c97c5fc328009cc6b27799 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 14:40:54 -0400 Subject: [PATCH 031/184] fix comments --- vms/platformvm/state/state_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 50ef6b4919df..d65d2af9bf3c 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1730,7 +1730,7 @@ func TestSubnetOnlyValidators(t *testing.T) { NodeID: sov.NodeID, PublicKey: pkBytes, Weight: 2, // Not removed - EndAccumulatedFee: 1, // Inactive + EndAccumulatedFee: 1, // Active }, { ValidationID: sov.ValidationID, @@ -1738,7 +1738,7 @@ func TestSubnetOnlyValidators(t *testing.T) { NodeID: sov.NodeID, PublicKey: pkBytes, Weight: 3, // Not removed - EndAccumulatedFee: 1, // Inactive + EndAccumulatedFee: 1, // Active }, }, }, @@ -1768,7 +1768,7 @@ func TestSubnetOnlyValidators(t *testing.T) { NodeID: sov.NodeID, PublicKey: otherPKBytes, Weight: 1, // Not removed - EndAccumulatedFee: 1, // Inactive + EndAccumulatedFee: 1, // Active }, }, }, From 3d04cefefe4fb1999404b6ac016f0603931e1798 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 14:45:53 -0400 Subject: [PATCH 032/184] nit --- node/overridden_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/overridden_manager.go b/node/overridden_manager.go index e961d2bd2d46..12c647b53fe4 100644 --- a/node/overridden_manager.go +++ b/node/overridden_manager.go @@ -56,7 +56,7 @@ func (o *overriddenManager) RemoveWeight(_ ids.ID, nodeID ids.NodeID, weight uin return o.manager.RemoveWeight(o.subnetID, nodeID, weight) } -func (o *overriddenManager) NumSubnets() int { +func (*overriddenManager) NumSubnets() int { return 1 } From dc35645a866cd8beec3f7e13844f18c2c74359d7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 14:46:22 -0400 Subject: [PATCH 033/184] nit --- node/overridden_manager.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/node/overridden_manager.go b/node/overridden_manager.go index 12c647b53fe4..61e37833f140 100644 --- a/node/overridden_manager.go +++ b/node/overridden_manager.go @@ -56,7 +56,10 @@ func (o *overriddenManager) RemoveWeight(_ ids.ID, nodeID ids.NodeID, weight uin return o.manager.RemoveWeight(o.subnetID, nodeID, weight) } -func (*overriddenManager) NumSubnets() int { +func (o *overriddenManager) NumSubnets() int { + if o.manager.Count(o.subnetID) == 0 { + return 0 + } return 1 } From d472a9f6ebe1dffa4ec34e21cb2b0ed46e40cf45 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 16:07:39 -0400 Subject: [PATCH 034/184] Reduce diff --- vms/platformvm/state/state.go | 206 ++++++------------ vms/platformvm/state/state_test.go | 12 +- vms/platformvm/state/subnet_only_validator.go | 29 ++- 3 files changed, 91 insertions(+), 156 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 56cb2d85920e..e2451ecdeedb 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -34,7 +34,6 @@ import ( "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/logging" - "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -2236,6 +2235,20 @@ func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) return nil, fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) } +func (s *state) addSoVToValidatorManager(sov SubnetOnlyValidator) error { + nodeID := sov.effectiveNodeID() + if s.validators.GetWeight(sov.SubnetID, nodeID) != 0 { + return s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight) + } + return s.validators.AddStaker( + sov.SubnetID, + nodeID, + sov.effectivePublicKey(), + sov.effectiveValidationID(), + sov.Weight, + ) +} + // updateValidatorManager updates the validator manager with the pending // validator set changes. // @@ -2292,115 +2305,41 @@ func (s *state) updateValidatorManager(updateValidators bool) error { } } - // Perform SoV deletions: - var ( - sovChanges = s.sovDiff.modified - sovChangesApplied set.Set[ids.ID] - ) - for validationID, sov := range sovChanges { - if !sov.isDeleted() { - // Additions and modifications are handled in the next loops. - continue - } - - sovChangesApplied.Add(validationID) - - priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - if err == database.ErrNotFound { - // Deleting a non-existent validator is a noop. This can happen if - // the validator was added and then immediately removed. - continue - } - if err != nil { - return err - } - - nodeID := ids.EmptyNodeID - if priorSOV.isActive() { - nodeID = priorSOV.NodeID - } - if err := s.validators.RemoveWeight(priorSOV.SubnetID, nodeID, priorSOV.Weight); err != nil { - return fmt.Errorf("failed to delete SoV validator: %w", err) - } - } - - // Perform modifications: - for validationID, sov := range sovChanges { - if sovChangesApplied.Contains(validationID) { - continue - } - + for validationID, sov := range s.sovDiff.modified { priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - if err == database.ErrNotFound { - // New additions are handled in the next loop. - continue - } - if err != nil { - return err - } - - sovChangesApplied.Add(validationID) - - switch { - case priorSOV.isInactive() && sov.isActive(): - // This validator is being activated. - pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) - err = errors.Join( - s.validators.RemoveWeight(sov.SubnetID, ids.EmptyNodeID, priorSOV.Weight), - s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight), - ) - case priorSOV.isActive() && sov.isInactive(): - // This validator is being deactivated. - inactiveWeight := s.validators.GetWeight(sov.SubnetID, ids.EmptyNodeID) - if inactiveWeight == 0 { - err = s.validators.AddStaker(sov.SubnetID, ids.EmptyNodeID, nil, ids.Empty, sov.Weight) + switch err { + case nil: + if sov.isDeleted() { + // Removing a validator + err = s.validators.RemoveWeight(priorSOV.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight) } else { - err = s.validators.AddWeight(sov.SubnetID, ids.EmptyNodeID, sov.Weight) - } - err = errors.Join( - err, - s.validators.RemoveWeight(sov.SubnetID, sov.NodeID, priorSOV.Weight), - ) - default: - // This validator's active status isn't changing. - nodeID := ids.EmptyNodeID - if sov.isActive() { - nodeID = sov.NodeID - } - if priorSOV.Weight < sov.Weight { - err = s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight-priorSOV.Weight) - } else if priorSOV.Weight > sov.Weight { - err = s.validators.RemoveWeight(sov.SubnetID, nodeID, priorSOV.Weight-sov.Weight) + // Modifying a validator + if priorSOV.isActive() == sov.isActive() { + // This validator's active status isn't changing. This means + // the effectiveNodeIDs are equal. + nodeID := sov.effectiveNodeID() + if priorSOV.Weight < sov.Weight { + err = s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight-priorSOV.Weight) + } else if priorSOV.Weight > sov.Weight { + err = s.validators.RemoveWeight(sov.SubnetID, nodeID, priorSOV.Weight-sov.Weight) + } + } else { + // This validator's active status is changing. + err = errors.Join( + s.validators.RemoveWeight(sov.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight), + s.addSoVToValidatorManager(sov), + ) + } } - } - if err != nil { - return err - } - } - - // Perform additions: - for validationID, sov := range sovChanges { - if sovChangesApplied.Contains(validationID) { - continue - } - - if sov.isActive() { - pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) - if err := s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight); err != nil { - return fmt.Errorf("failed to add SoV validator: %w", err) + case database.ErrNotFound: + if sov.isDeleted() { + // Deleting a non-existent validator is a noop. This can happen + // if the validator was added and then immediately removed. + continue } - continue - } - // This validator is inactive - var ( - inactiveWeight = s.validators.GetWeight(sov.SubnetID, ids.EmptyNodeID) - err error - ) - if inactiveWeight == 0 { - err = s.validators.AddStaker(sov.SubnetID, ids.EmptyNodeID, nil, ids.Empty, sov.Weight) - } else { - err = s.validators.AddWeight(sov.SubnetID, ids.EmptyNodeID, sov.Weight) + // Adding a validator + err = s.addSoVToValidatorManager(sov) } if err != nil { return err @@ -2467,61 +2406,40 @@ func (s *state) calculateValidatorDiffs() (map[subnetIDNodeID]*validatorDiff, er } } - // Perform SoV deletions: - for validationID := range s.sovDiff.modified { + // Calculate the changes to the ACP-77 validator set + for validationID, sov := range s.sovDiff.modified { priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - if err == database.ErrNotFound { - continue - } - if err != nil { - return nil, err - } - - var ( - diff *validatorDiff - subnetIDNodeID = subnetIDNodeID{ + if err == nil { + // Delete the prior validator + subnetIDNodeID := subnetIDNodeID{ subnetID: priorSOV.SubnetID, + nodeID: priorSOV.effectiveNodeID(), } - ) - if priorSOV.isActive() { - subnetIDNodeID.nodeID = priorSOV.NodeID - diff = getOrDefault(changes, subnetIDNodeID) - diff.prevPublicKey = priorSOV.PublicKey - } else { - subnetIDNodeID.nodeID = ids.EmptyNodeID - diff = getOrDefault(changes, subnetIDNodeID) + diff := getOrDefault(changes, subnetIDNodeID) + if err := diff.weightDiff.Add(true, priorSOV.Weight); err != nil { + return nil, err + } + diff.prevPublicKey = priorSOV.effectivePublicKeyBytes() } - - if err := diff.weightDiff.Add(true, priorSOV.Weight); err != nil { + if err != database.ErrNotFound && err != nil { return nil, err } - } - // Perform SoV additions: - for _, sov := range s.sovDiff.modified { // If the validator is being removed, we shouldn't work to re-add it. if sov.isDeleted() { continue } - var ( - diff *validatorDiff - subnetIDNodeID = subnetIDNodeID{ - subnetID: sov.SubnetID, - } - ) - if sov.isActive() { - subnetIDNodeID.nodeID = sov.NodeID - diff = getOrDefault(changes, subnetIDNodeID) - diff.newPublicKey = sov.PublicKey - } else { - subnetIDNodeID.nodeID = ids.EmptyNodeID - diff = getOrDefault(changes, subnetIDNodeID) + // Add the new validator + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.effectiveNodeID(), } - + diff := getOrDefault(changes, subnetIDNodeID) if err := diff.weightDiff.Add(false, sov.Weight); err != nil { return nil, err } + diff.newPublicKey = sov.effectivePublicKeyBytes() } return changes, nil diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index d65d2af9bf3c..bc3ab907c7c7 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1940,20 +1940,12 @@ func TestSubnetOnlyValidators(t *testing.T) { continue } - nodeID := sov.NodeID - publicKey := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) - // Inactive validators are combined into a single validator - // with the empty ID. - if sov.EndAccumulatedFee == 0 { - nodeID = ids.EmptyNodeID - publicKey = nil - } - + nodeID := sov.effectiveNodeID() vdr, ok := validatorSet[nodeID] if !ok { vdr = &validators.GetValidatorOutput{ NodeID: nodeID, - PublicKey: publicKey, + PublicKey: sov.effectivePublicKey(), } validatorSet[nodeID] = vdr } diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index ddaa5eb5a50a..24ea8bde739e 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/vms/platformvm/block" @@ -146,8 +147,32 @@ func (v SubnetOnlyValidator) isActive() bool { return v.Weight != 0 && v.EndAccumulatedFee != 0 } -func (v SubnetOnlyValidator) isInactive() bool { - return v.Weight != 0 && v.EndAccumulatedFee == 0 +func (v SubnetOnlyValidator) effectiveValidationID() ids.ID { + if v.isActive() { + return v.ValidationID + } + return ids.Empty +} + +func (v SubnetOnlyValidator) effectiveNodeID() ids.NodeID { + if v.isActive() { + return v.NodeID + } + return ids.EmptyNodeID +} + +func (v SubnetOnlyValidator) effectivePublicKey() *bls.PublicKey { + if v.isActive() { + return bls.PublicKeyFromValidUncompressedBytes(v.PublicKey) + } + return nil +} + +func (v SubnetOnlyValidator) effectivePublicKeyBytes() []byte { + if v.isActive() { + return v.PublicKey + } + return nil } func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (SubnetOnlyValidator, error) { From 34ba29b13325af33ba3773617c907db80269a73f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 29 Oct 2024 16:16:46 -0400 Subject: [PATCH 035/184] add comment --- vms/platformvm/state/state.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index e2451ecdeedb..36c95516bab3 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1894,6 +1894,8 @@ func (s *state) initValidatorSets() error { return err } + // It is required for the SoVs to be loaded first so that the total + // weight is equal to the active weights here. activeWeight, err := s.validators.TotalWeight(subnetID) if err != nil { return err From 97029aa1e051df281456cecded55e1b67b730c7d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 10:12:36 -0400 Subject: [PATCH 036/184] reduce diff --- vms/platformvm/state/state.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 36c95516bab3..b9c975599f56 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2653,7 +2653,6 @@ func (s *state) writeSubnetOnlyValidators() error { return err } } - maps.Clear(s.sovDiff.modifiedTotalWeight) sovChanges := s.sovDiff.modified // Perform deletions: From dbeee70561cce248f44769117c8097fef29b7c48 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 10:45:14 -0400 Subject: [PATCH 037/184] Add NumSubnets to the validator manager interface --- network/network.go | 2 +- node/node.go | 2 +- node/overridden_manager.go | 11 +++- snow/engine/snowman/bootstrap/bootstrapper.go | 2 +- .../snowman/bootstrap/bootstrapper_test.go | 4 +- snow/engine/snowman/syncer/state_syncer.go | 2 +- .../snowman/syncer/state_syncer_test.go | 12 ++-- snow/engine/snowman/syncer/utils_test.go | 2 +- snow/validators/manager.go | 16 ++++- snow/validators/manager_test.go | 61 +++++++++++++------ vms/platformvm/state/state.go | 10 +-- vms/platformvm/vm_test.go | 4 +- 12 files changed, 83 insertions(+), 45 deletions(-) diff --git a/network/network.go b/network/network.go index eab4ecca085e..816cbc69c7a2 100644 --- a/network/network.go +++ b/network/network.go @@ -778,7 +778,7 @@ func (n *network) samplePeers( // As an optimization, if there are fewer validators than // [numValidatorsToSample], only attempt to sample [numValidatorsToSample] // validators to potentially avoid iterating over the entire peer set. - numValidatorsToSample := min(config.Validators, n.config.Validators.Count(subnetID)) + numValidatorsToSample := min(config.Validators, n.config.Validators.NumValidators(subnetID)) n.peersLock.RLock() defer n.peersLock.RUnlock() diff --git a/node/node.go b/node/node.go index 1fedf35eb97e..8cdc8edfce17 100644 --- a/node/node.go +++ b/node/node.go @@ -600,7 +600,7 @@ func (n *Node) initNetworking(reg prometheus.Registerer) error { } n.onSufficientlyConnected = make(chan struct{}) - numBootstrappers := n.bootstrappers.Count(constants.PrimaryNetworkID) + numBootstrappers := n.bootstrappers.NumValidators(constants.PrimaryNetworkID) requiredConns := (3*numBootstrappers + 3) / 4 if requiredConns > 0 { diff --git a/node/overridden_manager.go b/node/overridden_manager.go index 484fe05da758..467dd7df1fa7 100644 --- a/node/overridden_manager.go +++ b/node/overridden_manager.go @@ -56,8 +56,15 @@ func (o *overriddenManager) RemoveWeight(_ ids.ID, nodeID ids.NodeID, weight uin return o.manager.RemoveWeight(o.subnetID, nodeID, weight) } -func (o *overriddenManager) Count(ids.ID) int { - return o.manager.Count(o.subnetID) +func (o *overriddenManager) NumSubnets() int { + if o.manager.NumValidators(o.subnetID) == 0 { + return 0 + } + return 1 +} + +func (o *overriddenManager) NumValidators(ids.ID) int { + return o.manager.NumValidators(o.subnetID) } func (o *overriddenManager) TotalWeight(ids.ID) (uint64, error) { diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 024925c79288..4b0b511910f0 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -300,7 +300,7 @@ func (b *Bootstrapper) sendBootstrappingMessagesOrFinish(ctx context.Context) er if numAccepted == 0 { b.Ctx.Log.Debug("restarting bootstrap", zap.String("reason", "no blocks accepted"), - zap.Int("numBeacons", b.Beacons.Count(b.Ctx.SubnetID)), + zap.Int("numBeacons", b.Beacons.NumValidators(b.Ctx.SubnetID)), ) // Invariant: These functions are mutualy recursive. However, when // [startBootstrapping] calls [sendMessagesOrFinish], it is guaranteed diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 772cf51281e9..7803f82a725f 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -98,7 +98,7 @@ func newConfig(t *testing.T) (Config, ids.NodeID, *enginetest.Sender, *blocktest AllGetsServer: snowGetHandler, Ctx: ctx, Beacons: vdrs, - SampleK: vdrs.Count(ctx.SubnetID), + SampleK: vdrs.NumValidators(ctx.SubnetID), StartupTracker: startupTracker, PeerTracker: peerTracker, Sender: sender, @@ -693,7 +693,7 @@ func TestBootstrapNoParseOnNew(t *testing.T) { AllGetsServer: snowGetHandler, Ctx: ctx, Beacons: peers, - SampleK: peers.Count(ctx.SubnetID), + SampleK: peers.NumValidators(ctx.SubnetID), StartupTracker: startupTracker, PeerTracker: peerTracker, Sender: sender, diff --git a/snow/engine/snowman/syncer/state_syncer.go b/snow/engine/snowman/syncer/state_syncer.go index 76e647e73a64..7e669fee2534 100644 --- a/snow/engine/snowman/syncer/state_syncer.go +++ b/snow/engine/snowman/syncer/state_syncer.go @@ -373,7 +373,7 @@ func (ss *stateSyncer) AcceptedStateSummary(ctx context.Context, nodeID ids.Node if votingStakes < ss.Alpha { ss.Ctx.Log.Debug("restarting state sync", zap.String("reason", "not enough votes received"), - zap.Int("numBeacons", ss.StateSyncBeacons.Count(ss.Ctx.SubnetID)), + zap.Int("numBeacons", ss.StateSyncBeacons.NumValidators(ss.Ctx.SubnetID)), zap.Int("numFailedSyncers", ss.failedVoters.Len()), ) return ss.startup(ctx) diff --git a/snow/engine/snowman/syncer/state_syncer_test.go b/snow/engine/snowman/syncer/state_syncer_test.go index fd062cb2d8b1..ffb7de7ffca7 100644 --- a/snow/engine/snowman/syncer/state_syncer_test.go +++ b/snow/engine/snowman/syncer/state_syncer_test.go @@ -247,7 +247,7 @@ func TestBeaconsAreReachedForFrontiersUponStartup(t *testing.T) { } // check that vdrs are reached out for frontiers - require.Len(contactedFrontiersProviders, min(beacons.Count(ctx.SubnetID), maxOutstandingBroadcastRequests)) + require.Len(contactedFrontiersProviders, min(beacons.NumValidators(ctx.SubnetID), maxOutstandingBroadcastRequests)) for beaconID := range contactedFrontiersProviders { // check that beacon is duly marked as reached out require.Contains(syncer.pendingSeeders, beaconID) @@ -344,7 +344,7 @@ func TestUnRequestedStateSummaryFrontiersAreDropped(t *testing.T) { // other listed vdrs are reached for data require.True( len(contactedFrontiersProviders) > initiallyReachedOutBeaconsSize || - len(contactedFrontiersProviders) == beacons.Count(ctx.SubnetID)) + len(contactedFrontiersProviders) == beacons.NumValidators(ctx.SubnetID)) } func TestMalformedStateSummaryFrontiersAreDropped(t *testing.T) { @@ -413,7 +413,7 @@ func TestMalformedStateSummaryFrontiersAreDropped(t *testing.T) { // are reached for data require.True( len(contactedFrontiersProviders) > initiallyReachedOutBeaconsSize || - len(contactedFrontiersProviders) == beacons.Count(ctx.SubnetID)) + len(contactedFrontiersProviders) == beacons.NumValidators(ctx.SubnetID)) } func TestLateResponsesFromUnresponsiveFrontiersAreNotRecorded(t *testing.T) { @@ -475,7 +475,7 @@ func TestLateResponsesFromUnresponsiveFrontiersAreNotRecorded(t *testing.T) { // are reached for data require.True( len(contactedFrontiersProviders) > initiallyReachedOutBeaconsSize || - len(contactedFrontiersProviders) == beacons.Count(ctx.SubnetID)) + len(contactedFrontiersProviders) == beacons.NumValidators(ctx.SubnetID)) // mock VM to simulate a valid but late summary is returned fullVM.CantParseStateSummary = true @@ -773,7 +773,7 @@ func TestUnRequestedVotesAreDropped(t *testing.T) { // other listed voters are reached out require.True( len(contactedVoters) > initiallyContactedVotersSize || - len(contactedVoters) == beacons.Count(ctx.SubnetID)) + len(contactedVoters) == beacons.NumValidators(ctx.SubnetID)) } func TestVotesForUnknownSummariesAreDropped(t *testing.T) { @@ -876,7 +876,7 @@ func TestVotesForUnknownSummariesAreDropped(t *testing.T) { // on unknown summary require.True( len(contactedVoters) > initiallyContactedVotersSize || - len(contactedVoters) == beacons.Count(ctx.SubnetID)) + len(contactedVoters) == beacons.NumValidators(ctx.SubnetID)) } func TestStateSummaryIsPassedToVMAsMajorityOfVotesIsCastedForIt(t *testing.T) { diff --git a/snow/engine/snowman/syncer/utils_test.go b/snow/engine/snowman/syncer/utils_test.go index 4cd6e58d840e..d13b7347771b 100644 --- a/snow/engine/snowman/syncer/utils_test.go +++ b/snow/engine/snowman/syncer/utils_test.go @@ -107,7 +107,7 @@ func buildTestsObjects( startupTracker, sender, beacons, - beacons.Count(ctx.SubnetID), + beacons.NumValidators(ctx.SubnetID), alpha, nil, fullVM, diff --git a/snow/validators/manager.go b/snow/validators/manager.go index 45ba32c0e261..dd2a8f88d1f8 100644 --- a/snow/validators/manager.go +++ b/snow/validators/manager.go @@ -80,8 +80,11 @@ type Manager interface { // If an error is returned, the set will be unmodified. RemoveWeight(subnetID ids.ID, nodeID ids.NodeID, weight uint64) error - // Count returns the number of validators currently in the subnet. - Count(subnetID ids.ID) int + // NumSubnets returns the number of subnets with non-zero weight. + NumSubnets() int + + // NumValidators returns the number of validators currently in the subnet. + NumValidators(subnetID ids.ID) int // TotalWeight returns the cumulative weight of all validators in the subnet. // Returns err if total weight overflows uint64. @@ -227,7 +230,14 @@ func (m *manager) RemoveWeight(subnetID ids.ID, nodeID ids.NodeID, weight uint64 return nil } -func (m *manager) Count(subnetID ids.ID) int { +func (m *manager) NumSubnets() int { + m.lock.RLock() + defer m.lock.RUnlock() + + return len(m.subnetToVdrs) +} + +func (m *manager) NumValidators(subnetID ids.ID) int { m.lock.RLock() set, exists := m.subnetToVdrs[subnetID] m.lock.RUnlock() diff --git a/snow/validators/manager_test.go b/snow/validators/manager_test.go index 365d7ffdf7d7..4449a324a57d 100644 --- a/snow/validators/manager_test.go +++ b/snow/validators/manager_test.go @@ -242,36 +242,57 @@ func TestGet(t *testing.T) { require.False(ok) } -func TestLen(t *testing.T) { - require := require.New(t) +func TestNum(t *testing.T) { + var ( + require = require.New(t) - m := NewManager() - subnetID := ids.GenerateTestID() + m = NewManager() - count := m.Count(subnetID) - require.Zero(count) + subnetID0 = ids.GenerateTestID() + subnetID1 = ids.GenerateTestID() + nodeID0 = ids.GenerateTestNodeID() + nodeID1 = ids.GenerateTestNodeID() + ) - nodeID0 := ids.GenerateTestNodeID() - require.NoError(m.AddStaker(subnetID, nodeID0, nil, ids.Empty, 1)) + require.Zero(m.NumSubnets()) + require.Zero(m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) - count = m.Count(subnetID) - require.Equal(1, count) + require.NoError(m.AddStaker(subnetID0, nodeID0, nil, ids.Empty, 1)) - nodeID1 := ids.GenerateTestNodeID() - require.NoError(m.AddStaker(subnetID, nodeID1, nil, ids.Empty, 1)) + require.Equal(1, m.NumSubnets()) + require.Equal(1, m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) - count = m.Count(subnetID) - require.Equal(2, count) + require.NoError(m.AddStaker(subnetID0, nodeID1, nil, ids.Empty, 1)) - require.NoError(m.RemoveWeight(subnetID, nodeID1, 1)) + require.Equal(1, m.NumSubnets()) + require.Equal(2, m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) - count = m.Count(subnetID) - require.Equal(1, count) + require.NoError(m.AddStaker(subnetID1, nodeID1, nil, ids.Empty, 2)) - require.NoError(m.RemoveWeight(subnetID, nodeID0, 1)) + require.Equal(2, m.NumSubnets()) + require.Equal(2, m.NumValidators(subnetID0)) + require.Equal(1, m.NumValidators(subnetID1)) + + require.NoError(m.RemoveWeight(subnetID0, nodeID1, 1)) + + require.Equal(2, m.NumSubnets()) + require.Equal(1, m.NumValidators(subnetID0)) + require.Equal(1, m.NumValidators(subnetID1)) + + require.NoError(m.RemoveWeight(subnetID0, nodeID0, 1)) + + require.Equal(1, m.NumSubnets()) + require.Zero(m.NumValidators(subnetID0)) + require.Equal(1, m.NumValidators(subnetID1)) + + require.NoError(m.RemoveWeight(subnetID1, nodeID1, 2)) - count = m.Count(subnetID) - require.Zero(count) + require.Zero(m.NumSubnets()) + require.Zero(m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) } func TestGetMap(t *testing.T) { diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index be6bee28cf0f..09a22f8c27de 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1750,13 +1750,13 @@ func (s *state) loadPendingValidators() error { // Invariant: initValidatorSets requires loadCurrentValidators to have already // been called. func (s *state) initValidatorSets() error { + if s.validators.NumSubnets() != 0 { + // Enforce the invariant that the validator set is empty here. + return errValidatorSetAlreadyPopulated + } + primaryNetworkValidators := s.currentStakers.validators[constants.PrimaryNetworkID] for subnetID, subnetValidators := range s.currentStakers.validators { - if s.validators.Count(subnetID) != 0 { - // Enforce the invariant that the validator set is empty here. - return fmt.Errorf("%w: %s", errValidatorSetAlreadyPopulated, subnetID) - } - for nodeID, subnetValidator := range subnetValidators { // The subnet validator's Public Key is inherited from the // corresponding primary network validator. diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 7048778b19bd..f377fd31f894 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -283,7 +283,7 @@ func TestGenesis(t *testing.T) { } // Ensure current validator set of primary network is correct - require.Len(genesisState.Validators, vm.Validators.Count(constants.PrimaryNetworkID)) + require.Len(genesisState.Validators, vm.Validators.NumValidators(constants.PrimaryNetworkID)) for _, nodeID := range genesistest.DefaultNodeIDs { _, ok := vm.Validators.GetValidator(constants.PrimaryNetworkID, nodeID) @@ -1326,7 +1326,7 @@ func TestBootstrapPartiallyAccepted(t *testing.T) { AllGetsServer: snowGetHandler, Ctx: consensusCtx, Beacons: beacons, - SampleK: beacons.Count(ctx.SubnetID), + SampleK: beacons.NumValidators(ctx.SubnetID), StartupTracker: startup, PeerTracker: peerTracker, Sender: sender, From c39a7cea786cdc8c04bcc9e4cec964a71d085aed Mon Sep 17 00:00:00 2001 From: marun Date: Wed, 30 Oct 2024 17:02:06 +0100 Subject: [PATCH 038/184] [tmpnet] Refactor bootstrap monitor kubernetes functions for reuse (#3446) Signed-off-by: marun Co-authored-by: Stephen Buttolph --- tests/fixture/bootstrapmonitor/common.go | 51 +-- .../fixture/bootstrapmonitor/e2e/e2e_test.go | 224 ++----------- tests/fixture/bootstrapmonitor/wait.go | 2 +- tests/fixture/tmpnet/kube.go | 299 ++++++++++++++++++ 4 files changed, 333 insertions(+), 243 deletions(-) create mode 100644 tests/fixture/tmpnet/kube.go diff --git a/tests/fixture/bootstrapmonitor/common.go b/tests/fixture/bootstrapmonitor/common.go index e67736140504..22fcc3209d4c 100644 --- a/tests/fixture/bootstrapmonitor/common.go +++ b/tests/fixture/bootstrapmonitor/common.go @@ -19,6 +19,7 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/version" @@ -39,54 +40,6 @@ type bootstrapTestDetails struct { StartTime time.Time `json:"startTime"` } -// WaitForPodCondition watches the specified pod until the status includes the specified condition. -func WaitForPodCondition(ctx context.Context, clientset *kubernetes.Clientset, namespace string, podName string, conditionType corev1.PodConditionType) error { - return waitForPodStatus( - ctx, - clientset, - namespace, - podName, - func(status *corev1.PodStatus) bool { - for _, condition := range status.Conditions { - if condition.Type == conditionType && condition.Status == corev1.ConditionTrue { - return true - } - } - return false - }, - ) -} - -// waitForPodStatus watches the specified pod until the status is deemed acceptable by the provided test function. -func waitForPodStatus( - ctx context.Context, - clientset *kubernetes.Clientset, - namespace string, - name string, - acceptable func(*corev1.PodStatus) bool, -) error { - watch, err := clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.SingleObject(metav1.ObjectMeta{Name: name})) - if err != nil { - return fmt.Errorf("failed to initiate watch of pod %s/%s: %w", namespace, name, err) - } - - for { - select { - case event := <-watch.ResultChan(): - pod, ok := event.Object.(*corev1.Pod) - if !ok { - continue - } - - if acceptable(&pod.Status) { - return nil - } - case <-ctx.Done(): - return fmt.Errorf("timeout waiting for pod readiness: %w", ctx.Err()) - } - } -} - // setImageDetails updates the pod's owning statefulset with the image of the specified container and associated version details func setImageDetails(ctx context.Context, log logging.Logger, clientset *kubernetes.Clientset, namespace string, podName string, imageDetails *ImageDetails) error { // Determine the name of the statefulset to update @@ -212,7 +165,7 @@ func getLatestImageDetails( } qualifiedPodName := fmt.Sprintf("%s.%s", namespace, createdPod.Name) - err = waitForPodStatus(ctx, clientset, namespace, createdPod.Name, func(status *corev1.PodStatus) bool { + err = tmpnet.WaitForPodStatus(ctx, clientset, namespace, createdPod.Name, func(status *corev1.PodStatus) bool { return status.Phase == corev1.PodSucceeded || status.Phase == corev1.PodFailed }) if err != nil { diff --git a/tests/fixture/bootstrapmonitor/e2e/e2e_test.go b/tests/fixture/bootstrapmonitor/e2e/e2e_test.go index 7b1fc55fda9c..512dffbd3162 100644 --- a/tests/fixture/bootstrapmonitor/e2e/e2e_test.go +++ b/tests/fixture/bootstrapmonitor/e2e/e2e_test.go @@ -7,9 +7,6 @@ import ( "bufio" "flag" "fmt" - "io" - "net/http" - "net/url" "os" "os/exec" "path/filepath" @@ -18,15 +15,9 @@ import ( "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/portforward" - "k8s.io/client-go/transport/spdy" - "k8s.io/utils/pointer" - "github.com/ava-labs/avalanchego/api/info" "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/tests" @@ -34,7 +25,6 @@ import ( "github.com/ava-labs/avalanchego/tests/fixture/e2e" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/logging" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -130,7 +120,7 @@ var _ = ginkgo.Describe("[Bootstrap Tester]", func() { ginkgo.By(fmt.Sprintf("Created namespace %q", namespace)) ginkgo.By("Creating a node to bootstrap from") - nodeStatefulSet := newNodeStatefulSet("avalanchego-node", defaultNodeFlags()) + nodeStatefulSet := newNodeStatefulSet("avalanchego-node", defaultPodFlags()) createdNodeStatefulSet, err := clientset.AppsV1().StatefulSets(namespace).Create(tc.DefaultContext(), nodeStatefulSet, metav1.CreateOptions{}) require.NoError(err) nodePodName := createdNodeStatefulSet.Name + "-0" @@ -257,205 +247,53 @@ func buildImage(tc tests.TestContext, imageName string, forceNewHash bool, scrip require.NoError(err, "Image build failed: %s", output) } -// newNodeStatefulSet returns a statefulset for an avalanchego node. func newNodeStatefulSet(name string, flags map[string]string) *appsv1.StatefulSet { - return &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: name + "-", - }, - Spec: appsv1.StatefulSetSpec{ - Replicas: pointer.Int32(1), - ServiceName: name, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": name, - }, - }, - VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: volumeName, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: resource.MustParse(volumeSize), - }, - }, - }, - }, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "app": name, - }, - Annotations: map[string]string{ - // This needs to be present to ensure compatibility with json patch replace - bootstrapmonitor.VersionsAnnotationKey: "", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: nodeContainerName, - Image: latestAvalanchegoImage, - Ports: []corev1.ContainerPort{ - { - Name: "http", - ContainerPort: config.DefaultHTTPPort, - }, - { - Name: "staker", - ContainerPort: config.DefaultStakingPort, - }, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: volumeName, - MountPath: nodeDataDir, - }, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/ext/health/liveness", - Port: intstr.FromInt(config.DefaultHTTPPort), - }, - }, - PeriodSeconds: 1, - SuccessThreshold: 1, - }, - Env: stringMapToEnvVarSlice(flags), - }, - }, - }, - }, - }, - } -} + statefulSet := tmpnet.NewNodeStatefulSet( + name, + latestAvalanchegoImage, + nodeContainerName, + volumeName, + volumeSize, + nodeDataDir, + flags, + ) -// stringMapToEnvVarSlice converts a string map to a kube EnvVar slice. -func stringMapToEnvVarSlice(mapping map[string]string) []corev1.EnvVar { - envVars := make([]corev1.EnvVar, len(mapping)) - var i int - for k, v := range mapping { - envVars[i] = corev1.EnvVar{ - Name: config.EnvVarName(config.EnvPrefix, k), - Value: v, - } - i++ + // The version annotations key needs to be present to ensure compatibility with json patch replace + if statefulSet.Spec.Template.Annotations == nil { + statefulSet.Spec.Template.Annotations = map[string]string{} } - return envVars + statefulSet.Spec.Template.Annotations[bootstrapmonitor.VersionsAnnotationKey] = "" + + return statefulSet } -// defaultNodeFlags defines common flags for avalanchego nodes used by this test -func defaultNodeFlags() map[string]string { - return map[string]string{ - config.DataDirKey: nodeDataDir, - config.NetworkNameKey: constants.LocalName, - config.SybilProtectionEnabledKey: "false", - config.HealthCheckFreqKey: "500ms", // Ensure rapid detection of a healthy state - config.LogDisplayLevelKey: logging.Debug.String(), - config.LogLevelKey: logging.Debug.String(), - config.HTTPHostKey: "0.0.0.0", // Need to bind to pod IP to ensure kubelet can access the http port for the readiness check - } +func defaultPodFlags() map[string]string { + return tmpnet.DefaultPodFlags(constants.LocalName, nodeDataDir) } // waitForPodCondition waits until the specified pod reports the specified condition func waitForPodCondition(tc tests.TestContext, clientset *kubernetes.Clientset, namespace string, podName string, conditionType corev1.PodConditionType) { - require.NoError(tc, bootstrapmonitor.WaitForPodCondition(tc.DefaultContext(), clientset, namespace, podName, conditionType)) + require.NoError(tc, tmpnet.WaitForPodCondition(tc.DefaultContext(), clientset, namespace, podName, conditionType)) } -// waitForNodeHealthy waits for the node running in the specified pod to report healthy. func waitForNodeHealthy(tc tests.TestContext, kubeconfig *restclient.Config, namespace string, podName string) ids.NodeID { - require := require.New(tc) - - // A forwarded connection enables connectivity without exposing the node external to the kube cluster - ginkgo.By(fmt.Sprintf("Enabling a local forward for pod %s.%s", namespace, podName)) - localPort, localPortStopChan, err := enableLocalForwardForPod(kubeconfig, namespace, podName, config.DefaultHTTPPort, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter) - require.NoError(err) - defer close(localPortStopChan) - localNodeURI := fmt.Sprintf("http://127.0.0.1:%d", localPort) - - infoClient := info.NewClient(localNodeURI) - bootstrapNodeID, _, err := infoClient.GetNodeID(tc.DefaultContext()) - require.NoError(err) - - ginkgo.By(fmt.Sprintf("Waiting for pod %s.%s to report a healthy status at %s", namespace, podName, localNodeURI)) - require.Eventually(func() bool { - healthReply, err := tmpnet.CheckNodeHealth(tc.DefaultContext(), localNodeURI) - if err != nil { - tc.Outf("Error checking node health: %v\n", err) - return false - } - return healthReply.Healthy - }, e2e.DefaultTimeout, e2e.DefaultPollingInterval) - - return bootstrapNodeID -} - -// enableLocalForwardForPod enables traffic forwarding from a local port to the specified pod with client-go. The returned -// stop channel should be closed to stop the port forwarding. -func enableLocalForwardForPod(kubeconfig *restclient.Config, namespace string, name string, port int, out, errOut io.Writer) (uint16, chan struct{}, error) { - transport, upgrader, err := spdy.RoundTripperFor(kubeconfig) - if err != nil { - return 0, nil, fmt.Errorf("failed to create round tripper: %w", err) - } - - dialer := spdy.NewDialer( - upgrader, - &http.Client{ - Transport: transport, - }, - http.MethodPost, - &url.URL{ - Scheme: "https", - Path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, name), - Host: strings.TrimPrefix(kubeconfig.Host, "https://"), - }, + nodeID, err := tmpnet.WaitForNodeHealthy( + tc.DefaultContext(), + tc.Outf, + kubeconfig, + namespace, + podName, + e2e.DefaultPollingInterval, + ginkgo.GinkgoWriter, + ginkgo.GinkgoWriter, ) - ports := []string{fmt.Sprintf("0:%d", port)} - - // Need to specify 127.0.0.1 to ensure that forwarding is only attempted for the ipv4 - // address of the pod. By default, kind is deployed with only ipv4, and attempting to - // connect to a pod with ipv6 will fail. - addresses := []string{"127.0.0.1"} - - stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1) - forwarder, err := portforward.NewOnAddresses(dialer, addresses, ports, stopChan, readyChan, out, errOut) - if err != nil { - return 0, nil, fmt.Errorf("failed to create forwarder: %w", err) - } - - go func() { - if err := forwarder.ForwardPorts(); err != nil { - // TODO(marun) Need better error handling here? Or is ok for test-only usage? - panic(err) - } - }() - - <-readyChan // Wait for port forwarding to be ready - - // Retrieve the dynamically allocated local port - forwardedPorts, err := forwarder.GetPorts() - if err != nil { - close(stopChan) - return 0, nil, fmt.Errorf("failed to get forwarded ports: %w", err) - } - if len(forwardedPorts) == 0 { - close(stopChan) - return 0, nil, fmt.Errorf("failed to find at least one forwarded port: %w", err) - } - return forwardedPorts[0].Local, stopChan, nil + require.NoError(tc, err) + return nodeID } // createBootstrapTester creates a pod that can continuously bootstrap from the specified bootstrap IP+ID. func createBootstrapTester(tc tests.TestContext, clientset *kubernetes.Clientset, namespace string, bootstrapIP string, bootstrapNodeID ids.NodeID) *appsv1.StatefulSet { - flags := defaultNodeFlags() + flags := defaultPodFlags() flags[config.BootstrapIPsKey] = fmt.Sprintf("%s:%d", bootstrapIP, config.DefaultStakingPort) flags[config.BootstrapIDsKey] = bootstrapNodeID.String() diff --git a/tests/fixture/bootstrapmonitor/wait.go b/tests/fixture/bootstrapmonitor/wait.go index 1d34ad4c26b6..62daf6293a7c 100644 --- a/tests/fixture/bootstrapmonitor/wait.go +++ b/tests/fixture/bootstrapmonitor/wait.go @@ -72,7 +72,7 @@ func WaitForCompletion( // Avoid checking node health before it reports initial ready log.Info("Waiting for pod readiness") - if err := WaitForPodCondition(ctx, clientset, namespace, podName, corev1.PodReady); err != nil { + if err := tmpnet.WaitForPodCondition(ctx, clientset, namespace, podName, corev1.PodReady); err != nil { return fmt.Errorf("failed to wait for pod condition: %w", err) } diff --git a/tests/fixture/tmpnet/kube.go b/tests/fixture/tmpnet/kube.go new file mode 100644 index 000000000000..3bd93bc8bc92 --- /dev/null +++ b/tests/fixture/tmpnet/kube.go @@ -0,0 +1,299 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package tmpnet + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + "k8s.io/utils/pointer" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/config" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/logging" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + restclient "k8s.io/client-go/rest" +) + +// DefaultPodFlags defines common flags for avalanchego nodes running in a pod. +func DefaultPodFlags(networkName string, dataDir string) map[string]string { + return map[string]string{ + config.DataDirKey: dataDir, + config.NetworkNameKey: networkName, + config.SybilProtectionEnabledKey: "false", + config.HealthCheckFreqKey: "500ms", // Ensure rapid detection of a healthy state + config.LogDisplayLevelKey: logging.Debug.String(), + config.LogLevelKey: logging.Debug.String(), + config.HTTPHostKey: "0.0.0.0", // Need to bind to pod IP to ensure kubelet can access the http port for the readiness check + } +} + +// newNodeStatefulSet returns a statefulset for an avalanchego node. +func NewNodeStatefulSet( + name string, + imageName string, + containerName string, + volumeName string, + volumeSize string, + volumeMountPath string, + flags map[string]string, +) *appsv1.StatefulSet { + return &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: name + "-", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: pointer.Int32(1), + ServiceName: name, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": name, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: volumeName, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(volumeSize), + }, + }, + }, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": name, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: containerName, + Image: imageName, + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: config.DefaultHTTPPort, + }, + { + Name: "staker", + ContainerPort: config.DefaultStakingPort, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: volumeMountPath, + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/ext/health/liveness", + Port: intstr.FromInt(config.DefaultHTTPPort), + }, + }, + PeriodSeconds: 1, + SuccessThreshold: 1, + }, + Env: stringMapToEnvVarSlice(flags), + }, + }, + }, + }, + }, + } +} + +// stringMapToEnvVarSlice converts a string map to a kube EnvVar slice. +func stringMapToEnvVarSlice(mapping map[string]string) []corev1.EnvVar { + envVars := make([]corev1.EnvVar, len(mapping)) + var i int + for k, v := range mapping { + envVars[i] = corev1.EnvVar{ + Name: config.EnvVarName(config.EnvPrefix, k), + Value: v, + } + i++ + } + return envVars +} + +// WaitForNodeHealthy waits for the node running in the specified pod to report healthy. +func WaitForNodeHealthy( + ctx context.Context, + // TODO(marun) Use an actual logger + logFunc func(format string, args ...interface{}), + kubeconfig *restclient.Config, + namespace string, + podName string, + healthCheckInterval time.Duration, + out io.Writer, + outErr io.Writer, +) (ids.NodeID, error) { + // A forwarded connection enables connectivity without exposing the node external to the kube cluster + localPort, localPortStopChan, err := enableLocalForwardForPod( + kubeconfig, + namespace, + podName, + config.DefaultHTTPPort, + out, + outErr, + ) + if err != nil { + return ids.NodeID{}, fmt.Errorf("failed to enable local forward for pod: %w", err) + } + defer close(localPortStopChan) + localNodeURI := fmt.Sprintf("http://127.0.0.1:%d", localPort) + + // TODO(marun) A node started with tmpnet should know the node ID before start + infoClient := info.NewClient(localNodeURI) + bootstrapNodeID, _, err := infoClient.GetNodeID(ctx) + if err != nil { + return ids.NodeID{}, fmt.Errorf("failed to retrieve node bootstrap ID: %w", err) + } + if err := wait.PollImmediateInfinite(healthCheckInterval, func() (bool, error) { + healthReply, err := CheckNodeHealth(ctx, localNodeURI) + if errors.Is(ErrUnrecoverableNodeHealthCheck, err) { + return false, err + } else if err != nil { + // Error is potentially recoverable - log and continue + logFunc("Error checking node health: %v", err) + return false, nil + } + return healthReply.Healthy, nil + }); err != nil { + return ids.NodeID{}, fmt.Errorf("failed to wait for node to report healthy: %w", err) + } + + return bootstrapNodeID, nil +} + +// WaitForPodCondition watches the specified pod until the status includes the specified condition. +func WaitForPodCondition(ctx context.Context, clientset *kubernetes.Clientset, namespace string, podName string, conditionType corev1.PodConditionType) error { + return WaitForPodStatus( + ctx, + clientset, + namespace, + podName, + func(status *corev1.PodStatus) bool { + for _, condition := range status.Conditions { + if condition.Type == conditionType && condition.Status == corev1.ConditionTrue { + return true + } + } + return false + }, + ) +} + +// WaitForPodStatus watches the specified pod until the status is deemed acceptable by the provided test function. +func WaitForPodStatus( + ctx context.Context, + clientset *kubernetes.Clientset, + namespace string, + name string, + acceptable func(*corev1.PodStatus) bool, +) error { + watch, err := clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.SingleObject(metav1.ObjectMeta{Name: name})) + if err != nil { + return fmt.Errorf("failed to initiate watch of pod %s/%s: %w", namespace, name, err) + } + + for { + select { + case event := <-watch.ResultChan(): + pod, ok := event.Object.(*corev1.Pod) + if !ok { + continue + } + + if acceptable(&pod.Status) { + return nil + } + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for pod readiness: %w", ctx.Err()) + } + } +} + +// enableLocalForwardForPod enables traffic forwarding from a local port to the specified pod with client-go. The returned +// stop channel should be closed to stop the port forwarding. +func enableLocalForwardForPod( + kubeconfig *restclient.Config, + namespace string, + name string, + port int, + out, errOut io.Writer, +) (uint16, chan struct{}, error) { + transport, upgrader, err := spdy.RoundTripperFor(kubeconfig) + if err != nil { + return 0, nil, fmt.Errorf("failed to create round tripper: %w", err) + } + + dialer := spdy.NewDialer( + upgrader, + &http.Client{ + Transport: transport, + }, + http.MethodPost, + &url.URL{ + Scheme: "https", + Path: fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, name), + Host: strings.TrimPrefix(kubeconfig.Host, "https://"), + }, + ) + addresses := []string{"localhost"} + ports := []string{fmt.Sprintf("0:%d", port)} + stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1) + forwarder, err := portforward.NewOnAddresses(dialer, addresses, ports, stopChan, readyChan, out, errOut) + if err != nil { + return 0, nil, fmt.Errorf("failed to create forwarder: %w", err) + } + + go func() { + if err := forwarder.ForwardPorts(); err != nil { + // TODO(marun) Need better error handling here + panic(err) + } + }() + + <-readyChan // Wait for port forwarding to be ready + + // Retrieve the dynamically allocated local port + forwardedPorts, err := forwarder.GetPorts() + if err != nil { + close(stopChan) + return 0, nil, fmt.Errorf("failed to get forwarded ports: %w", err) + } + if len(forwardedPorts) == 0 { + close(stopChan) + return 0, nil, fmt.Errorf("failed to find at least one forwarded port: %w", err) + } + return forwardedPorts[0].Local, stopChan, nil +} From 3621e53fa19e65df8ab977915876a29b648f9737 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 12:50:25 -0400 Subject: [PATCH 039/184] add comment --- vms/platformvm/state/state_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index bc3ab907c7c7..df6e92c7b088 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -2040,6 +2040,8 @@ func TestLoadSubnetOnlyValidatorAndLegacy(t *testing.T) { require.Equal(expectedValidatorSet, validatorSet) } +// TestSubnetOnlyValidatorAfterLegacyRemoval verifies that a legacy validator +// can be replaced by an SoV in the same block. func TestSubnetOnlyValidatorAfterLegacyRemoval(t *testing.T) { require := require.New(t) From ad5f27f332cf293337432dfb4b759da05a642789 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 12:55:44 -0400 Subject: [PATCH 040/184] Add NumSubnets to the validator manager interface (#3504) --- network/network.go | 2 +- node/node.go | 2 +- node/overridden_manager.go | 11 +++- snow/engine/snowman/bootstrap/bootstrapper.go | 2 +- .../snowman/bootstrap/bootstrapper_test.go | 4 +- snow/engine/snowman/syncer/state_syncer.go | 2 +- .../snowman/syncer/state_syncer_test.go | 12 ++-- snow/engine/snowman/syncer/utils_test.go | 2 +- snow/validators/manager.go | 16 ++++- snow/validators/manager_test.go | 61 +++++++++++++------ vms/platformvm/state/state.go | 10 +-- vms/platformvm/vm_test.go | 4 +- 12 files changed, 83 insertions(+), 45 deletions(-) diff --git a/network/network.go b/network/network.go index eab4ecca085e..816cbc69c7a2 100644 --- a/network/network.go +++ b/network/network.go @@ -778,7 +778,7 @@ func (n *network) samplePeers( // As an optimization, if there are fewer validators than // [numValidatorsToSample], only attempt to sample [numValidatorsToSample] // validators to potentially avoid iterating over the entire peer set. - numValidatorsToSample := min(config.Validators, n.config.Validators.Count(subnetID)) + numValidatorsToSample := min(config.Validators, n.config.Validators.NumValidators(subnetID)) n.peersLock.RLock() defer n.peersLock.RUnlock() diff --git a/node/node.go b/node/node.go index 1fedf35eb97e..8cdc8edfce17 100644 --- a/node/node.go +++ b/node/node.go @@ -600,7 +600,7 @@ func (n *Node) initNetworking(reg prometheus.Registerer) error { } n.onSufficientlyConnected = make(chan struct{}) - numBootstrappers := n.bootstrappers.Count(constants.PrimaryNetworkID) + numBootstrappers := n.bootstrappers.NumValidators(constants.PrimaryNetworkID) requiredConns := (3*numBootstrappers + 3) / 4 if requiredConns > 0 { diff --git a/node/overridden_manager.go b/node/overridden_manager.go index 484fe05da758..467dd7df1fa7 100644 --- a/node/overridden_manager.go +++ b/node/overridden_manager.go @@ -56,8 +56,15 @@ func (o *overriddenManager) RemoveWeight(_ ids.ID, nodeID ids.NodeID, weight uin return o.manager.RemoveWeight(o.subnetID, nodeID, weight) } -func (o *overriddenManager) Count(ids.ID) int { - return o.manager.Count(o.subnetID) +func (o *overriddenManager) NumSubnets() int { + if o.manager.NumValidators(o.subnetID) == 0 { + return 0 + } + return 1 +} + +func (o *overriddenManager) NumValidators(ids.ID) int { + return o.manager.NumValidators(o.subnetID) } func (o *overriddenManager) TotalWeight(ids.ID) (uint64, error) { diff --git a/snow/engine/snowman/bootstrap/bootstrapper.go b/snow/engine/snowman/bootstrap/bootstrapper.go index 024925c79288..4b0b511910f0 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper.go +++ b/snow/engine/snowman/bootstrap/bootstrapper.go @@ -300,7 +300,7 @@ func (b *Bootstrapper) sendBootstrappingMessagesOrFinish(ctx context.Context) er if numAccepted == 0 { b.Ctx.Log.Debug("restarting bootstrap", zap.String("reason", "no blocks accepted"), - zap.Int("numBeacons", b.Beacons.Count(b.Ctx.SubnetID)), + zap.Int("numBeacons", b.Beacons.NumValidators(b.Ctx.SubnetID)), ) // Invariant: These functions are mutualy recursive. However, when // [startBootstrapping] calls [sendMessagesOrFinish], it is guaranteed diff --git a/snow/engine/snowman/bootstrap/bootstrapper_test.go b/snow/engine/snowman/bootstrap/bootstrapper_test.go index 772cf51281e9..7803f82a725f 100644 --- a/snow/engine/snowman/bootstrap/bootstrapper_test.go +++ b/snow/engine/snowman/bootstrap/bootstrapper_test.go @@ -98,7 +98,7 @@ func newConfig(t *testing.T) (Config, ids.NodeID, *enginetest.Sender, *blocktest AllGetsServer: snowGetHandler, Ctx: ctx, Beacons: vdrs, - SampleK: vdrs.Count(ctx.SubnetID), + SampleK: vdrs.NumValidators(ctx.SubnetID), StartupTracker: startupTracker, PeerTracker: peerTracker, Sender: sender, @@ -693,7 +693,7 @@ func TestBootstrapNoParseOnNew(t *testing.T) { AllGetsServer: snowGetHandler, Ctx: ctx, Beacons: peers, - SampleK: peers.Count(ctx.SubnetID), + SampleK: peers.NumValidators(ctx.SubnetID), StartupTracker: startupTracker, PeerTracker: peerTracker, Sender: sender, diff --git a/snow/engine/snowman/syncer/state_syncer.go b/snow/engine/snowman/syncer/state_syncer.go index 76e647e73a64..7e669fee2534 100644 --- a/snow/engine/snowman/syncer/state_syncer.go +++ b/snow/engine/snowman/syncer/state_syncer.go @@ -373,7 +373,7 @@ func (ss *stateSyncer) AcceptedStateSummary(ctx context.Context, nodeID ids.Node if votingStakes < ss.Alpha { ss.Ctx.Log.Debug("restarting state sync", zap.String("reason", "not enough votes received"), - zap.Int("numBeacons", ss.StateSyncBeacons.Count(ss.Ctx.SubnetID)), + zap.Int("numBeacons", ss.StateSyncBeacons.NumValidators(ss.Ctx.SubnetID)), zap.Int("numFailedSyncers", ss.failedVoters.Len()), ) return ss.startup(ctx) diff --git a/snow/engine/snowman/syncer/state_syncer_test.go b/snow/engine/snowman/syncer/state_syncer_test.go index fd062cb2d8b1..ffb7de7ffca7 100644 --- a/snow/engine/snowman/syncer/state_syncer_test.go +++ b/snow/engine/snowman/syncer/state_syncer_test.go @@ -247,7 +247,7 @@ func TestBeaconsAreReachedForFrontiersUponStartup(t *testing.T) { } // check that vdrs are reached out for frontiers - require.Len(contactedFrontiersProviders, min(beacons.Count(ctx.SubnetID), maxOutstandingBroadcastRequests)) + require.Len(contactedFrontiersProviders, min(beacons.NumValidators(ctx.SubnetID), maxOutstandingBroadcastRequests)) for beaconID := range contactedFrontiersProviders { // check that beacon is duly marked as reached out require.Contains(syncer.pendingSeeders, beaconID) @@ -344,7 +344,7 @@ func TestUnRequestedStateSummaryFrontiersAreDropped(t *testing.T) { // other listed vdrs are reached for data require.True( len(contactedFrontiersProviders) > initiallyReachedOutBeaconsSize || - len(contactedFrontiersProviders) == beacons.Count(ctx.SubnetID)) + len(contactedFrontiersProviders) == beacons.NumValidators(ctx.SubnetID)) } func TestMalformedStateSummaryFrontiersAreDropped(t *testing.T) { @@ -413,7 +413,7 @@ func TestMalformedStateSummaryFrontiersAreDropped(t *testing.T) { // are reached for data require.True( len(contactedFrontiersProviders) > initiallyReachedOutBeaconsSize || - len(contactedFrontiersProviders) == beacons.Count(ctx.SubnetID)) + len(contactedFrontiersProviders) == beacons.NumValidators(ctx.SubnetID)) } func TestLateResponsesFromUnresponsiveFrontiersAreNotRecorded(t *testing.T) { @@ -475,7 +475,7 @@ func TestLateResponsesFromUnresponsiveFrontiersAreNotRecorded(t *testing.T) { // are reached for data require.True( len(contactedFrontiersProviders) > initiallyReachedOutBeaconsSize || - len(contactedFrontiersProviders) == beacons.Count(ctx.SubnetID)) + len(contactedFrontiersProviders) == beacons.NumValidators(ctx.SubnetID)) // mock VM to simulate a valid but late summary is returned fullVM.CantParseStateSummary = true @@ -773,7 +773,7 @@ func TestUnRequestedVotesAreDropped(t *testing.T) { // other listed voters are reached out require.True( len(contactedVoters) > initiallyContactedVotersSize || - len(contactedVoters) == beacons.Count(ctx.SubnetID)) + len(contactedVoters) == beacons.NumValidators(ctx.SubnetID)) } func TestVotesForUnknownSummariesAreDropped(t *testing.T) { @@ -876,7 +876,7 @@ func TestVotesForUnknownSummariesAreDropped(t *testing.T) { // on unknown summary require.True( len(contactedVoters) > initiallyContactedVotersSize || - len(contactedVoters) == beacons.Count(ctx.SubnetID)) + len(contactedVoters) == beacons.NumValidators(ctx.SubnetID)) } func TestStateSummaryIsPassedToVMAsMajorityOfVotesIsCastedForIt(t *testing.T) { diff --git a/snow/engine/snowman/syncer/utils_test.go b/snow/engine/snowman/syncer/utils_test.go index 4cd6e58d840e..d13b7347771b 100644 --- a/snow/engine/snowman/syncer/utils_test.go +++ b/snow/engine/snowman/syncer/utils_test.go @@ -107,7 +107,7 @@ func buildTestsObjects( startupTracker, sender, beacons, - beacons.Count(ctx.SubnetID), + beacons.NumValidators(ctx.SubnetID), alpha, nil, fullVM, diff --git a/snow/validators/manager.go b/snow/validators/manager.go index 45ba32c0e261..dd2a8f88d1f8 100644 --- a/snow/validators/manager.go +++ b/snow/validators/manager.go @@ -80,8 +80,11 @@ type Manager interface { // If an error is returned, the set will be unmodified. RemoveWeight(subnetID ids.ID, nodeID ids.NodeID, weight uint64) error - // Count returns the number of validators currently in the subnet. - Count(subnetID ids.ID) int + // NumSubnets returns the number of subnets with non-zero weight. + NumSubnets() int + + // NumValidators returns the number of validators currently in the subnet. + NumValidators(subnetID ids.ID) int // TotalWeight returns the cumulative weight of all validators in the subnet. // Returns err if total weight overflows uint64. @@ -227,7 +230,14 @@ func (m *manager) RemoveWeight(subnetID ids.ID, nodeID ids.NodeID, weight uint64 return nil } -func (m *manager) Count(subnetID ids.ID) int { +func (m *manager) NumSubnets() int { + m.lock.RLock() + defer m.lock.RUnlock() + + return len(m.subnetToVdrs) +} + +func (m *manager) NumValidators(subnetID ids.ID) int { m.lock.RLock() set, exists := m.subnetToVdrs[subnetID] m.lock.RUnlock() diff --git a/snow/validators/manager_test.go b/snow/validators/manager_test.go index 365d7ffdf7d7..4449a324a57d 100644 --- a/snow/validators/manager_test.go +++ b/snow/validators/manager_test.go @@ -242,36 +242,57 @@ func TestGet(t *testing.T) { require.False(ok) } -func TestLen(t *testing.T) { - require := require.New(t) +func TestNum(t *testing.T) { + var ( + require = require.New(t) - m := NewManager() - subnetID := ids.GenerateTestID() + m = NewManager() - count := m.Count(subnetID) - require.Zero(count) + subnetID0 = ids.GenerateTestID() + subnetID1 = ids.GenerateTestID() + nodeID0 = ids.GenerateTestNodeID() + nodeID1 = ids.GenerateTestNodeID() + ) - nodeID0 := ids.GenerateTestNodeID() - require.NoError(m.AddStaker(subnetID, nodeID0, nil, ids.Empty, 1)) + require.Zero(m.NumSubnets()) + require.Zero(m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) - count = m.Count(subnetID) - require.Equal(1, count) + require.NoError(m.AddStaker(subnetID0, nodeID0, nil, ids.Empty, 1)) - nodeID1 := ids.GenerateTestNodeID() - require.NoError(m.AddStaker(subnetID, nodeID1, nil, ids.Empty, 1)) + require.Equal(1, m.NumSubnets()) + require.Equal(1, m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) - count = m.Count(subnetID) - require.Equal(2, count) + require.NoError(m.AddStaker(subnetID0, nodeID1, nil, ids.Empty, 1)) - require.NoError(m.RemoveWeight(subnetID, nodeID1, 1)) + require.Equal(1, m.NumSubnets()) + require.Equal(2, m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) - count = m.Count(subnetID) - require.Equal(1, count) + require.NoError(m.AddStaker(subnetID1, nodeID1, nil, ids.Empty, 2)) - require.NoError(m.RemoveWeight(subnetID, nodeID0, 1)) + require.Equal(2, m.NumSubnets()) + require.Equal(2, m.NumValidators(subnetID0)) + require.Equal(1, m.NumValidators(subnetID1)) + + require.NoError(m.RemoveWeight(subnetID0, nodeID1, 1)) + + require.Equal(2, m.NumSubnets()) + require.Equal(1, m.NumValidators(subnetID0)) + require.Equal(1, m.NumValidators(subnetID1)) + + require.NoError(m.RemoveWeight(subnetID0, nodeID0, 1)) + + require.Equal(1, m.NumSubnets()) + require.Zero(m.NumValidators(subnetID0)) + require.Equal(1, m.NumValidators(subnetID1)) + + require.NoError(m.RemoveWeight(subnetID1, nodeID1, 2)) - count = m.Count(subnetID) - require.Zero(count) + require.Zero(m.NumSubnets()) + require.Zero(m.NumValidators(subnetID0)) + require.Zero(m.NumValidators(subnetID1)) } func TestGetMap(t *testing.T) { diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index be6bee28cf0f..09a22f8c27de 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1750,13 +1750,13 @@ func (s *state) loadPendingValidators() error { // Invariant: initValidatorSets requires loadCurrentValidators to have already // been called. func (s *state) initValidatorSets() error { + if s.validators.NumSubnets() != 0 { + // Enforce the invariant that the validator set is empty here. + return errValidatorSetAlreadyPopulated + } + primaryNetworkValidators := s.currentStakers.validators[constants.PrimaryNetworkID] for subnetID, subnetValidators := range s.currentStakers.validators { - if s.validators.Count(subnetID) != 0 { - // Enforce the invariant that the validator set is empty here. - return fmt.Errorf("%w: %s", errValidatorSetAlreadyPopulated, subnetID) - } - for nodeID, subnetValidator := range subnetValidators { // The subnet validator's Public Key is inherited from the // corresponding primary network validator. diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 7048778b19bd..f377fd31f894 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -283,7 +283,7 @@ func TestGenesis(t *testing.T) { } // Ensure current validator set of primary network is correct - require.Len(genesisState.Validators, vm.Validators.Count(constants.PrimaryNetworkID)) + require.Len(genesisState.Validators, vm.Validators.NumValidators(constants.PrimaryNetworkID)) for _, nodeID := range genesistest.DefaultNodeIDs { _, ok := vm.Validators.GetValidator(constants.PrimaryNetworkID, nodeID) @@ -1326,7 +1326,7 @@ func TestBootstrapPartiallyAccepted(t *testing.T) { AllGetsServer: snowGetHandler, Ctx: consensusCtx, Beacons: beacons, - SampleK: beacons.Count(ctx.SubnetID), + SampleK: beacons.NumValidators(ctx.SubnetID), StartupTracker: startup, PeerTracker: peerTracker, Sender: sender, From 92a2277da4f7daf1880d5eb5a9c623a0012e000c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 13:19:44 -0400 Subject: [PATCH 041/184] Delete empty entries --- vms/platformvm/state/state.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index b9c975599f56..488e593779b8 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2649,7 +2649,13 @@ func writePendingDiff( func (s *state) writeSubnetOnlyValidators() error { // Write modified weights: for subnetID, weight := range s.sovDiff.modifiedTotalWeight { - if err := database.PutUInt64(s.weightsDB, subnetID[:], weight); err != nil { + var err error + if weight == 0 { + err = s.weightsDB.Delete(subnetID[:]) + } else { + err = database.PutUInt64(s.weightsDB, subnetID[:], weight) + } + if err != nil { return err } } From 6375aa2667a9fb4013b3ade5ab9a2788dd04d215 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 17:36:43 -0400 Subject: [PATCH 042/184] simplify state futher --- vms/platformvm/state/state.go | 90 ++++++------------- vms/platformvm/state/subnet_only_validator.go | 52 +++++++++++ 2 files changed, 78 insertions(+), 64 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 488e593779b8..2dc3dfa0143a 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -329,8 +329,7 @@ type state struct { expiryDiff *expiryDiff expiryDB database.Database - activeSOVLookup map[ids.ID]SubnetOnlyValidator - activeSOVs *btree.BTreeG[SubnetOnlyValidator] + activeSOVs *activeSubnetOnlyValidators sovDiff *subnetOnlyValidatorsDiff subnetOnlyValidatorsDB database.Database weightsDB database.Database @@ -657,8 +656,7 @@ func New( expiryDiff: newExpiryDiff(), expiryDB: prefixdb.New(ExpiryReplayProtectionPrefix, baseDB), - activeSOVLookup: make(map[ids.ID]SubnetOnlyValidator), - activeSOVs: btree.NewG(defaultTreeDegree, SubnetOnlyValidator.Less), + activeSOVs: newActiveSubnetOnlyValidators(), sovDiff: newSubnetOnlyValidatorsDiff(), subnetOnlyValidatorsDB: subnetOnlyValidatorsDB, weightsDB: prefixdb.New(WeightsPrefix, subnetOnlyValidatorsDB), @@ -763,12 +761,12 @@ func (s *state) DeleteExpiry(entry ExpiryEntry) { func (s *state) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) { return s.sovDiff.getActiveSubnetOnlyValidatorsIterator( - iterator.FromTree(s.activeSOVs), + s.activeSOVs.newIterator(), ), nil } func (s *state) NumActiveSubnetOnlyValidators() int { - return len(s.activeSOVLookup) + s.sovDiff.numAddedActive + return s.activeSOVs.len() + s.sovDiff.numAddedActive } func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { @@ -795,7 +793,7 @@ func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator // SubnetOnlyValidator with the given validationID. It is guaranteed that any // returned validator is either active or inactive. func (s *state) getPersistedSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { - if sov, ok := s.activeSOVLookup[validationID]; ok { + if sov, ok := s.activeSOVs.get(validationID); ok { return sov, nil } @@ -1602,8 +1600,7 @@ func (s *state) loadActiveSubnetOnlyValidators() error { return fmt.Errorf("failed to unmarshal SubnetOnlyValidator: %w", err) } - s.activeSOVLookup[validationID] = sov - s.activeSOVs.ReplaceOrInsert(sov) + s.activeSOVs.put(sov) } return nil @@ -1872,11 +1869,8 @@ func (s *state) initValidatorSets() error { } // Load ACP77 validators - for validationID, sov := range s.activeSOVLookup { - pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) - if err := s.validators.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight); err != nil { - return err - } + if err := s.activeSOVs.addStakers(s.validators); err != nil { + return err } // Load inactive weights @@ -2660,72 +2654,40 @@ func (s *state) writeSubnetOnlyValidators() error { } } - sovChanges := s.sovDiff.modified - // Perform deletions: - for validationID, sov := range sovChanges { - if !sov.isDeleted() { - // Additions and modifications are handled in the next loops. - continue + for validationID, sov := range s.sovDiff.modified { + // Delete the prior validator if it exists + var err error + if s.activeSOVs.delete(validationID) { + err = deleteSubnetOnlyValidator(s.activeDB, validationID) + } else { + err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) + } + if err != nil { + return err } - // The next loops shouldn't consider this change. - delete(sovChanges, validationID) - + // Update the subnetIDNodeID mapping subnetIDNodeID := subnetIDNodeID{ subnetID: sov.SubnetID, nodeID: sov.NodeID, } subnetIDNodeIDKey := subnetIDNodeID.Marshal() - err := s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey) - if err != nil { - return err - } - - priorSOV, wasActive := s.activeSOVLookup[validationID] - if wasActive { - delete(s.activeSOVLookup, validationID) - s.activeSOVs.Delete(priorSOV) - err = deleteSubnetOnlyValidator(s.activeDB, validationID) + if sov.isDeleted() { + err = s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey) } else { - // It is technically possible for the validator not to exist on disk - // here, but that's fine as deleting an entry that doesn't exist is - // a noop. - err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) + err = s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]) } if err != nil { return err } - } - // Perform modifications and additions: - for validationID, sov := range sovChanges { - priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) - switch err { - case nil: - // This is modifying an existing validator - if priorSOV.isActive() { - delete(s.activeSOVLookup, validationID) - s.activeSOVs.Delete(priorSOV) - err = deleteSubnetOnlyValidator(s.activeDB, validationID) - } else { - err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) - } - case database.ErrNotFound: - // This is a new validator - subnetIDNodeID := subnetIDNodeID{ - subnetID: sov.SubnetID, - nodeID: sov.NodeID, - } - subnetIDNodeIDKey := subnetIDNodeID.Marshal() - err = s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]) - } - if err != nil { - return err + if sov.isDeleted() { + continue } + // Add the new validator if sov.isActive() { - s.activeSOVLookup[validationID] = sov - s.activeSOVs.ReplaceOrInsert(sov) + s.activeSOVs.put(sov) err = putSubnetOnlyValidator(s.activeDB, sov) } else { err = putSubnetOnlyValidator(s.inactiveDB, sov) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 24ea8bde739e..0814e304d068 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" @@ -317,3 +318,54 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne } return nil } + +type activeSubnetOnlyValidators struct { + lookup map[ids.ID]SubnetOnlyValidator + tree *btree.BTreeG[SubnetOnlyValidator] +} + +func newActiveSubnetOnlyValidators() *activeSubnetOnlyValidators { + return &activeSubnetOnlyValidators{ + lookup: make(map[ids.ID]SubnetOnlyValidator), + tree: btree.NewG(defaultTreeDegree, SubnetOnlyValidator.Less), + } +} + +func (a *activeSubnetOnlyValidators) get(validationID ids.ID) (SubnetOnlyValidator, bool) { + sov, ok := a.lookup[validationID] + return sov, ok +} + +func (a *activeSubnetOnlyValidators) put(sov SubnetOnlyValidator) { + a.lookup[sov.ValidationID] = sov + a.tree.ReplaceOrInsert(sov) +} + +func (a *activeSubnetOnlyValidators) delete(validationID ids.ID) bool { + sov, ok := a.lookup[validationID] + if !ok { + return false + } + + delete(a.lookup, validationID) + a.tree.Delete(sov) + return true +} + +func (a *activeSubnetOnlyValidators) len() int { + return len(a.lookup) +} + +func (a *activeSubnetOnlyValidators) newIterator() iterator.Iterator[SubnetOnlyValidator] { + return iterator.FromTree(a.tree) +} + +func (a *activeSubnetOnlyValidators) addStakers(vdrs validators.Manager) error { + for validationID, sov := range a.lookup { + pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) + if err := vdrs.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight); err != nil { + return err + } + } + return nil +} From aedff159444e006e901d4c7352049281edfbf58a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 17:44:25 -0400 Subject: [PATCH 043/184] nit --- vms/platformvm/state/state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 2dc3dfa0143a..54e07902e3a8 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2641,7 +2641,7 @@ func writePendingDiff( } func (s *state) writeSubnetOnlyValidators() error { - // Write modified weights: + // Write modified weights for subnetID, weight := range s.sovDiff.modifiedTotalWeight { var err error if weight == 0 { From 1ac030ac06f5b24f79bef12ae69e05a0b7adbe16 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 30 Oct 2024 17:46:49 -0400 Subject: [PATCH 044/184] nit --- vms/platformvm/state/state.go | 18 ++---------------- vms/platformvm/state/subnet_only_validator.go | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 54e07902e3a8..cbdda6537a53 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2231,20 +2231,6 @@ func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) return nil, fmt.Errorf("%w: %s", errMissingPrimaryNetworkValidator, nodeID) } -func (s *state) addSoVToValidatorManager(sov SubnetOnlyValidator) error { - nodeID := sov.effectiveNodeID() - if s.validators.GetWeight(sov.SubnetID, nodeID) != 0 { - return s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight) - } - return s.validators.AddStaker( - sov.SubnetID, - nodeID, - sov.effectivePublicKey(), - sov.effectiveValidationID(), - sov.Weight, - ) -} - // updateValidatorManager updates the validator manager with the pending // validator set changes. // @@ -2323,7 +2309,7 @@ func (s *state) updateValidatorManager(updateValidators bool) error { // This validator's active status is changing. err = errors.Join( s.validators.RemoveWeight(sov.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight), - s.addSoVToValidatorManager(sov), + addSoVToValidatorManager(s.validators, sov), ) } } @@ -2335,7 +2321,7 @@ func (s *state) updateValidatorManager(updateValidators bool) error { } // Adding a validator - err = s.addSoVToValidatorManager(sov) + err = addSoVToValidatorManager(s.validators, sov) } if err != nil { return err diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 0814e304d068..a299cd478c3e 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -369,3 +369,17 @@ func (a *activeSubnetOnlyValidators) addStakers(vdrs validators.Manager) error { } return nil } + +func addSoVToValidatorManager(vdrs validators.Manager, sov SubnetOnlyValidator) error { + nodeID := sov.effectiveNodeID() + if vdrs.GetWeight(sov.SubnetID, nodeID) != 0 { + return vdrs.AddWeight(sov.SubnetID, nodeID, sov.Weight) + } + return vdrs.AddStaker( + sov.SubnetID, + nodeID, + sov.effectivePublicKey(), + sov.effectiveValidationID(), + sov.Weight, + ) +} From 2a69d6441cf95f348c55d23f1baea0eb729dbc8f Mon Sep 17 00:00:00 2001 From: Michael Kaplan <55204436+michaelkaplan13@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:49:39 -0400 Subject: [PATCH 045/184] Clarify partial sync flag (#3505) Signed-off-by: Michael Kaplan <55204436+michaelkaplan13@users.noreply.github.com> --- config/config.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/config.md b/config/config.md index 1b9fae2bba95..7e0c490ac854 100644 --- a/config/config.md +++ b/config/config.md @@ -176,7 +176,9 @@ must be the same with the number of given `--state-sync-ids`. #### `--partial-sync-primary-network` (string) -Partial sync enables non-validators to optionally sync only the P-chain on the primary network. +Partial sync enables nodes that are not primary network validators to optionally sync +only the P-chain on the primary network. Nodes that use this option can still track +Subnets. After the Etna upgrade, nodes that use this option can also validate L1s. ## Chain Configs From d82d790ef71e27696b76eb6bae6109c88c94e085 Mon Sep 17 00:00:00 2001 From: yacovm Date: Thu, 31 Oct 2024 00:57:03 +0100 Subject: [PATCH 046/184] Update BLST to v0.3.13 (#3506) Signed-off-by: Yacov Manevich Co-authored-by: marun --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3900e0bbfc3c..bdd3d7d772e5 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.9.0 - github.com/supranational/blst v0.3.11 + github.com/supranational/blst v0.3.13 github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a github.com/thepudds/fzgen v0.4.2 github.com/tyler-smith/go-bip32 v1.0.0 diff --git a/go.sum b/go.sum index 9b4d4fa88781..6457f8ea36eb 100644 --- a/go.sum +++ b/go.sum @@ -602,8 +602,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= -github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= -github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= From 739544eaf2bc33a683d15ab7b80f90a73e574b3d Mon Sep 17 00:00:00 2001 From: yacovm Date: Thu, 31 Oct 2024 16:00:49 +0100 Subject: [PATCH 047/184] Restrict public keys prior to TLS handshake (#3501) Signed-off-by: Yacov Manevich --- network/peer/8192RSA_test.pem | 100 +++++++++++++++ network/peer/tls_config.go | 43 +++++++ network/peer/tls_config_test.go | 134 ++++++++++++++++++++ network/peer/upgrader_test.go | 212 ++++++++++++++++++++++++++++++++ staking/parse.go | 34 +++-- staking/parse_test.go | 73 +++++++++++ 6 files changed, 584 insertions(+), 12 deletions(-) create mode 100644 network/peer/8192RSA_test.pem create mode 100644 network/peer/tls_config_test.go create mode 100644 network/peer/upgrader_test.go diff --git a/network/peer/8192RSA_test.pem b/network/peer/8192RSA_test.pem new file mode 100644 index 000000000000..77d2ce8c7826 --- /dev/null +++ b/network/peer/8192RSA_test.pem @@ -0,0 +1,100 @@ +-----BEGIN ----- +MIISRAIBADANBgkqhkiG9w0BAQEFAASCEi4wghIqAgEAAoIEAQDBND9cycCvR+iY +uKrG58jvz5VcU+V4ff9AQ4jUXAUCcUTtxz/82BCSBS0SJZLPJq3HHMHFZ8Qnjrfo +yMYACZ5wnZvpyUdOMVqAka6k1IYLkD/+sQYN92c6GZIOxNciBrox804jRTGl9Frg +5AX4cmmGBXyhJVm0waxl2PAqdayPJ/cf+CSWn3XyzkE/irBHFLFny9BnH5h111lO +4AMpl8yojVWPqPVuj/S5JJ7z9Mnut6nMHEumy184+C952NhGMmlYhX4OTW/S/Ylp +b+vURMk44/iMCJ2XanSD85lEZOJKIq05e9vNdcIn23F//e//oD4zMpAfiwWOkcNP +tva8OLpAZcREbqHn/2skTkl25hLOtUubChF/Zfj5xAnbcD18iyIug/KryyQsA0HK +e2xzdBOkj5axRzOTIgg8bwz+eEoGLM0kmSxd5TQXA130bP7zhAyZ2Bb4pJsPC03/ +h8yc4lBhsagc1s2+tj/tdRk/fAwadwp2V1+hPI70dM13jt7xeNh6yyqNcP13Ky1Y +ajfYYsXSkSVOsovJmUyO7EqeHlP5bxUirljSG+RXqcbvdzvEaFTfM5iGMaHxn7IC +i8pJPUUCDseqsT0VFXD5UYc/G4rxSFPrecEQN14wHcMyUzO4r8CLjidBUJn6HQcQ +RiIP+uFSF70UAz5XvdmyqLeXS/ZhsMqLBH1qvsnd3cvGEStDjsujZf/u4VhOKe8f +SV9U2c7YNu22ShX1BnRzuZgDavnNr/Be+eyxwZ8z1XtGTqOMvL18kPMo9RGxaoKL +if+0yahFBGkY8TsmbB578Vxn0EYFx6neNw6H1HHpRS5osraG4iJTpBfgwVLyRLBC +aLB6mnsTie3bjUbIJCaFeOVPNrlyzNbuG22SJYCrOXIXzBNjh+GHNT//ubP/DWoz +wkVAHhooz315KMFiOyKfrlaoVa9IOFBEFJ2VmedA+j3VkJSfocBBGrdDGT0XijXX +ZCUSwn5EY7Ks5/gb3jGcpts028mTwkaLLRqL5e40ZF5FLmJHih7sbl6YyrUbg/Lv +ihe49CXO4DqWyCgq5+Zk1nq2eWalgsHpGchdMOPPkJHY/8HdAiBt/4trD12PstF3 +qy8rmzTh8CH+RzWTcnb5A0bwgVcTZErEbKcE938pfsFgenHQmAs2ryRIMmMnu2ck +GlrJzowYVmWFBPIIoT5BsytWYLluErs9p8FjhHhTA1z9AE56cyu5qwz46i4TAysF +TJ+LITvQLeVt1lYooXNfUXsdxRbBqKXb2kzkACWNhtVeofLsqFIxRM4z9F7X9r0h +skXG+42WZRrLK/3hW5NtQ2BX3WvEjBBlGwR4n6IcCJeX6plL0exiYMUuPqwogJBv +o3h/gCD5AgMBAAECggQASQ1EWAVBAgWigPxyNjs10tceloZyYZjihp4Cgqk4i6/g +bDfGjgf0XAHxBMeINyNc2ciZy9ZsaLih+TbRBvqcGeC+LyuX9ozat3peGpzxAjZM +vDSbIXTGZ0V74HG1FnyMso5YoSVsnF9EbXxKdaJtG+u/L/87aAlC8k+Qn71WvdpS +qpfc3cb1hhVOvoPmGzpLyf9akWN09jmy3wv8piFrlN+71lIAWwm7crXSFFQedlCj +tzWLtUl4e8X7zYqcXA57nqj6/NVyzshmyKM0/FH187jfJbOsQrBR1gKplR7AIV/z +N6UJeypne0KSK98MfA9O9XTM4eBi/YFH5EA+EvUwF2FjUKy0M1B0ZonjZT2hJt+N +8tVfwFgCSA5D2+EYnprNFeF2RFbPGoUwvyrj2tOtCa/xPp65dYyMqK0ksKMy+hq+ +hnQUPnyHsZvoTp9X1yO60ADQzrsOliWkHFZwm3FHC2ltM1pU+SNYEKUSItr4iJky +L4Th98k6FFyFxAsVaSBUWjmvoUNz0zdUMfYXn43ZVsDi5lrEWDnKpM/bduXowoup +5i8eDnPVZwAe5DSlOKJqVOrhZPwnS4EigavxlLfB/AEypevWOL6etOaKyOXVJ149 +vO+QfF0zE+ZtA/5JtC9gEmRxm1Sqo9ON9C1Qe9JUmAG50HNZgzuZsN/yaxah1lWl +0It7akaxJgeEl747FIsi8Q7+/6hrhKapEFrJjH/1KNy2yZc9S6e7N+hQkAO5iN0G +B3R/bVe+DFqK0iXas4Eac9/mnfqDNbPkxFX7OZkAFqDhbj+4j8JsVTTmCiPXiZQc +FvzHsF3WL+Gp+/iui80530Wds9hQ/z7rwIDsMRX/+TxzhkIHTl3BVZ1IbvWcf6Nh +Ieyp7Umr9+oaJr5sHuWwh+aa5vkIz+2l+wcDE9gfkxclWSxKhumb+SYH404JdORs +ZPm86sBmF1ePcOQ+T6oNVTPnM8pDWyO4E0bYsv/qso/ZEd0tIQiwQOoTyJ1kLe5X +5TdXYmV3oby2bp7FM3Tb1AX2e7LFOboxMImU9z+X9+orXy6IEky7QfE3w2SGzgmv +JrZ18vtpAVgFefqqj3PnmJlc8q6YbU31wHwnyiSZDSe0TEPr4hOPUOe/Qn7KJVy6 +fs+K6PWdcIDNZx9jgnTWPhkWYM0pGznAAy+V8PlCfQMc57I47HhnFzR4YGWWfox0 +K3TI7yUJJpa17TSTKUwsuFNTTahxC3439u1wj6xBdDf+vR0tq6svCmzBcJ15vS4r +yrm8J00OtPnlA1zoYlfWwDvNIFvQ5xgxNEcpnD2jUoe08FaYcZ6Xxz+92OhGNYPm +67d7P55mST60fa8fNWM82BW+tV5oo5+1n/uphjxQcQKCAgEA8EYIqh+ij5bL1+af +zf8BgB/ovdwKOma6MzHop1+fW+wRr92u7mYhkaI1bA+/X9FrC844Lb7oJ0w8E3gg +WMyw9DGHEqk+/FCiEfb/U8D9h0/zV8YTPlV1/sZmmwQgItoiaIZP6LOiNTevhjL6 +JNDM+Uh8Wd15AT9RA2vdPWdWR0DvEbRXPx2RkFpHoBDiI6+CuJ+vgpBbSSU7PTZ5 ++C8PTuFMh0V7fp16rufs8HNPt/CE/5z4KhVIArfgh6yF8VzW7Fz+RHL6Ao3n5khj +0/14nblKBEfeNpDcCMDRKPYxPi2iTvvMmk8ZZg1XsbdJQZbpUBfp66kDRlGuamlG +92Nqjl2+fPOJhmKQYls1oFL1Gp0RIQeYmJvVRwQlazo2a94QJN5A8MkhxXjLjHGz +IKKrwbJ4sRAS0GPs3hruRggDGoJpXMKWXa98cUp9KOYVvBnNoQLA6V8WDfqRCV58 +BY/ogpPGeJcoDE6h//SPL4vp2r2rGy9GGPnCbNZGfYuCOHlJvwcddsuM86ApSxOs +rsGMHMz85xtINlhIQgTyr13+EcZEkfB0j9Yv1mdlpTs5tuDDu09TGUEXAFUiD2Rs +WepKpT7rX+KoIuMjMwuUaLdgXP6OPvsPHqLc3oFKkMY4H0yHOq2bDse80jrEg++U +JbhKDABA0xUSmSCHxGNMTvD4x28CggIBAM3Zh2AwWiLsHCHqPY5/NWenLENeWDhs +Omirl5OkTRGYxrzvELDDnMmyh0QfbdkUn6aoXird8sPGHvjYIgKVxiK/q8Hbz1x+ +a9lqfLV2DBSQZGq3OvpiLubPcHCnJm3+MXL7foer49HatAK7qimcq8SKEMGXx2Fy +pzIHtoBm7JXZqrhr6o6K6GCpPm57YrkqEAz6bu+oCgHzmm4QN5aVcFr3/JxSZbdV +hccRBP87BkzyQrfgDqra2oxvKNV1KL7ByhdQxWq76/YkCI+U59U2rJM6UhRNPizG +/VGrhAahSuZkFkWhwKvg9QxBjixljRwOrFsJW5lmkzX26hnsyv0czE4O+nxtP4Ug +BMQ/QdrwQmjMNp95rYXhbCeirjuc3O3G58uHAgjx+UIopE4SNmRgqQb0tXM2DEN9 +8Ppx1aklcyBt+17qh2ToTkZuW6KK39eYXZi8ZtYl0C908vJ7RUQGlL+wwlvVswCv +KuQOXWl4Kv1oVKf8k/0lqtZh7LQwTGUPspndGyBxuN5f9UFvOxLpy5k6yA/J7Idp +k/0nzQei++UltQ//LiyMSb/JpA7GAEE68HoOhDzsablNoLaiIPrUfo7SLFUw7n/H +m2CiSF21KBEPuq5waXpe8aeYtbCcXjCZI34Swkt892mws01XY4AiJ5xUizv+gcJD +hADngf/XcioXAoICAQDH89BEG12CFyD+RCubF2sdP/DFB3fvkAvGjPMrTpVkvvkd +HOP1+0JWWuIQUq6VQ8bMpUn1L9ks0vFv1lk87OMZ5Jmeuv/yo/ur7ZwgDAwwbiV5 +VxoulppCcsNyn6VKu7NEvvmDEvKbTQMiMAwhVS4vCdaKRpfrpNB7g2kzL2sKkwwg +9K5ilO3NboQKveIjhmzHzgQWKKH/Jh+9Wjd4hVk88JtqOzWBcfZl1hZFKAEgduWH +fw66nsk1keYlojo5WWR2gREMz44lUAi7iGSjR134C/l/xHs1d6nVEvk9GFx0fS+E +gWGMzOS7G8Ft4LTzA26YO75sYlOaUmFOptvrBm3nmjXq8BTzo9S6NWNUT5UwF6Po +k9S2s4BywA2PxXsCm2Nd+yOZ/he/qT3jW7+RGi7LXAW6fEDb8TxuvYSq/QHwLrUV +/814m5B5C19LCObviZ2pL4xw6bOF4I6QeHPHgTIicG4LbudiDpIcWl5KWCo94fei +AN5Z7IeTYWJ6Gf49lxn7AiXP9acQG6ohk3byW5mJYkHY5chbiW5gmpOHwzWrfw8T +UEMAbGOVDqj1L2thOH1KxMHH03Ybzb0xiAXvcd261Li2K/52QgXJ9goEdw6XdTPV +T8MOYMRj2r696mdMDLjA6TaPv0LwxP1DOr5UAaCFijRoNTIsAnlZwrT/QOQXuwKC +AgEAxqtiF3izFa9Q+36KSIQHc/GJK7/bXyE9QhYR5aGV7BzJ+kC0mBVCtfuCx0Ga +D//ykbM/pxmsmjwVWk+mi14n6xOX3jKaMAenaR94Gt5CjHpLIB+VYV/vKj4co+z+ +jvvcl7+X/7Lq3ne4ckbS1PRrZvVldKJbAHbaXNPK1KQBRCLevL0SlN4FpnzRT2nv +/wtUkGIHPW+tsPJ+IimurLuvw2xBtlFj8AwvX8/SRc6epxbNQ4+QOF+evBjwjQtU +9r4roFMJJZkXA+kFBiZNlZ798d5Ap21hS3AFvpPNiWST2EXSpQOW44vqlRiT8c9U +4DZdLEOczzGLdHLIv5qk0qK/n7qfEAWUX5RmZU0z7u0g+unU8hdKXMMSUjKU+93J +8AafYfP8B8wZqDt3UA4NxtTvbVIx6W7JaT4cnGnPLz+AnFTpXVL2t3HpUdpiwD5O +CVL5SlbS3W2DProdW9+TGzNKzrL28hEOgOOOfqpKh2c9/nJ5+eMwpQp8lgnOnJ1c +rdD3q74U1zxKkvyDxNJobjmMkWeE/JACozJHbPXD0NIBUMgStsyusLn414vxtXxt +dIdA3lwyTmZRJ1F/gaR6Nftt5cN8m//svxBTqnEVbLNRZx4KKx88/aiyi/E7sadI +1JiIA75xHNAQLUYn1sY3tsu/9QY3lwBsFaR5uzG0aspxWaMCggIBAM10S3/g8Zbi +DGX70XlgNWBQMtlKhQWK64I3VdP79pJ8LZu2mPnOmwV6oa+MAW2pKWJ96ZuARj1k +jOSzi5022qfBQOx2zD1jDOWZa2FMz7gyJWiLKSBG3b0UVWa+2OxxIkIK2pEWYURX +qvbVy/6Kh34xupzHYAvyYraac2NKIpZHmxVxYZjazm7Ot6bn7oXG09D11oXHq4/r +u9hGCkOVD9pKu7it/8IQMyNNCm8Sw8ShYLrA6PYtGOqV6ByuUp7EgcJzbPNGHlXX +p7fwIsxInWhZXqFz8ARji+za4G65vr+tjQzBMGL6V/QWNzM8CRgXQJwjr50bk79t +U64t/I/bHTnQiEUMqkdE4ly1A3QxttUaCN6s63Rj+Pfy7cJZZOehSFn0YAH16BYH +NUrjQxhKy3U5UdWGxp9V8wZ0YW9ThGJM8g/n3PZjhQK/LilwUkDk4eGwMspyh09z +3Azsfvl+nsiNBu3ft3dLggX729cWKkGg1Kdv7OTcmxLTipkYESXH708SsGlJhqSq +YW9sl2Ankve2apwrLTLQUE06o4KF5F9KP2MC3uXiwYAmoHuyYMnZ9fcfH2P1zbQL +czHlpnSg1/TDbJfAU9E9LauPuha63KrqFaPNOpjDFNyotPxbe+Oy35B0UX5ZVamN +Jk2UczPy2rNK8CZ6iIuChYZ5f3gujYHb +-----END ----- diff --git a/network/peer/tls_config.go b/network/peer/tls_config.go index 9673b98dc8f1..bce5aa1e7d95 100644 --- a/network/peer/tls_config.go +++ b/network/peer/tls_config.go @@ -4,8 +4,22 @@ package peer import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" "crypto/tls" + "errors" "io" + + "github.com/ava-labs/avalanchego/staking" +) + +var ( + ErrNoCertsSent = errors.New("no certificates sent by peer") + ErrEmptyCert = errors.New("certificate sent by peer is empty") + ErrEmptyPublicKey = errors.New("no public key sent by peer") + ErrCurveMismatch = errors.New("only P256 is allowed for ECDSA") + ErrUnsupportedKeyType = errors.New("key type is not supported") ) // TLSConfig returns the TLS config that will allow secure connections to other @@ -26,5 +40,34 @@ func TLSConfig(cert tls.Certificate, keyLogWriter io.Writer) *tls.Config { InsecureSkipVerify: true, //#nosec G402 MinVersion: tls.VersionTLS13, KeyLogWriter: keyLogWriter, + VerifyConnection: ValidateCertificate, + } +} + +// ValidateCertificate validates TLS certificates according their public keys on the leaf certificate in the certification chain. +func ValidateCertificate(cs tls.ConnectionState) error { + if len(cs.PeerCertificates) == 0 { + return ErrNoCertsSent + } + + if cs.PeerCertificates[0] == nil { + return ErrEmptyCert + } + + pk := cs.PeerCertificates[0].PublicKey + + switch key := pk.(type) { + case *ecdsa.PublicKey: + if key == nil { + return ErrEmptyPublicKey + } + if key.Curve != elliptic.P256() { + return ErrCurveMismatch + } + return nil + case *rsa.PublicKey: + return staking.ValidateRSAPublicKeyIsWellFormed(key) + default: + return ErrUnsupportedKeyType } } diff --git a/network/peer/tls_config_test.go b/network/peer/tls_config_test.go new file mode 100644 index 000000000000..41cf6a4dca8d --- /dev/null +++ b/network/peer/tls_config_test.go @@ -0,0 +1,134 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package peer_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ed25519" + + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/staking" +) + +func TestValidateCertificate(t *testing.T) { + for _, testCase := range []struct { + description string + input func(t *testing.T) tls.ConnectionState + expectedErr error + }{ + { + description: "Valid TLS cert", + input: func(t *testing.T) tls.ConnectionState { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + x509Cert := makeCert(t, key, &key.PublicKey) + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{x509Cert}} + }, + }, + { + description: "No TLS certs given", + input: func(*testing.T) tls.ConnectionState { + return tls.ConnectionState{} + }, + expectedErr: peer.ErrNoCertsSent, + }, + { + description: "Empty certificate given by peer", + input: func(*testing.T) tls.ConnectionState { + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{nil}} + }, + expectedErr: peer.ErrEmptyCert, + }, + { + description: "nil RSA key", + input: func(t *testing.T) tls.ConnectionState { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + x509CertWithNilPK := makeCert(t, key, &key.PublicKey) + x509CertWithNilPK.PublicKey = (*rsa.PublicKey)(nil) + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{x509CertWithNilPK}} + }, + expectedErr: staking.ErrInvalidRSAPublicKey, + }, + { + description: "No public key in the cert given", + input: func(t *testing.T) tls.ConnectionState { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + x509CertWithNoPK := makeCert(t, key, &key.PublicKey) + x509CertWithNoPK.PublicKey = nil + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{x509CertWithNoPK}} + }, + expectedErr: peer.ErrUnsupportedKeyType, + }, + { + description: "EC cert", + input: func(t *testing.T) tls.ConnectionState { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + ecCert := makeCert(t, ecKey, &ecKey.PublicKey) + + require.NoError(t, err) + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{ecCert}} + }, + }, + { + description: "EC cert with empty key", + expectedErr: peer.ErrEmptyPublicKey, + input: func(t *testing.T) tls.ConnectionState { + ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + ecCert := makeCert(t, ecKey, &ecKey.PublicKey) + ecCert.PublicKey = (*ecdsa.PublicKey)(nil) + + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{ecCert}} + }, + }, + { + description: "EC cert with P384 curve", + expectedErr: peer.ErrCurveMismatch, + input: func(t *testing.T) tls.ConnectionState { + ecKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + + ecCert := makeCert(t, ecKey, &ecKey.PublicKey) + + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{ecCert}} + }, + }, + { + description: "EC cert with ed25519 key not supported", + expectedErr: peer.ErrUnsupportedKeyType, + input: func(t *testing.T) tls.ConnectionState { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + basicCert := basicCert() + certBytes, err := x509.CreateCertificate(rand.Reader, basicCert, basicCert, pub, priv) + require.NoError(t, err) + + ecCert, err := x509.ParseCertificate(certBytes) + require.NoError(t, err) + + return tls.ConnectionState{PeerCertificates: []*x509.Certificate{ecCert}} + }, + }, + } { + t.Run(testCase.description, func(t *testing.T) { + require.Equal(t, testCase.expectedErr, peer.ValidateCertificate(testCase.input(t))) + }) + } +} diff --git a/network/peer/upgrader_test.go b/network/peer/upgrader_test.go new file mode 100644 index 000000000000..48bc8e192cd2 --- /dev/null +++ b/network/peer/upgrader_test.go @@ -0,0 +1,212 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package peer_test + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "math/big" + "net" + "sync" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/require" + + _ "embed" + + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/staking" +) + +// 8192RSA_test.pem is used here because it's too expensive +// to generate an 8K bit RSA key under the time constraint of the weak Github CI runners. + +//go:embed 8192RSA_test.pem +var fat8192BitRSAKey []byte + +func TestBlockClientsWithIncorrectRSAKeys(t *testing.T) { + for _, testCase := range []struct { + description string + genClientTLSCert func() tls.Certificate + expectedErr error + }{ + { + description: "Proper key size and private key - 2048", + genClientTLSCert: func() tls.Certificate { + privKey2048, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + clientCert2048 := makeTLSCert(t, privKey2048) + return clientCert2048 + }, + }, + { + description: "Proper key size and private key - 4096", + genClientTLSCert: func() tls.Certificate { + privKey4096, err := rsa.GenerateKey(rand.Reader, 4096) + require.NoError(t, err) + clientCert4096 := makeTLSCert(t, privKey4096) + return clientCert4096 + }, + }, + { + description: "Too big key", + genClientTLSCert: func() tls.Certificate { + block, _ := pem.Decode(fat8192BitRSAKey) + require.NotNil(t, block) + rsaFatKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + require.NoError(t, err) + privKey8192 := rsaFatKey.(*rsa.PrivateKey) + // Sanity check - ensure privKey8192 is indeed an 8192 RSA key + require.Equal(t, 8192, privKey8192.N.BitLen()) + clientCert8192 := makeTLSCert(t, privKey8192) + return clientCert8192 + }, + expectedErr: staking.ErrUnsupportedRSAModulusBitLen, + }, + { + description: "Improper public exponent", + genClientTLSCert: func() tls.Certificate { + clientCertBad := makeTLSCert(t, nonStandardRSAKey(t)) + return clientCertBad + }, + expectedErr: staking.ErrUnsupportedRSAPublicExponent, + }, + } { + t.Run(testCase.description, func(t *testing.T) { + serverKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + serverCert := makeTLSCert(t, serverKey) + + config := peer.TLSConfig(serverCert, nil) + + c := prometheus.NewCounter(prometheus.CounterOpts{}) + + // Initialize upgrader with a mock that fails when it's incremented. + failOnIncrementCounter := &mockPrometheusCounter{ + Counter: c, + onIncrement: func() { + require.FailNow(t, "should not have invoked") + }, + } + upgrader := peer.NewTLSServerUpgrader(config, failOnIncrementCounter) + + clientConfig := tls.Config{ + ClientAuth: tls.RequireAnyClientCert, + InsecureSkipVerify: true, //#nosec G402 + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{testCase.genClientTLSCert()}, + } + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + conn, err := listener.Accept() + require.NoError(t, err) + + _, _, _, err = upgrader.Upgrade(conn) + + require.ErrorIs(t, err, testCase.expectedErr) + }() + + conn, err := tls.Dial("tcp", listener.Addr().String(), &clientConfig) + require.NoError(t, err) + + require.NoError(t, conn.Handshake()) + + wg.Wait() + }) + } +} + +func nonStandardRSAKey(t *testing.T) *rsa.PrivateKey { + for { + sk, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // This speeds up RSA operations, and was initialized during the key-gen. + // If we wish to override the key parameters we need to nullify this, + // otherwise the signer will use these values and the verifier will use + // the values we override, and verification will fail. + sk.Precomputed = rsa.PrecomputedValues{} + + // We want a non-standard E, so let's use E = 257 and derive D again. + e := 257 + sk.PublicKey.E = e + sk.E = e + + p := sk.Primes[0] + q := sk.Primes[1] + + pminus1 := new(big.Int).Sub(p, big.NewInt(1)) + qminus1 := new(big.Int).Sub(q, big.NewInt(1)) + + phiN := big.NewInt(0).Mul(pminus1, qminus1) + + sk.D = big.NewInt(0).ModInverse(big.NewInt(int64(e)), phiN) + + if sk.D == nil { + // If we ended up picking a bad starting modulus, try again. + continue + } + + return sk + } +} + +func makeTLSCert(t *testing.T, privKey *rsa.PrivateKey) tls.Certificate { + x509Cert := makeCert(t, privKey, &privKey.PublicKey) + + rawX509PEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: x509Cert.Raw}) + privateKeyInDER, err := x509.MarshalPKCS8PrivateKey(privKey) + require.NoError(t, err) + + privateKeyInPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyInDER}) + + tlsCertServer, err := tls.X509KeyPair(rawX509PEM, privateKeyInPEM) + require.NoError(t, err) + + return tlsCertServer +} + +func makeCert(t *testing.T, privateKey any, publicKey any) *x509.Certificate { + // Create a self-signed cert + basicCert := basicCert() + certBytes, err := x509.CreateCertificate(rand.Reader, basicCert, basicCert, publicKey, privateKey) + require.NoError(t, err) + + cert, err := x509.ParseCertificate(certBytes) + require.NoError(t, err) + + return cert +} + +func basicCert() *x509.Certificate { + return &x509.Certificate{ + SerialNumber: big.NewInt(0).SetInt64(100), + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour).UTC(), + BasicConstraintsValid: true, + } +} + +type mockPrometheusCounter struct { + prometheus.Counter + onIncrement func() +} + +func (m *mockPrometheusCounter) Inc() { + m.onIncrement() +} diff --git a/staking/parse.go b/staking/parse.go index 28ae02cbc644..6a544ea6332d 100644 --- a/staking/parse.go +++ b/staking/parse.go @@ -136,18 +136,8 @@ func parsePublicKey(oid asn1.ObjectIdentifier, publicKey asn1.BitString) (crypto return nil, ErrInvalidRSAPublicExponent } - if pub.N.Sign() <= 0 { - return nil, ErrRSAModulusNotPositive - } - - if bitLen := pub.N.BitLen(); bitLen != allowedRSALargeModulusLen && bitLen != allowedRSASmallModulusLen { - return nil, fmt.Errorf("%w: %d", ErrUnsupportedRSAModulusBitLen, bitLen) - } - if pub.N.Bit(0) == 0 { - return nil, ErrRSAModulusIsEven - } - if pub.E != allowedRSAPublicExponentValue { - return nil, fmt.Errorf("%w: %d", ErrUnsupportedRSAPublicExponent, pub.E) + if err := ValidateRSAPublicKeyIsWellFormed(pub); err != nil { + return nil, err } return pub, nil case oid.Equal(oidPublicKeyECDSA): @@ -165,3 +155,23 @@ func parsePublicKey(oid asn1.ObjectIdentifier, publicKey asn1.BitString) (crypto return nil, ErrUnknownPublicKeyAlgorithm } } + +// ValidateRSAPublicKeyIsWellFormed validates the given RSA public key +func ValidateRSAPublicKeyIsWellFormed(pub *rsa.PublicKey) error { + if pub == nil { + return ErrInvalidRSAPublicKey + } + if pub.N.Sign() <= 0 { + return ErrRSAModulusNotPositive + } + if bitLen := pub.N.BitLen(); bitLen != allowedRSALargeModulusLen && bitLen != allowedRSASmallModulusLen { + return fmt.Errorf("%w: %d", ErrUnsupportedRSAModulusBitLen, bitLen) + } + if pub.N.Bit(0) == 0 { + return ErrRSAModulusIsEven + } + if pub.E != allowedRSAPublicExponentValue { + return fmt.Errorf("%w: %d", ErrUnsupportedRSAPublicExponent, pub.E) + } + return nil +} diff --git a/staking/parse_test.go b/staking/parse_test.go index 41704e3b71dc..b0b032b1f6ea 100644 --- a/staking/parse_test.go +++ b/staking/parse_test.go @@ -4,6 +4,9 @@ package staking import ( + "crypto/rand" + "crypto/rsa" + "math/big" "testing" "github.com/stretchr/testify/require" @@ -31,3 +34,73 @@ func BenchmarkParse(b *testing.B) { require.NoError(b, err) } } + +func TestValidateRSAPublicKeyIsWellFormed(t *testing.T) { + validKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + for _, testCase := range []struct { + description string + expectErr error + getPK func() rsa.PublicKey + }{ + { + description: "valid public key", + getPK: func() rsa.PublicKey { + return validKey.PublicKey + }, + }, + { + description: "even modulus", + expectErr: ErrRSAModulusIsEven, + getPK: func() rsa.PublicKey { + evenN := new(big.Int).Set(validKey.N) + evenN.Add(evenN, big.NewInt(1)) + return rsa.PublicKey{ + N: evenN, + E: 65537, + } + }, + }, + { + description: "unsupported exponent", + expectErr: ErrUnsupportedRSAPublicExponent, + getPK: func() rsa.PublicKey { + return rsa.PublicKey{ + N: validKey.N, + E: 3, + } + }, + }, + { + description: "unsupported modulus bit len", + expectErr: ErrUnsupportedRSAModulusBitLen, + getPK: func() rsa.PublicKey { + bigMod := new(big.Int).Set(validKey.N) + bigMod.Lsh(bigMod, 2049) + return rsa.PublicKey{ + N: bigMod, + E: 65537, + } + }, + }, + { + description: "not positive modulus", + expectErr: ErrRSAModulusNotPositive, + getPK: func() rsa.PublicKey { + minusN := new(big.Int).Set(validKey.N) + minusN.Neg(minusN) + return rsa.PublicKey{ + N: minusN, + E: 65537, + } + }, + }, + } { + t.Run(testCase.description, func(t *testing.T) { + pk := testCase.getPK() + err := ValidateRSAPublicKeyIsWellFormed(&pk) + require.ErrorIs(t, err, testCase.expectErr) + }) + } +} From 9e0d7d5efe5ef7fc801b7a747d08123a515f37a4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 11:03:00 -0400 Subject: [PATCH 048/184] Add caching --- vms/platformvm/config/execution_config.go | 54 +++++---- .../config/execution_config_test.go | 25 ++-- vms/platformvm/state/state.go | 107 +++++++++++++++--- 3 files changed, 137 insertions(+), 49 deletions(-) diff --git a/vms/platformvm/config/execution_config.go b/vms/platformvm/config/execution_config.go index e5bef1637d05..2c63c6299e58 100644 --- a/vms/platformvm/config/execution_config.go +++ b/vms/platformvm/config/execution_config.go @@ -12,34 +12,40 @@ import ( ) var DefaultExecutionConfig = ExecutionConfig{ - Network: network.DefaultConfig, - BlockCacheSize: 64 * units.MiB, - TxCacheSize: 128 * units.MiB, - TransformedSubnetTxCacheSize: 4 * units.MiB, - RewardUTXOsCacheSize: 2048, - ChainCacheSize: 2048, - ChainDBCacheSize: 2048, - BlockIDCacheSize: 8192, - FxOwnerCacheSize: 4 * units.MiB, - SubnetConversionCacheSize: 4 * units.MiB, - ChecksumsEnabled: false, - MempoolPruneFrequency: 30 * time.Minute, + Network: network.DefaultConfig, + BlockCacheSize: 64 * units.MiB, + TxCacheSize: 128 * units.MiB, + TransformedSubnetTxCacheSize: 4 * units.MiB, + RewardUTXOsCacheSize: 2048, + ChainCacheSize: 2048, + ChainDBCacheSize: 2048, + BlockIDCacheSize: 8192, + FxOwnerCacheSize: 4 * units.MiB, + SubnetConversionCacheSize: 4 * units.MiB, + L1WeightsCacheSize: 16 * units.KiB, + L1InactiveValidatorsCacheSize: 256 * units.KiB, + L1SubnetIDNodeIDCacheSize: 16 * units.KiB, + ChecksumsEnabled: false, + MempoolPruneFrequency: 30 * time.Minute, } // ExecutionConfig provides execution parameters of PlatformVM type ExecutionConfig struct { - Network network.Config `json:"network"` - BlockCacheSize int `json:"block-cache-size"` - TxCacheSize int `json:"tx-cache-size"` - TransformedSubnetTxCacheSize int `json:"transformed-subnet-tx-cache-size"` - RewardUTXOsCacheSize int `json:"reward-utxos-cache-size"` - ChainCacheSize int `json:"chain-cache-size"` - ChainDBCacheSize int `json:"chain-db-cache-size"` - BlockIDCacheSize int `json:"block-id-cache-size"` - FxOwnerCacheSize int `json:"fx-owner-cache-size"` - SubnetConversionCacheSize int `json:"subnet-conversion-cache-size"` - ChecksumsEnabled bool `json:"checksums-enabled"` - MempoolPruneFrequency time.Duration `json:"mempool-prune-frequency"` + Network network.Config `json:"network"` + BlockCacheSize int `json:"block-cache-size"` + TxCacheSize int `json:"tx-cache-size"` + TransformedSubnetTxCacheSize int `json:"transformed-subnet-tx-cache-size"` + RewardUTXOsCacheSize int `json:"reward-utxos-cache-size"` + ChainCacheSize int `json:"chain-cache-size"` + ChainDBCacheSize int `json:"chain-db-cache-size"` + BlockIDCacheSize int `json:"block-id-cache-size"` + FxOwnerCacheSize int `json:"fx-owner-cache-size"` + SubnetConversionCacheSize int `json:"subnet-conversion-cache-size"` + L1WeightsCacheSize int `json:"l1-weights-cache-size"` + L1InactiveValidatorsCacheSize int `json:"l1-inactive-validators-cache-size"` + L1SubnetIDNodeIDCacheSize int `json:"l1-subnet-id-node-id-cache-size"` + ChecksumsEnabled bool `json:"checksums-enabled"` + MempoolPruneFrequency time.Duration `json:"mempool-prune-frequency"` } // GetExecutionConfig returns an ExecutionConfig diff --git a/vms/platformvm/config/execution_config_test.go b/vms/platformvm/config/execution_config_test.go index c938c177add3..f4b077689b23 100644 --- a/vms/platformvm/config/execution_config_test.go +++ b/vms/platformvm/config/execution_config_test.go @@ -81,17 +81,20 @@ func TestExecutionConfigUnmarshal(t *testing.T) { ExpectedBloomFilterFalsePositiveProbability: 16, MaxBloomFilterFalsePositiveProbability: 17, }, - BlockCacheSize: 1, - TxCacheSize: 2, - TransformedSubnetTxCacheSize: 3, - RewardUTXOsCacheSize: 5, - ChainCacheSize: 6, - ChainDBCacheSize: 7, - BlockIDCacheSize: 8, - FxOwnerCacheSize: 9, - SubnetConversionCacheSize: 10, - ChecksumsEnabled: true, - MempoolPruneFrequency: time.Minute, + BlockCacheSize: 1, + TxCacheSize: 2, + TransformedSubnetTxCacheSize: 3, + RewardUTXOsCacheSize: 5, + ChainCacheSize: 6, + ChainDBCacheSize: 7, + BlockIDCacheSize: 8, + FxOwnerCacheSize: 9, + SubnetConversionCacheSize: 10, + L1WeightsCacheSize: 11, + L1InactiveValidatorsCacheSize: 12, + L1SubnetIDNodeIDCacheSize: 13, + ChecksumsEnabled: true, + MempoolPruneFrequency: time.Minute, } verifyInitializedStruct(t, *expected) verifyInitializedStruct(t, expected.Network) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index cbdda6537a53..b059eeb4595e 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -34,6 +34,7 @@ import ( "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/utils/timer" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -86,7 +87,7 @@ var ( SupplyPrefix = []byte("supply") ChainPrefix = []byte("chain") ExpiryReplayProtectionPrefix = []byte("expiryReplayProtection") - SubnetOnlyValidatorsPrefix = []byte("subnetOnlyValidators") + SubnetOnlyPrefix = []byte("subnetOnly") WeightsPrefix = []byte("weights") SubnetIDNodeIDPrefix = []byte("subnetIDNodeID") ActivePrefix = []byte("active") @@ -273,6 +274,15 @@ type stateBlk struct { * | | '-. subnetDelegator * | | '-. list * | | '-- txID -> nil + * | |-. subnetOnly + * | | |-. weights + * | | | '-- subnetID -> weight + * | | |-. subnetIDNodeID + * | | | '-- subnetID+nodeID -> validationID + * | | |-. active + * | | | '-- validationID -> subnetOnlyValidator + * | | '-. inactive + * | | '-- validationID -> subnetOnlyValidator * | |-. weight diffs * | | '-- subnet+height+nodeID -> weightChange * | '-. pub key diffs @@ -332,9 +342,12 @@ type state struct { activeSOVs *activeSubnetOnlyValidators sovDiff *subnetOnlyValidatorsDiff subnetOnlyValidatorsDB database.Database + weightsCache cache.Cacher[ids.ID, uint64] // subnetID -> total SoV weight weightsDB database.Database + subnetIDNodeIDCache cache.Cacher[subnetIDNodeID, bool] // subnetID+nodeID -> is validator subnetIDNodeIDDB database.Database activeDB database.Database + inactiveCache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]] // validationID -> SubnetOnlyValidator inactiveDB database.Database currentStakers *baseStakers @@ -528,8 +541,6 @@ func New( baseDB := versiondb.New(db) - subnetOnlyValidatorsDB := prefixdb.New(SubnetOnlyValidatorsPrefix, baseDB) - validatorsDB := prefixdb.New(ValidatorsPrefix, baseDB) currentValidatorsDB := prefixdb.New(CurrentPrefix, validatorsDB) @@ -544,9 +555,55 @@ func New( pendingSubnetValidatorBaseDB := prefixdb.New(SubnetValidatorPrefix, pendingValidatorsDB) pendingSubnetDelegatorBaseDB := prefixdb.New(SubnetDelegatorPrefix, pendingValidatorsDB) + subnetOnlyValidatorsDB := prefixdb.New(SubnetOnlyPrefix, validatorsDB) + validatorWeightDiffsDB := prefixdb.New(ValidatorWeightDiffsPrefix, validatorsDB) validatorPublicKeyDiffsDB := prefixdb.New(ValidatorPublicKeyDiffsPrefix, validatorsDB) + weightsCache, err := metercacher.New( + "sov_weights_cache", + metricsReg, + cache.NewSizedLRU[ids.ID, uint64](execCfg.L1WeightsCacheSize, func(ids.ID, uint64) int { + return ids.IDLen + wrappers.LongLen + }), + ) + if err != nil { + return nil, err + } + + inactiveSOVsCache, err := metercacher.New( + "sov_inactive_cache", + metricsReg, + cache.NewSizedLRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]( + execCfg.L1InactiveValidatorsCacheSize, + func(_ ids.ID, maybeSOV maybe.Maybe[SubnetOnlyValidator]) int { + const sovOverhead = ids.IDLen + ids.NodeIDLen + 4*wrappers.LongLen + 3*constants.PointerOverhead + const maybeSOVOverhead = wrappers.BoolLen + sovOverhead + const overhead = ids.IDLen + maybeSOVOverhead + if maybeSOV.IsNothing() { + return overhead + } + + sov := maybeSOV.Value() + return overhead + len(sov.PublicKey) + len(sov.RemainingBalanceOwner) + len(sov.DeactivationOwner) + }, + ), + ) + if err != nil { + return nil, err + } + + subnetIDNodeIDCache, err := metercacher.New( + "sov_subnet_id_node_id_cache", + metricsReg, + cache.NewSizedLRU[subnetIDNodeID, bool](execCfg.L1SubnetIDNodeIDCacheSize, func(subnetIDNodeID, bool) int { + return ids.IDLen + ids.NodeIDLen + wrappers.BoolLen + }), + ) + if err != nil { + return nil, err + } + txCache, err := metercacher.New( "tx_cache", metricsReg, @@ -659,9 +716,12 @@ func New( activeSOVs: newActiveSubnetOnlyValidators(), sovDiff: newSubnetOnlyValidatorsDiff(), subnetOnlyValidatorsDB: subnetOnlyValidatorsDB, + weightsCache: weightsCache, weightsDB: prefixdb.New(WeightsPrefix, subnetOnlyValidatorsDB), + subnetIDNodeIDCache: subnetIDNodeIDCache, subnetIDNodeIDDB: prefixdb.New(SubnetIDNodeIDPrefix, subnetOnlyValidatorsDB), activeDB: prefixdb.New(ActivePrefix, subnetOnlyValidatorsDB), + inactiveCache: inactiveSOVsCache, inactiveDB: prefixdb.New(InactivePrefix, subnetOnlyValidatorsDB), currentStakers: newBaseStakers(), @@ -774,7 +834,10 @@ func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { return weight, nil } - // TODO: Add caching + if weight, ok := s.weightsCache.Get(subnetID); ok { + return weight, nil + } + return database.WithDefault(database.GetUInt64, s.weightsDB, subnetID[:], 0) } @@ -797,7 +860,13 @@ func (s *state) getPersistedSubnetOnlyValidator(validationID ids.ID) (SubnetOnly return sov, nil } - // TODO: Add caching + if maybeSOV, ok := s.inactiveCache.Get(validationID); ok { + if maybeSOV.IsNothing() { + return SubnetOnlyValidator{}, database.ErrNotFound + } + return maybeSOV.Value(), nil + } + return getSubnetOnlyValidator(s.inactiveDB, validationID) } @@ -810,9 +879,11 @@ func (s *state) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool subnetID: subnetID, nodeID: nodeID, } - key := subnetIDNodeID.Marshal() + if has, ok := s.subnetIDNodeIDCache.Get(subnetIDNodeID); ok { + return has, nil + } - // TODO: Add caching + key := subnetIDNodeID.Marshal() return s.subnetIDNodeIDDB.Has(key) } @@ -2638,6 +2709,8 @@ func (s *state) writeSubnetOnlyValidators() error { if err != nil { return err } + + s.weightsCache.Put(subnetID, weight) } for validationID, sov := range s.sovDiff.modified { @@ -2646,6 +2719,7 @@ func (s *state) writeSubnetOnlyValidators() error { if s.activeSOVs.delete(validationID) { err = deleteSubnetOnlyValidator(s.activeDB, validationID) } else { + s.inactiveCache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]()) err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) } if err != nil { @@ -2653,12 +2727,15 @@ func (s *state) writeSubnetOnlyValidators() error { } // Update the subnetIDNodeID mapping - subnetIDNodeID := subnetIDNodeID{ - subnetID: sov.SubnetID, - nodeID: sov.NodeID, - } - subnetIDNodeIDKey := subnetIDNodeID.Marshal() - if sov.isDeleted() { + var ( + isDeleted = sov.isDeleted() + subnetIDNodeID = subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + subnetIDNodeIDKey = subnetIDNodeID.Marshal() + ) + if isDeleted { err = s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey) } else { err = s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]) @@ -2667,7 +2744,8 @@ func (s *state) writeSubnetOnlyValidators() error { return err } - if sov.isDeleted() { + s.subnetIDNodeIDCache.Put(subnetIDNodeID, !isDeleted) + if isDeleted { continue } @@ -2676,6 +2754,7 @@ func (s *state) writeSubnetOnlyValidators() error { s.activeSOVs.put(sov) err = putSubnetOnlyValidator(s.activeDB, sov) } else { + s.inactiveCache.Put(validationID, maybe.Some(sov)) err = putSubnetOnlyValidator(s.inactiveDB, sov) } if err != nil { From 9993b05f81e521e1b9ea6185eb66024e4e47e6e1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 11:07:01 -0400 Subject: [PATCH 049/184] nit --- vms/platformvm/state/state.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index b059eeb4595e..1abc1e227b1e 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -577,9 +577,11 @@ func New( cache.NewSizedLRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]( execCfg.L1InactiveValidatorsCacheSize, func(_ ids.ID, maybeSOV maybe.Maybe[SubnetOnlyValidator]) int { - const sovOverhead = ids.IDLen + ids.NodeIDLen + 4*wrappers.LongLen + 3*constants.PointerOverhead - const maybeSOVOverhead = wrappers.BoolLen + sovOverhead - const overhead = ids.IDLen + maybeSOVOverhead + const ( + sovOverhead = ids.IDLen + ids.NodeIDLen + 4*wrappers.LongLen + 3*constants.PointerOverhead + maybeSOVOverhead = wrappers.BoolLen + sovOverhead + overhead = ids.IDLen + maybeSOVOverhead + ) if maybeSOV.IsNothing() { return overhead } From 547d426c08f399ba2f2a4534aac5bda8de67e301 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 11:07:32 -0400 Subject: [PATCH 050/184] nit --- vms/platformvm/state/state.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 1abc1e227b1e..cdb4f9c59483 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -580,14 +580,14 @@ func New( const ( sovOverhead = ids.IDLen + ids.NodeIDLen + 4*wrappers.LongLen + 3*constants.PointerOverhead maybeSOVOverhead = wrappers.BoolLen + sovOverhead - overhead = ids.IDLen + maybeSOVOverhead + entryOverhead = ids.IDLen + maybeSOVOverhead ) if maybeSOV.IsNothing() { - return overhead + return entryOverhead } sov := maybeSOV.Value() - return overhead + len(sov.PublicKey) + len(sov.RemainingBalanceOwner) + len(sov.DeactivationOwner) + return entryOverhead + len(sov.PublicKey) + len(sov.RemainingBalanceOwner) + len(sov.DeactivationOwner) }, ), ) From 6bcc0ead771e14c0d7108fdc3728bf040d675da9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 11:56:42 -0400 Subject: [PATCH 051/184] Add TODOs --- vms/platformvm/state/state.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index cdb4f9c59483..14c3f5ff0f7d 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1941,12 +1941,14 @@ func (s *state) initValidatorSets() error { return errValidatorSetAlreadyPopulated } - // Load ACP77 validators + // Load active ACP-77 validators if err := s.activeSOVs.addStakers(s.validators); err != nil { return err } - // Load inactive weights + // Load inactive ACP-77 validator weights + // + // TODO: L1s with no active weight should not be held in memory. it := s.weightsDB.NewIterator() defer it.Release() @@ -2309,6 +2311,8 @@ func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) // // This function must be called prior to writeCurrentStakers and // writeSubnetOnlyValidators. +// +// TODO: L1s with no active weight should not be held in memory. func (s *state) updateValidatorManager(updateValidators bool) error { if !updateValidators { return nil From a7792c738b496250e92f619bca306d6f87b7025c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 12:21:48 -0400 Subject: [PATCH 052/184] Add config changes to readme --- RELEASES.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index 16b371b67a12..d5c4b453189e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,14 @@ # Release Notes +## Pending Release + +### Configs + +- Added P-chain configs + - `"l1-weights-cache-size"` + - `"l1-inactive-validators-cache-size"` + - `"l1-subnet-id-node-id-cache-size"` + ## [v1.11.11](https://github.com/ava-labs/avalanchego/releases/tag/v1.11.11) This version is backwards compatible to [v1.11.0](https://github.com/ava-labs/avalanchego/releases/tag/v1.11.0). It is optional, but encouraged. From 4fccda345e38bb3de5c00fbe5daecebd37cfb439 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 12:40:50 -0400 Subject: [PATCH 053/184] ACP-77: Filter the inactive validator from block proposals and tx gossip (#3509) --- network/p2p/validators.go | 2 + network/p2p/validators_test.go | 25 +++++++++ vms/proposervm/proposer/windower.go | 2 + vms/proposervm/proposer/windower_test.go | 66 ++++++++++++++++-------- 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/network/p2p/validators.go b/network/p2p/validators.go index 161d84d88372..699d74e5e5f8 100644 --- a/network/p2p/validators.go +++ b/network/p2p/validators.go @@ -98,6 +98,8 @@ func (v *Validators) refresh(ctx context.Context) { return } + delete(validatorSet, ids.EmptyNodeID) // Ignore inactive ACP-77 validators. + for nodeID, vdr := range validatorSet { v.validatorList = append(v.validatorList, validator{ nodeID: nodeID, diff --git a/network/p2p/validators_test.go b/network/p2p/validators_test.go index a3753d0e180e..462d425e8b4d 100644 --- a/network/p2p/validators_test.go +++ b/network/p2p/validators_test.go @@ -296,6 +296,31 @@ func TestValidatorsTop(t *testing.T) { nodeID1, }, }, + { + name: "top ignores inactive validators", + validators: []validator{ + { + nodeID: ids.EmptyNodeID, + weight: 4, + }, + { + nodeID: nodeID1, + weight: 2, + }, + { + nodeID: nodeID2, + weight: 1, + }, + { + nodeID: nodeID3, + weight: 1, + }, + }, + percentage: .5, + expected: []ids.NodeID{ + nodeID1, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/vms/proposervm/proposer/windower.go b/vms/proposervm/proposer/windower.go index f0122fb13c7d..2074561eede6 100644 --- a/vms/proposervm/proposer/windower.go +++ b/vms/proposervm/proposer/windower.go @@ -238,6 +238,8 @@ func (w *windower) makeSampler( return nil, nil, err } + delete(validatorsMap, ids.EmptyNodeID) // Ignore inactive ACP-77 validators. + validators := make([]validatorData, 0, len(validatorsMap)) for k, v := range validatorsMap { validators = append(validators, validatorData{ diff --git a/vms/proposervm/proposer/windower_test.go b/vms/proposervm/proposer/windower_test.go index 2d90efaa5e3a..107f8c2c81f5 100644 --- a/vms/proposervm/proposer/windower_test.go +++ b/vms/proposervm/proposer/windower_test.go @@ -26,28 +26,49 @@ var ( ) func TestWindowerNoValidators(t *testing.T) { - require := require.New(t) - - _, vdrState := makeValidators(t, 0) - w := New(vdrState, subnetID, randomChainID) - - var ( - chainHeight uint64 = 1 - pChainHeight uint64 = 0 - nodeID = ids.GenerateTestNodeID() - slot uint64 = 1 - ) - delay, err := w.Delay(context.Background(), chainHeight, pChainHeight, nodeID, MaxVerifyWindows) - require.NoError(err) - require.Zero(delay) + tests := []struct { + name string + validators []ids.NodeID + }{ + { + name: "no validators", + }, + { + name: "only inactive validators", + validators: []ids.NodeID{ + ids.EmptyNodeID, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + w := New( + makeValidatorState(t, test.validators), + subnetID, + randomChainID, + ) + + var ( + chainHeight uint64 = 1 + pChainHeight uint64 = 0 + nodeID = ids.GenerateTestNodeID() + slot uint64 = 1 + ) + delay, err := w.Delay(context.Background(), chainHeight, pChainHeight, nodeID, MaxVerifyWindows) + require.NoError(err) + require.Zero(delay) - proposer, err := w.ExpectedProposer(context.Background(), chainHeight, pChainHeight, slot) - require.ErrorIs(err, ErrAnyoneCanPropose) - require.Equal(ids.EmptyNodeID, proposer) + proposer, err := w.ExpectedProposer(context.Background(), chainHeight, pChainHeight, slot) + require.ErrorIs(err, ErrAnyoneCanPropose) + require.Equal(ids.EmptyNodeID, proposer) - delay, err = w.MinDelayForProposer(context.Background(), chainHeight, pChainHeight, nodeID, slot) - require.ErrorIs(err, ErrAnyoneCanPropose) - require.Zero(delay) + delay, err = w.MinDelayForProposer(context.Background(), chainHeight, pChainHeight, nodeID, slot) + require.ErrorIs(err, ErrAnyoneCanPropose) + require.Zero(delay) + }) + } } func TestWindowerRepeatedValidator(t *testing.T) { @@ -450,7 +471,10 @@ func makeValidators(t testing.TB, count int) ([]ids.NodeID, *validatorstest.Stat for i := range validatorIDs { validatorIDs[i] = ids.BuildTestNodeID([]byte{byte(i) + 1}) } + return validatorIDs, makeValidatorState(t, validatorIDs) +} +func makeValidatorState(t testing.TB, validatorIDs []ids.NodeID) *validatorstest.State { vdrState := &validatorstest.State{ T: t, GetValidatorSetF: func(context.Context, uint64, ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) { @@ -464,5 +488,5 @@ func makeValidators(t testing.TB, count int) ([]ids.NodeID, *validatorstest.Stat return vdrs, nil }, } - return validatorIDs, vdrState + return vdrState } From 4723c46bdba39a88ce02d8253cbbe045df21f888 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 13:00:11 -0400 Subject: [PATCH 054/184] Improve doc for PutSubnetOnlyValidator --- vms/platformvm/state/subnet_only_validator.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index a299cd478c3e..642b081800f0 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -51,13 +51,21 @@ type SubnetOnlyValidators interface { // exists. HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) - // PutSubnetOnlyValidator inserts [sov] as a validator. + // PutSubnetOnlyValidator inserts [sov] as a validator. If the weight of the + // validator is 0, the validator is removed. // // If inserting this validator attempts to modify any of the constant fields // of the subnet only validator struct, an error will be returned. // - // If inserting this validator would cause the mapping of subnetID+nodeID to - // validationID to be non-unique, an error will be returned. + // If inserting this validator would cause the total weight of subnet only + // validators on a subnet to overflow MaxUint64, an error will be returned. + // + // If inserting this validator would cause there to be multiple validators + // with the same subnetID and nodeID pair to exist at the same time, an + // error will be returned. + // + // If an SoV with the same validationID as a previously removed SoV is + // added, the behavior is undefined. PutSubnetOnlyValidator(sov SubnetOnlyValidator) error } From f1ca6e6206e4dc01c7f0f2f5967aa06b7aad907e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 13:17:19 -0400 Subject: [PATCH 055/184] Add test that decreases weight --- vms/platformvm/state/state_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index df6e92c7b088..a1a8feb6676e 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1665,6 +1665,29 @@ func TestSubnetOnlyValidators(t *testing.T) { }, }, }, + { + name: "decrease active weight", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 2, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Decreased + EndAccumulatedFee: 1, // Active + }, + }, + }, { name: "deactivate", initial: []SubnetOnlyValidator{ From 71f88e87f9c0d78b47e5cd72125a5732eaedb53b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 13:42:16 -0400 Subject: [PATCH 056/184] Fix regression --- vms/platformvm/state/state.go | 111 ++++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 14c3f5ff0f7d..7f136ea49bef 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2364,40 +2364,58 @@ func (s *state) updateValidatorManager(updateValidators bool) error { } } + // Remove all deleted SoV validators. This must be done before adding new + // SoV validators to support the case where a validator is removed and then + // immediately re-added with a different validationID. for validationID, sov := range s.sovDiff.modified { + if !sov.isDeleted() { + continue + } + + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) + if err == database.ErrNotFound { + // Deleting a non-existent validator is a noop. This can happen if + // the validator was added and then immediately removed. + continue + } + if err != nil { + return err + } + + if err := s.validators.RemoveWeight(priorSOV.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight); err != nil { + return err + } + } + + // Now the removed SoV validators have been deleted, perform additions and + // modifications. + for validationID, sov := range s.sovDiff.modified { + if sov.isDeleted() { + continue + } + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) switch err { case nil: - if sov.isDeleted() { - // Removing a validator - err = s.validators.RemoveWeight(priorSOV.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight) - } else { - // Modifying a validator - if priorSOV.isActive() == sov.isActive() { - // This validator's active status isn't changing. This means - // the effectiveNodeIDs are equal. - nodeID := sov.effectiveNodeID() - if priorSOV.Weight < sov.Weight { - err = s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight-priorSOV.Weight) - } else if priorSOV.Weight > sov.Weight { - err = s.validators.RemoveWeight(sov.SubnetID, nodeID, priorSOV.Weight-sov.Weight) - } - } else { - // This validator's active status is changing. - err = errors.Join( - s.validators.RemoveWeight(sov.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight), - addSoVToValidatorManager(s.validators, sov), - ) + // Modifying an existing validator + if priorSOV.isActive() == sov.isActive() { + // This validator's active status isn't changing. This means + // the effectiveNodeIDs are equal. + nodeID := sov.effectiveNodeID() + if priorSOV.Weight < sov.Weight { + err = s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight-priorSOV.Weight) + } else if priorSOV.Weight > sov.Weight { + err = s.validators.RemoveWeight(sov.SubnetID, nodeID, priorSOV.Weight-sov.Weight) } + } else { + // This validator's active status is changing. + err = errors.Join( + s.validators.RemoveWeight(sov.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight), + addSoVToValidatorManager(s.validators, sov), + ) } case database.ErrNotFound: - if sov.isDeleted() { - // Deleting a non-existent validator is a noop. This can happen - // if the validator was added and then immediately removed. - continue - } - - // Adding a validator + // Adding a new validator err = addSoVToValidatorManager(s.validators, sov) } if err != nil { @@ -2719,6 +2737,10 @@ func (s *state) writeSubnetOnlyValidators() error { s.weightsCache.Put(subnetID, weight) } + // The SoV diff application is split into two loops to ensure that all + // deletions to the subnetIDNodeIDDB happen prior to any additions. + // Otherwise replacing an SoV by deleting it and then re-adding it with a + // different validationID could result in an inconsistent state. for validationID, sov := range s.sovDiff.modified { // Delete the prior validator if it exists var err error @@ -2732,30 +2754,45 @@ func (s *state) writeSubnetOnlyValidators() error { return err } - // Update the subnetIDNodeID mapping + if !sov.isDeleted() { + continue + } + var ( - isDeleted = sov.isDeleted() subnetIDNodeID = subnetIDNodeID{ subnetID: sov.SubnetID, nodeID: sov.NodeID, } subnetIDNodeIDKey = subnetIDNodeID.Marshal() ) - if isDeleted { - err = s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey) - } else { - err = s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]) - } - if err != nil { + if err := s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey); err != nil { return err } - s.subnetIDNodeIDCache.Put(subnetIDNodeID, !isDeleted) - if isDeleted { + s.subnetIDNodeIDCache.Put(subnetIDNodeID, false) + } + + for validationID, sov := range s.sovDiff.modified { + if sov.isDeleted() { continue } + // Update the subnetIDNodeID mapping + var ( + subnetIDNodeID = subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + subnetIDNodeIDKey = subnetIDNodeID.Marshal() + ) + if err := s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]); err != nil { + return err + } + + s.subnetIDNodeIDCache.Put(subnetIDNodeID, true) + // Add the new validator + var err error if sov.isActive() { s.activeSOVs.put(sov) err = putSubnetOnlyValidator(s.activeDB, sov) From c2ffd1733eb4b0f4ad4bd16ed026cb4831f2b946 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 13:43:29 -0400 Subject: [PATCH 057/184] nit --- vms/platformvm/state/state.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 7f136ea49bef..a5d39ff0079d 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2387,8 +2387,8 @@ func (s *state) updateValidatorManager(updateValidators bool) error { } } - // Now the removed SoV validators have been deleted, perform additions and - // modifications. + // Now that the removed SoV validators have been deleted, perform additions + // and modifications. for validationID, sov := range s.sovDiff.modified { if sov.isDeleted() { continue From 91a6465a027a84db85ea634a398df9283ba30866 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 14:56:27 -0400 Subject: [PATCH 058/184] nit --- vms/platformvm/block/executor/verifier.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index 6616fd58bad3..d79f1d32b298 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -17,8 +17,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) @@ -390,7 +390,7 @@ func (v *verifier) proposalBlock( onDecisionState state.Diff, onCommitState state.Diff, onAbortState state.Diff, - feeCalculator fee.Calculator, + feeCalculator txfee.Calculator, inputs set.Set[ids.ID], atomicRequests map[ids.ID]*atomic.Requests, onAcceptFunc func(), @@ -441,7 +441,7 @@ func (v *verifier) proposalBlock( func (v *verifier) standardBlock( b block.Block, txs []*txs.Tx, - feeCalculator fee.Calculator, + feeCalculator txfee.Calculator, onAcceptState state.Diff, ) error { inputs, atomicRequests, onAcceptFunc, err := v.processStandardTxs( @@ -471,7 +471,7 @@ func (v *verifier) standardBlock( return nil } -func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculator, diff state.Diff, parentID ids.ID) ( +func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calculator, diff state.Diff, parentID ids.ID) ( set.Set[ids.ID], map[ids.ID]*atomic.Requests, func(), @@ -483,7 +483,7 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculato if isEtna { var blockComplexity gas.Dimensions for _, tx := range txs { - txComplexity, err := fee.TxComplexity(tx.Unsigned) + txComplexity, err := txfee.TxComplexity(tx.Unsigned) if err != nil { txID := tx.ID() v.MarkDropped(txID, err) From 07370a5b489be965816689abeb061aadad2d29f8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 15:19:31 -0400 Subject: [PATCH 059/184] nits --- vms/platformvm/state/chain_time_helpers.go | 26 +++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/vms/platformvm/state/chain_time_helpers.go b/vms/platformvm/state/chain_time_helpers.go index 97c2da561363..17d466bcea40 100644 --- a/vms/platformvm/state/chain_time_helpers.go +++ b/vms/platformvm/state/chain_time_helpers.go @@ -49,12 +49,12 @@ func NextBlockTime( } // GetNextStakerChangeTime returns the next time a staker will be either added -// or removed to/from the current validator set. If the next staker change time -// is further in the future than [defaultTime], then [defaultTime] is returned. +// or removed to/from the validator set. If the next staker change time is +// further in the future than [nextTime], then [nextTime] is returned. func GetNextStakerChangeTime( config validatorfee.Config, state Chain, - defaultTime time.Time, + nextTime time.Time, ) (time.Time, error) { currentIterator, err := state.GetCurrentStakerIterator() if err != nil { @@ -75,8 +75,8 @@ func GetNextStakerChangeTime( } time := it.Value().NextTime - if time.Before(defaultTime) { - defaultTime = time + if time.Before(nextTime) { + nextTime = time } } @@ -88,12 +88,11 @@ func GetNextStakerChangeTime( // If there are no SoVs, return if !sovIterator.Next() { - return defaultTime, nil + return nextTime, nil } + // Calculate the remaining funds that the next validator to evict has. var ( - currentTime = state.GetTimestamp() - maxSeconds = uint64(defaultTime.Sub(currentTime) / time.Second) sov = sovIterator.Value() accruedFees = state.GetAccruedFees() ) @@ -102,6 +101,11 @@ func GetNextStakerChangeTime( return time.Time{}, err } + // Calculate how many seconds the remaining funds can last for. + var ( + currentTime = state.GetTimestamp() + maxSeconds = uint64(nextTime.Sub(currentTime) / time.Second) + ) feeState := validatorfee.State{ Current: gas.Gas(state.NumActiveSubnetOnlyValidators()), Excess: state.GetSoVExcess(), @@ -113,10 +117,10 @@ func GetNextStakerChangeTime( ) deactivationTime := currentTime.Add(time.Duration(remainingSeconds) * time.Second) - if deactivationTime.Before(defaultTime) { - defaultTime = deactivationTime + if deactivationTime.Before(nextTime) { + nextTime = deactivationTime } - return defaultTime, nil + return nextTime, nil } // PickFeeCalculator creates either a static or a dynamic fee calculator, From 23aff43b8cd7664022e2b0f08f7c6a47a6e8a965 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 31 Oct 2024 15:24:42 -0400 Subject: [PATCH 060/184] Add additional test --- vms/platformvm/state/chain_time_helpers_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/vms/platformvm/state/chain_time_helpers_test.go b/vms/platformvm/state/chain_time_helpers_test.go index d93ab1bfd48c..32d11d1f7719 100644 --- a/vms/platformvm/state/chain_time_helpers_test.go +++ b/vms/platformvm/state/chain_time_helpers_test.go @@ -15,6 +15,7 @@ import ( "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -113,7 +114,7 @@ func TestGetNextStakerChangeTime(t *testing.T) { expected: genesistest.DefaultValidatorStartTime.Add(time.Second), }, { - name: "current and subnet only validators", + name: "current and subnet only validator with low balance", sovs: []SubnetOnlyValidator{ { ValidationID: ids.GenerateTestID(), @@ -126,6 +127,20 @@ func TestGetNextStakerChangeTime(t *testing.T) { maxTime: mockable.MaxTime, expected: genesistest.DefaultValidatorStartTime.Add(time.Second), }, + { + name: "current and subnet only validator with high balance", + sovs: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + Weight: 1, + EndAccumulatedFee: units.Avax, // This validator won't be evicted soon. + }, + }, + maxTime: mockable.MaxTime, + expected: genesistest.DefaultValidatorEndTime, + }, { name: "restricted timestamp", maxTime: genesistest.DefaultValidatorStartTime, From 8d229519bc0c4e478410947219056bc8076bde73 Mon Sep 17 00:00:00 2001 From: marun Date: Thu, 31 Oct 2024 21:00:20 +0100 Subject: [PATCH 061/184] [testing] Enable bootstrap testing of partial sync (#3508) --- tests/fixture/bootstrapmonitor/README.md | 43 ++++--- .../bootstrapmonitor/bootstrap_test_config.go | 87 +++++++++++++-- .../bootstrap_test_config_test.go | 105 ++++++++++++++++-- 3 files changed, 198 insertions(+), 37 deletions(-) diff --git a/tests/fixture/bootstrapmonitor/README.md b/tests/fixture/bootstrapmonitor/README.md index bb232da3ed43..a772ae0f57d0 100644 --- a/tests/fixture/bootstrapmonitor/README.md +++ b/tests/fixture/bootstrapmonitor/README.md @@ -12,22 +12,30 @@ node is running be compatible with the historical data of that network. Bootstrapping regularly is a good way of insuring against regressions in compatibility. -### Types of bootstrap testing for C-Chain +### Sync modes for bootstrap testing -The X-Chain and P-Chain always synchronize all state, but the bulk of -data for testnet and mainnet is on the C-Chain and there are 2 options: +The 'sync mode' for a bootstrap test determines what history will be +processed (fetched and executed) during the bootstrap process of a +non-validating node. There are 3 such modes: -#### State Sync +#### C-Chain State Sync -A bootstrap with state sync enabled (the default) ensures that only -recent blocks will be processed. +In this mode, the full history of the X- and P-Chains will +processed. Only recent blocks of the C-Chain will be processed. This +is the default mode. -#### Full Sync +#### Only P-Chain Full Sync + +In this mode, only the full history of the P-Chain will be +processed. This is the expected mode for L1 validators. This mode is +configured by the supplying the `--partial-sync-primary-network` +flag. -All history will be processed, though with pruning (enabled by -default) not all history will be stored. +#### Full Sync -To enable, supply `state-sync-enabled: false` as C-Chain configuration. +In this mode, the full history of the X-, P- and C-Chains will be +processed. This is configured by including +`state-sync-enabled:false` in the C-Chain configuration. ## Overview @@ -48,9 +56,18 @@ and initiates a new test when one is found. - The network targeted by the test is determined by the value of the `AVAGO_NETWORK_NAME` env var set for the avalanchego container. - - Whether state sync is enabled is determined by the value of the - `AVAGO_CHAIN_CONFIG_CONTENT` env var set for the avalanchego - container. + - By default, the sync mode will be `c-chain-state-sync`. The other sync + modes require providing additional configuration: + - The `only-p-chain-full-sync` mode is enabled by setting the + `AVAGO_PARTIAL_SYNC_PRIMARY_NETWORK` env var to `"true"` for + the avalanchego container. If this mode is enabled, the state + sync configuration for the C-Chain will be ignored. + - The `full-sync` mode is enabled by including + `state-sync-enabled:false` in the + `AVAGO_CHAIN_CONFIG_CONTENT` env var set for the avalanchego + container and either not including a value for + `AVAGO_PARTIAL_SYNC_PRIMARY_NETWORK` or setting it to + `"false"`. - The image used by the test is determined by the image configured for the avalanchego container. - The versions of the avalanchego image used by the test is diff --git a/tests/fixture/bootstrapmonitor/bootstrap_test_config.go b/tests/fixture/bootstrapmonitor/bootstrap_test_config.go index 6f79669a9902..b8cadd072794 100644 --- a/tests/fixture/bootstrapmonitor/bootstrap_test_config.go +++ b/tests/fixture/bootstrapmonitor/bootstrap_test_config.go @@ -21,11 +21,28 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const VersionsAnnotationKey = "avalanche.avax.network/avalanchego-versions" +// The sync mode of a bootstrap configuration should be as explicit as possible to ensure +// unambiguous names if/when new sync modes become supported. +// +// For example, at time of writing only the C-Chain supports state sync. The temptation +// might be to call this mode that tests C-Chain state sync `state-sync`. But if the P- +// or X-Chains start supporting state sync in the future, a name like +// `c-chain-state-sync` ensures that logs and metrics for historical results can be +// differentiated from new results that involve state sync of the other chains. +type SyncMode string + +const ( + FullSync SyncMode = "full-sync" + CChainStateSync SyncMode = "c-chain-state-sync" // aka state sync + OnlyPChainFullSync SyncMode = "p-chain-full-sync-only" // aka partial sync + + VersionsAnnotationKey = "avalanche.avax.network/avalanchego-versions" +) var ( - chainConfigContentEnvName = config.EnvVarName(config.EnvPrefix, config.ChainConfigContentKey) - networkEnvName = config.EnvVarName(config.EnvPrefix, config.NetworkNameKey) + chainConfigContentEnvName = config.EnvVarName(config.EnvPrefix, config.ChainConfigContentKey) + networkEnvName = config.EnvVarName(config.EnvPrefix, config.NetworkNameKey) + partialSyncPrimaryNetworkEnvName = config.EnvVarName(config.EnvPrefix, config.PartialSyncPrimaryNetworkKey) // Errors for bootstrapTestConfigForPod errContainerNotFound = errors.New("container not found") @@ -40,10 +57,10 @@ var ( ) type BootstrapTestConfig struct { - Network string `json:"network"` - StateSyncEnabled bool `json:"stateSyncEnabled"` - Image string `json:"image"` - Versions *version.Versions `json:"versions,omitempty"` + Network string `json:"network"` + SyncMode SyncMode `json:"syncMode"` + Image string `json:"image"` + Versions *version.Versions `json:"versions,omitempty"` } // GetBootstrapTestConfigFromPod extracts the bootstrap test configuration from the specified pod. @@ -81,16 +98,16 @@ func bootstrapTestConfigForPod(pod *corev1.Pod, nodeContainerName string) (*Boot return nil, fmt.Errorf("%w in container %q", errInvalidNetworkEnvVar, nodeContainerName) } - // Determine whether state sync is enabled in the env vars - stateSyncEnabled, err := stateSyncEnabledFromEnvVars(nodeContainer.Env) + // Determine the sync mode from the env vars + syncMode, err := syncModeFromEnvVars(nodeContainer.Env) if err != nil { return nil, err } testConfig := &BootstrapTestConfig{ - Network: network, - StateSyncEnabled: stateSyncEnabled, - Image: nodeContainer.Image, + Network: network, + SyncMode: syncMode, + Image: nodeContainer.Image, } // Attempt to retrieve the image versions from a pod annotation. The annotation may not be populated in @@ -105,6 +122,52 @@ func bootstrapTestConfigForPod(pod *corev1.Pod, nodeContainerName string) (*Boot return testConfig, nil } +// syncModeFromEnvVars derives the bootstrap sync mode from the provided environment variables. +func syncModeFromEnvVars(env []corev1.EnvVar) (SyncMode, error) { + partialSyncPrimaryNetwork, err := partialSyncEnabledFromEnvVars(env) + if err != nil { + return "", err + } + if partialSyncPrimaryNetwork { + // If partial sync is enabled, only the P-Chain will be synced so the state sync + // configuration of the C-Chain is irrelevant. + return OnlyPChainFullSync, nil + } + stateSyncEnabled, err := stateSyncEnabledFromEnvVars(env) + if err != nil { + return "", err + } + if !stateSyncEnabled { + // Full sync is enabled + return FullSync, nil + } + + // C-Chain state sync is assumed if the other modes are not explicitly enabled + return CChainStateSync, nil +} + +// partialSyncEnabledFromEnvVars determines whether the env vars configure partial sync +// for a node container. Partial sync is assumed to be enabled if the +// AVAGO_PARTIAL_SYNC_PRIMARY_NETWORK env var is set and evaluates to true. +func partialSyncEnabledFromEnvVars(env []corev1.EnvVar) (bool, error) { + var rawPartialSyncPrimaryNetwork string + for _, envVar := range env { + if envVar.Name == partialSyncPrimaryNetworkEnvName { + rawPartialSyncPrimaryNetwork = envVar.Value + break + } + } + if len(rawPartialSyncPrimaryNetwork) == 0 { + return false, nil + } + + partialSyncPrimaryNetwork, err := cast.ToBoolE(rawPartialSyncPrimaryNetwork) + if err != nil { + return false, fmt.Errorf("%w (%v): %w", errFailedToCastToBool, rawPartialSyncPrimaryNetwork, err) + } + return partialSyncPrimaryNetwork, nil +} + // stateSyncEnabledFromEnvVars determines whether the env vars configure state sync for a // node container. State sync is assumed to be enabled if the chain config content is // missing, does not contain C-Chain configuration, or the C-Chain configuration does not diff --git a/tests/fixture/bootstrapmonitor/bootstrap_test_config_test.go b/tests/fixture/bootstrapmonitor/bootstrap_test_config_test.go index 31226b65b838..8f2450a15666 100644 --- a/tests/fixture/bootstrapmonitor/bootstrap_test_config_test.go +++ b/tests/fixture/bootstrapmonitor/bootstrap_test_config_test.go @@ -51,7 +51,7 @@ func TestBootstrapTestConfigForPod(t *testing.T) { expectedErr: errInvalidNetworkEnvVar, }, { - name: "valid configuration without versions and state sync disabled", + name: "valid configuration without versions", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -63,20 +63,15 @@ func TestBootstrapTestConfigForPod(t *testing.T) { Name: networkEnvName, Value: networkName, }, - { - Name: chainConfigContentEnvName, - // Sets state-sync-enabled:false for the C-Chain - Value: "eyJDIjp7IkNvbmZpZyI6ImV5SnpkR0YwWlMxemVXNWpMV1Z1WVdKc1pXUWlPbVpoYkhObGZRPT0iLCJVcGdyYWRlIjpudWxsfX0=", - }, }, }, }, }, }, expectedConfig: &BootstrapTestConfig{ - Network: networkName, - StateSyncEnabled: false, - Image: imageName, + Network: networkName, + SyncMode: CChainStateSync, + Image: imageName, }, }, { @@ -103,9 +98,9 @@ func TestBootstrapTestConfigForPod(t *testing.T) { }, }, expectedConfig: &BootstrapTestConfig{ - Network: networkName, - StateSyncEnabled: true, - Image: imageName, + Network: networkName, + SyncMode: CChainStateSync, + Image: imageName, Versions: &version.Versions{ Application: "avalanchego/1.11.11", Database: "v1.4.5", @@ -158,6 +153,92 @@ func marshalAndEncode(t *testing.T, chainConfigs map[string]chains.ChainConfig) return base64.StdEncoding.EncodeToString(chainConfigContent) } +func TestSyncModeFromEnvVars(t *testing.T) { + tests := []struct { + name string + envVars []corev1.EnvVar + expectedMode SyncMode + }{ + { + name: "default to state sync", + expectedMode: CChainStateSync, + }, + { + name: "partial sync", + envVars: []corev1.EnvVar{ + { + Name: partialSyncPrimaryNetworkEnvName, + Value: "true", + }, + }, + expectedMode: OnlyPChainFullSync, + }, + { + name: "full sync", + envVars: []corev1.EnvVar{ + { + Name: partialSyncPrimaryNetworkEnvName, + Value: "false", + }, + { + Name: chainConfigContentEnvName, + // Sets state-sync-enabled:false for the C-Chain + Value: "eyJDIjp7IkNvbmZpZyI6ImV5SnpkR0YwWlMxemVXNWpMV1Z1WVdKc1pXUWlPbVpoYkhObGZRPT0iLCJVcGdyYWRlIjpudWxsfX0=", + }, + }, + expectedMode: FullSync, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + syncMode, err := syncModeFromEnvVars(test.envVars) + require.NoError(err) + require.Equal(test.expectedMode, syncMode) + }) + } +} + +func TestPartialSyncEnabledFromEnvVars(t *testing.T) { + tests := []struct { + name string + partialSyncPrimaryNetwork string + expectedEnabled bool + expectedErr error + }{ + { + name: "env var not set", + partialSyncPrimaryNetwork: "", + expectedEnabled: false, + }, + { + name: "env var set to true", + partialSyncPrimaryNetwork: "true", + expectedEnabled: true, + }, + { + name: "env var set to invalid value", + partialSyncPrimaryNetwork: "not-a-bool", + expectedErr: errFailedToCastToBool, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + env := []corev1.EnvVar{ + { + Name: partialSyncPrimaryNetworkEnvName, + Value: test.partialSyncPrimaryNetwork, + }, + } + enabled, err := partialSyncEnabledFromEnvVars(env) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expectedEnabled, enabled) + }) + } +} + func TestStateSyncEnabledFromEnvVars(t *testing.T) { invalidJSON := "asdf" invalidBase64 := "abc$def" From 66011f0069ad4df9462e6bb96f14e8325018d8d0 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 13:50:29 -0400 Subject: [PATCH 062/184] Move caching logic --- vms/platformvm/state/state.go | 23 ++-- vms/platformvm/state/subnet_only_validator.go | 45 +++++-- .../state/subnet_only_validator_test.go | 111 ++++++++++++++---- 3 files changed, 135 insertions(+), 44 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index a5d39ff0079d..b7b1341bd23f 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -104,6 +104,8 @@ var ( HeightsIndexedKey = []byte("heights indexed") InitializedKey = []byte("initialized") BlocksReindexedKey = []byte("blocks reindexed") + + emptySoVCache = &cache.Empty[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{} ) // Chain collects all methods to manage the state of the chain for block @@ -862,14 +864,7 @@ func (s *state) getPersistedSubnetOnlyValidator(validationID ids.ID) (SubnetOnly return sov, nil } - if maybeSOV, ok := s.inactiveCache.Get(validationID); ok { - if maybeSOV.IsNothing() { - return SubnetOnlyValidator{}, database.ErrNotFound - } - return maybeSOV.Value(), nil - } - - return getSubnetOnlyValidator(s.inactiveDB, validationID) + return getSubnetOnlyValidator(s.inactiveCache, s.inactiveDB, validationID) } func (s *state) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) { @@ -1942,7 +1937,7 @@ func (s *state) initValidatorSets() error { } // Load active ACP-77 validators - if err := s.activeSOVs.addStakers(s.validators); err != nil { + if err := s.activeSOVs.addStakersToValidatorManager(s.validators); err != nil { return err } @@ -2745,10 +2740,9 @@ func (s *state) writeSubnetOnlyValidators() error { // Delete the prior validator if it exists var err error if s.activeSOVs.delete(validationID) { - err = deleteSubnetOnlyValidator(s.activeDB, validationID) + err = deleteSubnetOnlyValidator(s.activeDB, emptySoVCache, validationID) } else { - s.inactiveCache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]()) - err = deleteSubnetOnlyValidator(s.inactiveDB, validationID) + err = deleteSubnetOnlyValidator(s.inactiveDB, s.inactiveCache, validationID) } if err != nil { return err @@ -2795,10 +2789,9 @@ func (s *state) writeSubnetOnlyValidators() error { var err error if sov.isActive() { s.activeSOVs.put(sov) - err = putSubnetOnlyValidator(s.activeDB, sov) + err = putSubnetOnlyValidator(s.activeDB, emptySoVCache, sov) } else { - s.inactiveCache.Put(validationID, maybe.Some(sov)) - err = putSubnetOnlyValidator(s.inactiveDB, sov) + err = putSubnetOnlyValidator(s.inactiveDB, s.inactiveCache, sov) } if err != nil { return err diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 642b081800f0..556ba3dd27b9 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -10,6 +10,7 @@ import ( "github.com/google/btree" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/validators" @@ -17,6 +18,7 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/math" + "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/vms/platformvm/block" ) @@ -184,7 +186,18 @@ func (v SubnetOnlyValidator) effectivePublicKeyBytes() []byte { return nil } -func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (SubnetOnlyValidator, error) { +func getSubnetOnlyValidator( + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]], + db database.KeyValueReader, + validationID ids.ID, +) (SubnetOnlyValidator, error) { + if maybeSOV, ok := cache.Get(validationID); ok { + if maybeSOV.IsNothing() { + return SubnetOnlyValidator{}, database.ErrNotFound + } + return maybeSOV.Value(), nil + } + bytes, err := db.Get(validationID[:]) if err != nil { return SubnetOnlyValidator{}, err @@ -199,16 +212,34 @@ func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (Su return vdr, nil } -func putSubnetOnlyValidator(db database.KeyValueWriter, vdr SubnetOnlyValidator) error { - bytes, err := block.GenesisCodec.Marshal(block.CodecVersion, vdr) +func putSubnetOnlyValidator( + db database.KeyValueWriter, + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]], + sov SubnetOnlyValidator, +) error { + bytes, err := block.GenesisCodec.Marshal(block.CodecVersion, sov) if err != nil { return fmt.Errorf("failed to marshal SubnetOnlyValidator: %w", err) } - return db.Put(vdr.ValidationID[:], bytes) + if err := db.Put(sov.ValidationID[:], bytes); err != nil { + return err + } + + cache.Put(sov.ValidationID, maybe.Some(sov)) + return nil } -func deleteSubnetOnlyValidator(db database.KeyValueDeleter, validationID ids.ID) error { - return db.Delete(validationID[:]) +func deleteSubnetOnlyValidator( + db database.KeyValueDeleter, + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]], + validationID ids.ID, +) error { + if err := db.Delete(validationID[:]); err != nil { + return err + } + + cache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]()) + return nil } type subnetOnlyValidatorsDiff struct { @@ -368,7 +399,7 @@ func (a *activeSubnetOnlyValidators) newIterator() iterator.Iterator[SubnetOnlyV return iterator.FromTree(a.tree) } -func (a *activeSubnetOnlyValidators) addStakers(vdrs validators.Manager) error { +func (a *activeSubnetOnlyValidators) addStakersToValidatorManager(vdrs validators.Manager) error { for validationID, sov := range a.lookup { pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) if err := vdrs.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight); err != nil { diff --git a/vms/platformvm/state/subnet_only_validator_test.go b/vms/platformvm/state/subnet_only_validator_test.go index 6b6c86520a66..b7399eee195b 100644 --- a/vms/platformvm/state/subnet_only_validator_test.go +++ b/vms/platformvm/state/subnet_only_validator_test.go @@ -9,11 +9,13 @@ import ( "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/vms/platformvm/block" "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -139,11 +141,8 @@ func TestSubnetOnlyValidator_constantsAreUnmodified(t *testing.T) { } func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) { - require := require.New(t) - db := memdb.New() - sk, err := bls.NewSecretKey() - require.NoError(err) + require.NoError(t, err) pk := bls.PublicFromSecretKey(sk) pkBytes := bls.PublicKeyToUncompressedBytes(pk) @@ -154,7 +153,7 @@ func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) { }, } remainingBalanceOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &remainingBalanceOwner) - require.NoError(err) + require.NoError(t, err) var deactivationOwner fx.Owner = &secp256k1fx.OutputOwners{ Threshold: 1, @@ -163,9 +162,9 @@ func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) { }, } deactivationOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &deactivationOwner) - require.NoError(err) + require.NoError(t, err) - vdr := SubnetOnlyValidator{ + sov := SubnetOnlyValidator{ ValidationID: ids.GenerateTestID(), SubnetID: ids.GenerateTestID(), NodeID: ids.GenerateTestNodeID(), @@ -178,24 +177,92 @@ func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) { EndAccumulatedFee: rand.Uint64(), // #nosec G404 } - // Validator hasn't been put on disk yet - gotVdr, err := getSubnetOnlyValidator(db, vdr.ValidationID) - require.ErrorIs(err, database.ErrNotFound) - require.Zero(gotVdr) + var ( + addedDB = memdb.New() + removedDB = memdb.New() + addedAndRemovedDB = memdb.New() + addedAndRemovedAndAddedDB = memdb.New() + + addedCache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + removedCache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + addedAndRemovedCache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + addedAndRemovedAndAddedCache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + ) // Place the validator on disk - require.NoError(putSubnetOnlyValidator(db, vdr)) + require.NoError(t, putSubnetOnlyValidator(addedDB, addedCache, sov)) + require.NoError(t, putSubnetOnlyValidator(addedAndRemovedDB, addedAndRemovedCache, sov)) + require.NoError(t, putSubnetOnlyValidator(addedAndRemovedAndAddedDB, addedAndRemovedAndAddedCache, sov)) - // Verify that the validator can be fetched from disk - gotVdr, err = getSubnetOnlyValidator(db, vdr.ValidationID) - require.NoError(err) - require.Equal(vdr, gotVdr) + // Remove the validator on disk + require.NoError(t, deleteSubnetOnlyValidator(removedDB, removedCache, sov.ValidationID)) + require.NoError(t, deleteSubnetOnlyValidator(addedAndRemovedDB, addedAndRemovedCache, sov.ValidationID)) + require.NoError(t, deleteSubnetOnlyValidator(addedAndRemovedAndAddedDB, addedAndRemovedAndAddedCache, sov.ValidationID)) - // Remove the validator from disk - require.NoError(deleteSubnetOnlyValidator(db, vdr.ValidationID)) + // Reintroduce the validator to disk + require.NoError(t, putSubnetOnlyValidator(addedAndRemovedAndAddedDB, addedAndRemovedAndAddedCache, sov)) - // Verify that the validator has been removed from disk - gotVdr, err = getSubnetOnlyValidator(db, vdr.ValidationID) - require.ErrorIs(err, database.ErrNotFound) - require.Zero(gotVdr) + addedTests := []struct { + name string + db database.Database + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]] + }{ + { + name: "added in cache", + db: memdb.New(), + cache: addedCache, + }, + { + name: "added on disk", + db: addedDB, + cache: emptySoVCache, + }, + { + name: "added and removed and added in cache", + db: memdb.New(), + cache: addedAndRemovedAndAddedCache, + }, + { + name: "added and removed and added on disk", + db: addedAndRemovedAndAddedDB, + cache: emptySoVCache, + }, + } + for _, test := range addedTests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + gotSOV, err := getSubnetOnlyValidator(test.cache, test.db, sov.ValidationID) + require.NoError(err) + require.Equal(sov, gotSOV) + }) + } + + removedTests := []struct { + name string + db database.Database + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]] + }{ + { + name: "empty", + db: memdb.New(), + cache: emptySoVCache, + }, + { + name: "removed from cache", + db: addedDB, + cache: removedCache, + }, + { + name: "removed from disk", + db: removedDB, + cache: emptySoVCache, + }, + } + for _, test := range removedTests { + t.Run(test.name, func(t *testing.T) { + _, err := getSubnetOnlyValidator(test.cache, test.db, sov.ValidationID) + require.ErrorIs(t, err, database.ErrNotFound) + }) + } } From 7404ffe96aac8bdc67c5a80bcf7688e9773f0a70 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 15:14:41 -0400 Subject: [PATCH 063/184] Rename `constantsAreUnmodified` to `immutableFieldsAreUnmodified` (#3513) --- vms/platformvm/state/subnet_only_validator.go | 7 ++++--- .../state/subnet_only_validator_test.go | 18 +++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 1da34a502990..79033297e18b 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -88,9 +88,10 @@ func (v SubnetOnlyValidator) Compare(o SubnetOnlyValidator) int { } } -// constantsAreUnmodified returns true if the constants of this validator have -// not been modified compared to the other validator. -func (v SubnetOnlyValidator) constantsAreUnmodified(o SubnetOnlyValidator) bool { +// immutableFieldsAreUnmodified returns true if two versions of the same +// validator are valid. Either because the validationID has changed or because +// no unexpected fields have been modified. +func (v SubnetOnlyValidator) immutableFieldsAreUnmodified(o SubnetOnlyValidator) bool { if v.ValidationID != o.ValidationID { return true } diff --git a/vms/platformvm/state/subnet_only_validator_test.go b/vms/platformvm/state/subnet_only_validator_test.go index 6b6c86520a66..4f73cc287194 100644 --- a/vms/platformvm/state/subnet_only_validator_test.go +++ b/vms/platformvm/state/subnet_only_validator_test.go @@ -75,7 +75,7 @@ func TestSubnetOnlyValidator_Compare(t *testing.T) { } } -func TestSubnetOnlyValidator_constantsAreUnmodified(t *testing.T) { +func TestSubnetOnlyValidator_immutableFieldsAreUnmodified(t *testing.T) { var ( randomSOV = func() SubnetOnlyValidator { return SubnetOnlyValidator{ @@ -100,41 +100,41 @@ func TestSubnetOnlyValidator_constantsAreUnmodified(t *testing.T) { t.Run("equal", func(t *testing.T) { v := randomizeSOV(sov) - require.True(t, sov.constantsAreUnmodified(v)) + require.True(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("everything is different", func(t *testing.T) { v := randomizeSOV(randomSOV()) - require.True(t, sov.constantsAreUnmodified(v)) + require.True(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("different subnetID", func(t *testing.T) { v := randomizeSOV(sov) v.SubnetID = ids.GenerateTestID() - require.False(t, sov.constantsAreUnmodified(v)) + require.False(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("different nodeID", func(t *testing.T) { v := randomizeSOV(sov) v.NodeID = ids.GenerateTestNodeID() - require.False(t, sov.constantsAreUnmodified(v)) + require.False(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("different publicKey", func(t *testing.T) { v := randomizeSOV(sov) v.PublicKey = utils.RandomBytes(bls.PublicKeyLen) - require.False(t, sov.constantsAreUnmodified(v)) + require.False(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("different remainingBalanceOwner", func(t *testing.T) { v := randomizeSOV(sov) v.RemainingBalanceOwner = utils.RandomBytes(32) - require.False(t, sov.constantsAreUnmodified(v)) + require.False(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("different deactivationOwner", func(t *testing.T) { v := randomizeSOV(sov) v.DeactivationOwner = utils.RandomBytes(32) - require.False(t, sov.constantsAreUnmodified(v)) + require.False(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("different startTime", func(t *testing.T) { v := randomizeSOV(sov) v.StartTime = rand.Uint64() // #nosec G404 - require.False(t, sov.constantsAreUnmodified(v)) + require.False(t, sov.immutableFieldsAreUnmodified(v)) }) } From d183148fe949cf9b7cec9e4914f52923964cdee5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 15:33:57 -0400 Subject: [PATCH 064/184] merged --- vms/platformvm/state/subnet_only_validator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 8979da0cc9e3..c0a363d4e4a9 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -290,7 +290,7 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne ) switch priorSOV, err := state.GetSubnetOnlyValidator(sov.ValidationID); err { case nil: - if !priorSOV.constantsAreUnmodified(sov) { + if !priorSOV.immutableFieldsAreUnmodified(sov) { return ErrMutatedSubnetOnlyValidator } From b1bb45872d423ddec5e9493dbcb165b27f885209 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 15:36:20 -0400 Subject: [PATCH 065/184] nit --- vms/platformvm/state/state_test.go | 2 +- vms/platformvm/state/subnet_only_validator.go | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index a1a8feb6676e..2b28dcc0977c 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1997,7 +1997,7 @@ func TestSubnetOnlyValidators(t *testing.T) { } // TestLoadSubnetOnlyValidatorAndLegacy tests that the state can be loaded when -// there is a mix of legacy validators and subnet only validators in the same +// there is a mix of legacy validators and subnet-only validators in the same // subnet. func TestLoadSubnetOnlyValidatorAndLegacy(t *testing.T) { var ( diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index c0a363d4e4a9..3f5799e7da28 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -26,22 +26,22 @@ var ( _ btree.LessFunc[SubnetOnlyValidator] = SubnetOnlyValidator.Less _ utils.Sortable[SubnetOnlyValidator] = SubnetOnlyValidator{} - ErrMutatedSubnetOnlyValidator = errors.New("subnet only validator contains mutated constant fields") - ErrConflictingSubnetOnlyValidator = errors.New("subnet only validator contains conflicting subnetID + nodeID pair") - ErrDuplicateSubnetOnlyValidator = errors.New("subnet only validator contains duplicate subnetID + nodeID pair") + ErrMutatedSubnetOnlyValidator = errors.New("subnet-only validator contains mutated constant fields") + ErrConflictingSubnetOnlyValidator = errors.New("subnet-only validator contains conflicting subnetID + nodeID pair") + ErrDuplicateSubnetOnlyValidator = errors.New("subnet-only validator contains duplicate subnetID + nodeID pair") ) type SubnetOnlyValidators interface { // GetActiveSubnetOnlyValidatorsIterator returns an iterator of all the - // active subnet only validators in increasing order of EndAccumulatedFee. + // active subnet-only validators in increasing order of EndAccumulatedFee. GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) // NumActiveSubnetOnlyValidators returns the number of currently active - // subnet only validators. + // subnet-only validators. NumActiveSubnetOnlyValidators() int // WeightOfSubnetOnlyValidators returns the total active and inactive weight - // of subnet only validators on [subnetID]. + // of subnet-only validators on [subnetID]. WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) // GetSubnetOnlyValidator returns the validator with [validationID] if it @@ -57,9 +57,9 @@ type SubnetOnlyValidators interface { // validator is 0, the validator is removed. // // If inserting this validator attempts to modify any of the constant fields - // of the subnet only validator struct, an error will be returned. + // of the subnet-only validator struct, an error will be returned. // - // If inserting this validator would cause the total weight of subnet only + // If inserting this validator would cause the total weight of subnet-only // validators on a subnet to overflow MaxUint64, an error will be returned. // // If inserting this validator would cause there to be multiple validators From ad31107c4501d4c0a264e87bf31f5c6c81029962 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 15:59:41 -0400 Subject: [PATCH 066/184] Add weight diff helpers --- vms/platformvm/state/stakers.go | 4 +- vms/platformvm/state/state.go | 10 ++- vms/platformvm/state/state_test.go | 111 ++++++++++------------------- 3 files changed, 48 insertions(+), 77 deletions(-) diff --git a/vms/platformvm/state/stakers.go b/vms/platformvm/state/stakers.go index 14e4dcf7b1ef..f076e23ab59b 100644 --- a/vms/platformvm/state/stakers.go +++ b/vms/platformvm/state/stakers.go @@ -283,7 +283,7 @@ func (d *diffValidator) WeightDiff() (ValidatorWeightDiff, error) { } for _, staker := range d.deletedDelegators { - if err := weightDiff.Add(true, staker.Weight); err != nil { + if err := weightDiff.Sub(staker.Weight); err != nil { return ValidatorWeightDiff{}, fmt.Errorf("failed to decrease node weight diff: %w", err) } } @@ -294,7 +294,7 @@ func (d *diffValidator) WeightDiff() (ValidatorWeightDiff, error) { for addedDelegatorIterator.Next() { staker := addedDelegatorIterator.Value() - if err := weightDiff.Add(false, staker.Weight); err != nil { + if err := weightDiff.Add(staker.Weight); err != nil { return ValidatorWeightDiff{}, fmt.Errorf("failed to increase node weight diff: %w", err) } } diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 09a22f8c27de..3368884da910 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -425,7 +425,15 @@ type ValidatorWeightDiff struct { Amount uint64 `serialize:"true"` } -func (v *ValidatorWeightDiff) Add(negative bool, amount uint64) error { +func (v *ValidatorWeightDiff) Add(amount uint64) error { + return v.add(false, amount) +} + +func (v *ValidatorWeightDiff) Sub(amount uint64) error { + return v.add(true, amount) +} + +func (v *ValidatorWeightDiff) add(negative bool, amount uint64) error { if v.Decrease == negative { var err error v.Amount, err = safemath.Add(v.Amount, amount) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 143f673b4e0f..f56204d1f208 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -671,29 +671,32 @@ func createPermissionlessDelegatorTx(subnetID ids.ID, delegatorData txs.Validato } func TestValidatorWeightDiff(t *testing.T) { + type op struct { + op func(*ValidatorWeightDiff, uint64) error + amount uint64 + } type test struct { name string - ops []func(*ValidatorWeightDiff) error + ops []op expected *ValidatorWeightDiff expectedErr error } + var ( + add = (*ValidatorWeightDiff).Add + sub = (*ValidatorWeightDiff).Sub + ) tests := []test{ { name: "no ops", - ops: []func(*ValidatorWeightDiff) error{}, expected: &ValidatorWeightDiff{}, expectedErr: nil, }, { name: "simple decrease", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) - }, + ops: []op{ + {sub, 1}, + {sub, 1}, }, expected: &ValidatorWeightDiff{ Decrease: true, @@ -703,26 +706,17 @@ func TestValidatorWeightDiff(t *testing.T) { }, { name: "decrease overflow", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(true, math.MaxUint64) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) - }, + ops: []op{ + {sub, math.MaxUint64}, + {sub, 1}, }, - expected: &ValidatorWeightDiff{}, expectedErr: safemath.ErrOverflow, }, { name: "simple increase", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) - }, + ops: []op{ + {add, 1}, + {add, 1}, }, expected: &ValidatorWeightDiff{ Decrease: false, @@ -732,58 +726,24 @@ func TestValidatorWeightDiff(t *testing.T) { }, { name: "increase overflow", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(false, math.MaxUint64) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) - }, + ops: []op{ + {add, math.MaxUint64}, + {add, 1}, }, - expected: &ValidatorWeightDiff{}, expectedErr: safemath.ErrOverflow, }, { name: "varied use", - ops: []func(*ValidatorWeightDiff) error{ - // Add to 0 - func(d *ValidatorWeightDiff) error { - return d.Add(false, 2) // Value 2 - }, - // Subtract from positive number - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) // Value 1 - }, - // Subtract from positive number - // to make it negative - func(d *ValidatorWeightDiff) error { - return d.Add(true, 3) // Value -2 - }, - // Subtract from a negative number - func(d *ValidatorWeightDiff) error { - return d.Add(true, 3) // Value -5 - }, - // Add to a negative number - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) // Value -4 - }, - // Add to a negative number - // to make it positive - func(d *ValidatorWeightDiff) error { - return d.Add(false, 5) // Value 1 - }, - // Add to a positive number - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) // Value 2 - }, - // Get to zero - func(d *ValidatorWeightDiff) error { - return d.Add(true, 2) // Value 0 - }, - // Subtract from zero - func(d *ValidatorWeightDiff) error { - return d.Add(true, 2) // Value -2 - }, + ops: []op{ + {add, 2}, // = 2 + {sub, 1}, // = 1 + {sub, 3}, // = -2 + {sub, 3}, // = -5 + {add, 1}, // = -4 + {add, 5}, // = 1 + {add, 1}, // = 2 + {sub, 2}, // = 0 + {sub, 2}, // = -2 }, expected: &ValidatorWeightDiff{ Decrease: true, @@ -796,10 +756,13 @@ func TestValidatorWeightDiff(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require := require.New(t) - diff := &ValidatorWeightDiff{} - errs := wrappers.Errs{} + + var ( + diff = &ValidatorWeightDiff{} + errs = wrappers.Errs{} + ) for _, op := range tt.ops { - errs.Add(op(diff)) + errs.Add(op.op(diff, op.amount)) } require.ErrorIs(errs.Err, tt.expectedErr) if tt.expectedErr != nil { From cee236e010c43d4e1ffdfef2552065a55423556a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 16:47:48 -0400 Subject: [PATCH 067/184] nit --- vms/platformvm/state/state_test.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index f56204d1f208..60b92c58dc44 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -688,9 +688,8 @@ func TestValidatorWeightDiff(t *testing.T) { ) tests := []test{ { - name: "no ops", - expected: &ValidatorWeightDiff{}, - expectedErr: nil, + name: "no ops", + expected: &ValidatorWeightDiff{}, }, { name: "simple decrease", @@ -702,7 +701,6 @@ func TestValidatorWeightDiff(t *testing.T) { Decrease: true, Amount: 2, }, - expectedErr: nil, }, { name: "decrease overflow", @@ -722,7 +720,6 @@ func TestValidatorWeightDiff(t *testing.T) { Decrease: false, Amount: 2, }, - expectedErr: nil, }, { name: "increase overflow", @@ -749,7 +746,6 @@ func TestValidatorWeightDiff(t *testing.T) { Decrease: true, Amount: 2, }, - expectedErr: nil, }, } From 900eba34150de4285c59a71c757a13644b2d8e37 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 16:51:57 -0400 Subject: [PATCH 068/184] add -> addOrSub --- vms/platformvm/state/state.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 3368884da910..1e45b6f07826 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -426,15 +426,15 @@ type ValidatorWeightDiff struct { } func (v *ValidatorWeightDiff) Add(amount uint64) error { - return v.add(false, amount) + return v.addOrSub(false, amount) } func (v *ValidatorWeightDiff) Sub(amount uint64) error { - return v.add(true, amount) + return v.addOrSub(true, amount) } -func (v *ValidatorWeightDiff) add(negative bool, amount uint64) error { - if v.Decrease == negative { +func (v *ValidatorWeightDiff) addOrSub(sub bool, amount uint64) error { + if v.Decrease == sub { var err error v.Amount, err = safemath.Add(v.Amount, amount) return err @@ -444,7 +444,7 @@ func (v *ValidatorWeightDiff) add(negative bool, amount uint64) error { v.Amount -= amount } else { v.Amount = safemath.AbsDiff(v.Amount, amount) - v.Decrease = negative + v.Decrease = sub } return nil } From bcae9cdc5d98853d7ebca6cc95dd2e65895e9a56 Mon Sep 17 00:00:00 2001 From: cam-schultz <78878559+cam-schultz@users.noreply.github.com> Date: Fri, 1 Nov 2024 18:34:54 -0500 Subject: [PATCH 069/184] Accept info.Peers args (#3515) --- api/info/client.go | 8 +++++--- tests/e2e/faultinjection/duplicate_node_id.go | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api/info/client.go b/api/info/client.go index 4f198d0c0e7f..0e6582534660 100644 --- a/api/info/client.go +++ b/api/info/client.go @@ -25,7 +25,7 @@ type Client interface { GetNetworkID(context.Context, ...rpc.Option) (uint32, error) GetNetworkName(context.Context, ...rpc.Option) (string, error) GetBlockchainID(context.Context, string, ...rpc.Option) (ids.ID, error) - Peers(context.Context, ...rpc.Option) ([]Peer, error) + Peers(context.Context, []ids.NodeID, ...rpc.Option) ([]Peer, error) IsBootstrapped(context.Context, string, ...rpc.Option) (bool, error) GetTxFee(context.Context, ...rpc.Option) (*GetTxFeeResponse, error) Upgrades(context.Context, ...rpc.Option) (*upgrade.Config, error) @@ -83,9 +83,11 @@ func (c *client) GetBlockchainID(ctx context.Context, alias string, options ...r return res.BlockchainID, err } -func (c *client) Peers(ctx context.Context, options ...rpc.Option) ([]Peer, error) { +func (c *client) Peers(ctx context.Context, nodeIDs []ids.NodeID, options ...rpc.Option) ([]Peer, error) { res := &PeersReply{} - err := c.requester.SendRequest(ctx, "info.peers", struct{}{}, res, options...) + err := c.requester.SendRequest(ctx, "info.peers", &PeersArgs{ + NodeIDs: nodeIDs, + }, res, options...) return res.Peers, err } diff --git a/tests/e2e/faultinjection/duplicate_node_id.go b/tests/e2e/faultinjection/duplicate_node_id.go index d0a72d5b0574..6c0b0e5c1a4c 100644 --- a/tests/e2e/faultinjection/duplicate_node_id.go +++ b/tests/e2e/faultinjection/duplicate_node_id.go @@ -68,7 +68,7 @@ func checkConnectedPeers(tc tests.TestContext, existingNodes []*tmpnet.Node, new // Collect the node ids of the new node's peers infoClient := info.NewClient(newNode.URI) - peers, err := infoClient.Peers(tc.DefaultContext()) + peers, err := infoClient.Peers(tc.DefaultContext(), nil) require.NoError(err) peerIDs := set.NewSet[ids.NodeID](len(existingNodes)) for _, peer := range peers { @@ -81,7 +81,7 @@ func checkConnectedPeers(tc tests.TestContext, existingNodes []*tmpnet.Node, new // Check that the new node is a peer infoClient := info.NewClient(existingNode.URI) - peers, err := infoClient.Peers(tc.DefaultContext()) + peers, err := infoClient.Peers(tc.DefaultContext(), nil) require.NoError(err) isPeer := false for _, peer := range peers { From 7cbf31b3f759f20235838146a705a90ae0a3df40 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 1 Nov 2024 20:00:29 -0400 Subject: [PATCH 070/184] fix merge --- vms/platformvm/state/state.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 07dce8b248a6..ad09f28457b7 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -2496,7 +2496,7 @@ func (s *state) calculateValidatorDiffs() (map[subnetIDNodeID]*validatorDiff, er nodeID: priorSOV.effectiveNodeID(), } diff := getOrDefault(changes, subnetIDNodeID) - if err := diff.weightDiff.Add(true, priorSOV.Weight); err != nil { + if err := diff.weightDiff.Sub(priorSOV.Weight); err != nil { return nil, err } diff.prevPublicKey = priorSOV.effectivePublicKeyBytes() @@ -2516,7 +2516,7 @@ func (s *state) calculateValidatorDiffs() (map[subnetIDNodeID]*validatorDiff, er nodeID: sov.effectiveNodeID(), } diff := getOrDefault(changes, subnetIDNodeID) - if err := diff.weightDiff.Add(false, sov.Weight); err != nil { + if err := diff.weightDiff.Add(sov.Weight); err != nil { return nil, err } diff.newPublicKey = sov.effectivePublicKeyBytes() From 33d297d236960e0b335a8be7f1daedd16db7368c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 Nov 2024 12:44:02 -0400 Subject: [PATCH 071/184] Add test --- vms/platformvm/block/executor/verifier.go | 2 +- .../block/executor/verifier_test.go | 184 +++++++++++++++++- 2 files changed, 180 insertions(+), 6 deletions(-) diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index d79f1d32b298..56c030c05366 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -563,11 +563,11 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calcula // This ensures that SoVs are not undercharged for the next second. if isEtna { var ( + accruedFees = diff.GetAccruedFees() validatorFeeState = validatorfee.State{ Current: gas.Gas(diff.NumActiveSubnetOnlyValidators()), Excess: diff.GetSoVExcess(), } - accruedFees = diff.GetAccruedFees() potentialCost = validatorFeeState.CostOf( v.txExecutorBackend.Config.ValidatorFeeConfig, 1, // 1 second diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 91015bdcd95e..141792177eb7 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -21,9 +21,12 @@ import ( "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -35,13 +38,15 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool/mempoolmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txsmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) func newTestVerifier(t testing.TB, s state.State) *verifier { @@ -71,9 +76,15 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { }, txExecutorBackend: &executor.Backend{ Config: &config.Config{ - CreateAssetTxFee: genesis.LocalParams.CreateAssetTxFee, - StaticFeeConfig: genesis.LocalParams.StaticFeeConfig, - DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + CreateAssetTxFee: genesis.LocalParams.CreateAssetTxFee, + StaticFeeConfig: genesis.LocalParams.StaticFeeConfig, + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: validatorfee.Config{ + Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: gas.Price(2 * units.NanoAvax), + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, SybilProtectionEnabled: true, UpgradeConfig: upgrades, }, @@ -1134,7 +1145,7 @@ func TestBlockExecutionWithComplexity(t *testing.T) { baseTx1, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) require.NoError(t, err) - blockComplexity, err := fee.TxComplexity(baseTx0.Unsigned, baseTx1.Unsigned) + blockComplexity, err := txfee.TxComplexity(baseTx0.Unsigned, baseTx1.Unsigned) require.NoError(t, err) blockGas, err := blockComplexity.ToGas(verifier.txExecutorBackend.Config.DynamicFeeConfig.Weights) require.NoError(t, err) @@ -1204,3 +1215,166 @@ func TestBlockExecutionWithComplexity(t *testing.T) { }) } } + +func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + var ( + pk = bls.PublicFromSecretKey(sk) + pkBytes = bls.PublicKeyToUncompressedBytes(pk) + + fractionalTimeSoV0 = state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, + EndAccumulatedFee: 5 * units.NanoAvax, // lasts 2.5 seconds + } + fractionalTimeSoV1 = state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, + EndAccumulatedFee: 5 * units.NanoAvax, // lasts 2.5 seconds + } + fractionalSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(2 * time.Second) // evicts early rather than late + + wholeTimeSoV = state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, + EndAccumulatedFee: 8 * units.NanoAvax, // lasts 4 seconds + } + wholeSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(4 * time.Second) // evicts on time + ) + + tests := []struct { + name string + initialSoVs []state.SubnetOnlyValidator + timestamp time.Time + expectedErr error + expectedSoVs []state.SubnetOnlyValidator + }{ + { + name: "no SoVs", + timestamp: genesistest.DefaultValidatorStartTime.Add(10 * time.Second), + }, + { + name: "fractional SoVs are not overcharged", + initialSoVs: []state.SubnetOnlyValidator{ + fractionalTimeSoV0, + }, + timestamp: fractionalSoVEvictedTime.Add(-time.Second), + expectedSoVs: []state.SubnetOnlyValidator{ + fractionalTimeSoV0, + }, + }, + { + name: "fractional SoVs are not undercharged", + initialSoVs: []state.SubnetOnlyValidator{ + fractionalTimeSoV0, + fractionalTimeSoV1, + }, + timestamp: fractionalSoVEvictedTime, + }, + { + name: "whole SoVs are not overcharged", + initialSoVs: []state.SubnetOnlyValidator{ + wholeTimeSoV, + }, + timestamp: wholeSoVEvictedTime.Add(-time.Second), + expectedSoVs: []state.SubnetOnlyValidator{ + wholeTimeSoV, + }, + }, + { + name: "whole SoVs are not undercharged", + initialSoVs: []state.SubnetOnlyValidator{ + wholeTimeSoV, + }, + timestamp: wholeSoVEvictedTime, + }, + { + name: "partial eviction", + initialSoVs: []state.SubnetOnlyValidator{ + fractionalTimeSoV0, + wholeTimeSoV, + }, + timestamp: fractionalSoVEvictedTime, + expectedSoVs: []state.SubnetOnlyValidator{ + wholeTimeSoV, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + s := statetest.New(t, statetest.Config{}) + for _, sov := range test.initialSoVs { + require.NoError(s.PutSubnetOnlyValidator(sov)) + } + + verifier := newTestVerifier(t, s) + verifier.txExecutorBackend.Clk.Set(test.timestamp) + + wallet := txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + s, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + nil, // chainIDs + ) + + baseTx, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) + require.NoError(err) + + timestamp, _, err := state.NextBlockTime( + verifier.txExecutorBackend.Config.ValidatorFeeConfig, + s, + verifier.txExecutorBackend.Clk, + ) + require.NoError(err) + + lastAcceptedID := s.GetLastAccepted() + lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) + require.NoError(err) + + blk, err := block.NewBanffStandardBlock( + timestamp, + lastAcceptedID, + lastAccepted.Height()+1, + []*txs.Tx{ + baseTx, + }, + ) + require.NoError(err) + + blkID := blk.ID() + err = blk.Visit(verifier) + require.ErrorIs(err, test.expectedErr) + if err != nil { + require.NotContains(verifier.blkIDToState, blkID) + return + } + + require.Contains(verifier.blkIDToState, blkID) + blockState := verifier.blkIDToState[blkID] + require.Equal(blk, blockState.statelessBlock) + + sovs, err := blockState.onAcceptState.GetActiveSubnetOnlyValidatorsIterator() + require.NoError(err) + require.Equal( + test.expectedSoVs, + iterator.ToSlice(sovs), + ) + }) + } +} From 486f732a9ee0b04aa075c386e090f76fec8493e2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 Nov 2024 12:45:51 -0400 Subject: [PATCH 072/184] nit --- vms/platformvm/block/executor/verifier_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 141792177eb7..69833a670d94 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -82,7 +82,7 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { ValidatorFeeConfig: validatorfee.Config{ Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, Target: genesis.LocalParams.ValidatorFeeConfig.Target, - MinPrice: gas.Price(2 * units.NanoAvax), + MinPrice: gas.Price(2 * units.NanoAvax), // Min price is increased to allow fractional fees ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, }, SybilProtectionEnabled: true, From b448a042a285835b170752a576eefdd465be4aec Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 Nov 2024 12:47:37 -0400 Subject: [PATCH 073/184] reduce diff --- vms/platformvm/block/executor/verifier_test.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 69833a670d94..97a1c2ca1e51 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -1257,7 +1257,6 @@ func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { name string initialSoVs []state.SubnetOnlyValidator timestamp time.Time - expectedErr error expectedSoVs []state.SubnetOnlyValidator }{ { @@ -1357,14 +1356,9 @@ func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { ) require.NoError(err) - blkID := blk.ID() - err = blk.Visit(verifier) - require.ErrorIs(err, test.expectedErr) - if err != nil { - require.NotContains(verifier.blkIDToState, blkID) - return - } + require.NoError(blk.Visit(verifier)) + blkID := blk.ID() require.Contains(verifier.blkIDToState, blkID) blockState := verifier.blkIDToState[blkID] require.Equal(blk, blockState.statelessBlock) From 0685531798bf4821c815b86246108a6620795ab6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 Nov 2024 13:43:32 -0400 Subject: [PATCH 074/184] Add tests --- .../block/executor/verifier_test.go | 39 ++---- .../txs/executor/state_changes_test.go | 120 ++++++++++++++++++ 2 files changed, 134 insertions(+), 25 deletions(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 97a1c2ca1e51..043ec8f7caba 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -1224,33 +1224,22 @@ func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { pk = bls.PublicFromSecretKey(sk) pkBytes = bls.PublicKeyToUncompressedBytes(pk) - fractionalTimeSoV0 = state.SubnetOnlyValidator{ - ValidationID: ids.GenerateTestID(), - SubnetID: ids.GenerateTestID(), - NodeID: ids.GenerateTestNodeID(), - PublicKey: pkBytes, - Weight: 1, - EndAccumulatedFee: 5 * units.NanoAvax, // lasts 2.5 seconds - } - fractionalTimeSoV1 = state.SubnetOnlyValidator{ - ValidationID: ids.GenerateTestID(), - SubnetID: ids.GenerateTestID(), - NodeID: ids.GenerateTestNodeID(), - PublicKey: pkBytes, - Weight: 1, - EndAccumulatedFee: 5 * units.NanoAvax, // lasts 2.5 seconds + newSoV = func(endAccumulatedFee uint64) state.SubnetOnlyValidator { + return state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, + EndAccumulatedFee: endAccumulatedFee, + } } - fractionalSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(2 * time.Second) // evicts early rather than late + fractionalTimeSoV0 = newSoV(5 * units.NanoAvax) // lasts 2.5 seconds + fractionalTimeSoV1 = newSoV(5 * units.NanoAvax) // lasts 2.5 seconds + wholeTimeSoV = newSoV(8 * units.NanoAvax) // lasts 4 seconds - wholeTimeSoV = state.SubnetOnlyValidator{ - ValidationID: ids.GenerateTestID(), - SubnetID: ids.GenerateTestID(), - NodeID: ids.GenerateTestNodeID(), - PublicKey: pkBytes, - Weight: 1, - EndAccumulatedFee: 8 * units.NanoAvax, // lasts 4 seconds - } - wholeSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(4 * time.Second) // evicts on time + fractionalSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(2 * time.Second) // evicts early rather than late + wholeSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(4 * time.Second) // evicts on time ) tests := []struct { diff --git a/vms/platformvm/txs/executor/state_changes_test.go b/vms/platformvm/txs/executor/state_changes_test.go index 6ae427129201..42477a15e28d 100644 --- a/vms/platformvm/txs/executor/state_changes_test.go +++ b/vms/platformvm/txs/executor/state_changes_test.go @@ -12,8 +12,10 @@ import ( "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" @@ -223,3 +225,121 @@ func TestAdvanceTimeTo_RemovesStaleExpiries(t *testing.T) { }) } } + +func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + var ( + pk = bls.PublicFromSecretKey(sk) + pkBytes = bls.PublicKeyToUncompressedBytes(pk) + + newSoV = func(endAccumulatedFee uint64) state.SubnetOnlyValidator { + return state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, + EndAccumulatedFee: endAccumulatedFee, + } + } + sovToEvict0 = newSoV(3 * units.NanoAvax) // lasts 3 seconds + sovToEvict1 = newSoV(3 * units.NanoAvax) // lasts 3 seconds + sovToKeep = newSoV(units.Avax) + + currentTime = genesistest.DefaultValidatorStartTime + newTime = currentTime.Add(3 * time.Second) + ) + + tests := []struct { + name string + initialSoVs []state.SubnetOnlyValidator + expectedModified bool + expectedSoVs []state.SubnetOnlyValidator + }{ + { + name: "no SoVs", + expectedModified: false, + }, + { + name: "evicted one", + initialSoVs: []state.SubnetOnlyValidator{ + sovToEvict0, + }, + expectedModified: true, + }, + { + name: "evicted multiple", + initialSoVs: []state.SubnetOnlyValidator{ + sovToEvict0, + sovToEvict1, + }, + expectedModified: true, + }, + { + name: "evicted only 2", + initialSoVs: []state.SubnetOnlyValidator{ + sovToEvict0, + sovToEvict1, + sovToKeep, + }, + expectedModified: true, + expectedSoVs: []state.SubnetOnlyValidator{ + sovToKeep, + }, + }, + { + name: "chooses not to evict", + initialSoVs: []state.SubnetOnlyValidator{ + sovToKeep, + }, + expectedModified: false, + expectedSoVs: []state.SubnetOnlyValidator{ + sovToKeep, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var ( + require = require.New(t) + s = statetest.New(t, statetest.Config{}) + ) + + for _, sov := range test.initialSoVs { + require.NoError(s.PutSubnetOnlyValidator(sov)) + } + + // Ensure the invariant that [newTime <= nextStakerChangeTime] on + // AdvanceTimeTo is maintained. + nextStakerChangeTime, err := state.GetNextStakerChangeTime( + genesis.LocalParams.ValidatorFeeConfig, + s, + mockable.MaxTime, + ) + require.NoError(err) + require.False(newTime.After(nextStakerChangeTime)) + + validatorsModified, err := AdvanceTimeTo( + &Backend{ + Config: &config.Config{ + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + }, + }, + s, + newTime, + ) + require.NoError(err) + require.Equal(test.expectedModified, validatorsModified) + + activeSoVs, err := s.GetActiveSubnetOnlyValidatorsIterator() + require.NoError(err) + require.Equal( + test.expectedSoVs, + iterator.ToSlice(activeSoVs), + ) + }) + } +} From bb9f853994404dbb7f58fa651134b0a68df80d9e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sat, 2 Nov 2024 13:50:45 -0400 Subject: [PATCH 075/184] test excess and fees --- .../txs/executor/state_changes_test.go | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/txs/executor/state_changes_test.go b/vms/platformvm/txs/executor/state_changes_test.go index 42477a15e28d..944fd7253859 100644 --- a/vms/platformvm/txs/executor/state_changes_test.go +++ b/vms/platformvm/txs/executor/state_changes_test.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" + "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) func TestAdvanceTimeTo_UpdatesFeeState(t *testing.T) { @@ -226,10 +227,15 @@ func TestAdvanceTimeTo_RemovesStaleExpiries(t *testing.T) { } } -func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { +func TestAdvanceTimeTo_UpdateSoVs(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) + const ( + secondsToAdvance = 3 + timeToAdvance = secondsToAdvance * time.Second + ) + var ( pk = bls.PublicFromSecretKey(sk) pkBytes = bls.PublicKeyToUncompressedBytes(pk) @@ -249,7 +255,7 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { sovToKeep = newSoV(units.Avax) currentTime = genesistest.DefaultValidatorStartTime - newTime = currentTime.Add(3 * time.Second) + newTime = currentTime.Add(timeToAdvance) ) tests := []struct { @@ -257,10 +263,12 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { initialSoVs []state.SubnetOnlyValidator expectedModified bool expectedSoVs []state.SubnetOnlyValidator + expectedExcess gas.Gas }{ { name: "no SoVs", expectedModified: false, + expectedExcess: 0, }, { name: "evicted one", @@ -268,6 +276,7 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { sovToEvict0, }, expectedModified: true, + expectedExcess: 0, }, { name: "evicted multiple", @@ -276,6 +285,7 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { sovToEvict1, }, expectedModified: true, + expectedExcess: 3, }, { name: "evicted only 2", @@ -288,6 +298,7 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { expectedSoVs: []state.SubnetOnlyValidator{ sovToKeep, }, + expectedExcess: 6, }, { name: "chooses not to evict", @@ -298,6 +309,7 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { expectedSoVs: []state.SubnetOnlyValidator{ sovToKeep, }, + expectedExcess: 0, }, } for _, test := range tests { @@ -324,8 +336,14 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { validatorsModified, err := AdvanceTimeTo( &Backend{ Config: &config.Config{ - ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, - UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + // ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, + ValidatorFeeConfig: fee.Config{ + Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, + Target: 1, + MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), }, }, s, @@ -340,6 +358,9 @@ func TestAdvanceTimeTo_DeactivatesSoVs(t *testing.T) { test.expectedSoVs, iterator.ToSlice(activeSoVs), ) + + require.Equal(test.expectedExcess, s.GetSoVExcess()) + require.Equal(uint64(secondsToAdvance), s.GetAccruedFees()) }) } } From 5798ac0900bbe258a311c63c55ab506cf9efc4d7 Mon Sep 17 00:00:00 2001 From: yacovm Date: Sun, 3 Nov 2024 17:11:59 +0100 Subject: [PATCH 076/184] Return shallow copy of validator set in platformVM's validator manager (#3512) Signed-off-by: Yacov Manevich --- vms/platformvm/validators/manager.go | 5 +++-- vms/platformvm/vm_regression_test.go | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index 23ed1ccd8c65..bd6fecce96ef 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "maps" "time" "github.com/ava-labs/avalanchego/cache" @@ -199,7 +200,7 @@ func (m *manager) GetValidatorSet( if validatorSet, ok := validatorSetsCache.Get(targetHeight); ok { m.metrics.IncValidatorSetsCached() - return validatorSet, nil + return maps.Clone(validatorSet), nil } etnaHeight, err := m.state.GetEtnaHeight() @@ -230,7 +231,7 @@ func (m *manager) GetValidatorSet( m.metrics.IncValidatorSetsCreated() m.metrics.AddValidatorSetsDuration(duration) m.metrics.AddValidatorSetsHeightDiff(currentHeight - targetHeight) - return validatorSet, nil + return maps.Clone(validatorSet), nil } func (m *manager) getValidatorSetCache(subnetID ids.ID) cache.Cacher[uint64, map[ids.NodeID]*validators.GetValidatorOutput] { diff --git a/vms/platformvm/vm_regression_test.go b/vms/platformvm/vm_regression_test.go index 4dc69a07990e..9a3c440e75c6 100644 --- a/vms/platformvm/vm_regression_test.go +++ b/vms/platformvm/vm_regression_test.go @@ -1971,6 +1971,22 @@ func TestSubnetValidatorPopulatedToEmptyBLSKeyDiff(t *testing.T) { } } +func TestValidatorSetReturnsCopy(t *testing.T) { + require := require.New(t) + + vm, _, _ := defaultVM(t, upgradetest.Latest) + + validators1, err := vm.GetValidatorSet(context.Background(), 1, constants.PrimaryNetworkID) + require.NoError(err) + + validators2, err := vm.GetValidatorSet(context.Background(), 1, constants.PrimaryNetworkID) + require.NoError(err) + + require.NotNil(validators1[genesistest.DefaultNodeIDs[0]]) + delete(validators1, genesistest.DefaultNodeIDs[0]) + require.NotNil(validators2[genesistest.DefaultNodeIDs[0]]) +} + func TestSubnetValidatorSetAfterPrimaryNetworkValidatorRemoval(t *testing.T) { // A primary network validator and a subnet validator are running. // Primary network validator terminates its staking cycle. From 9e9d6583da46e7fea80b5aea96a4c5a2a61ef474 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Sun, 3 Nov 2024 11:15:36 -0500 Subject: [PATCH 077/184] Add `ValidatorWeightDiff` `Add` and `Sub` helpers (#3514) --- vms/platformvm/state/stakers.go | 4 +- vms/platformvm/state/state.go | 14 +++- vms/platformvm/state/state_test.go | 119 ++++++++++------------------- 3 files changed, 52 insertions(+), 85 deletions(-) diff --git a/vms/platformvm/state/stakers.go b/vms/platformvm/state/stakers.go index 14e4dcf7b1ef..f076e23ab59b 100644 --- a/vms/platformvm/state/stakers.go +++ b/vms/platformvm/state/stakers.go @@ -283,7 +283,7 @@ func (d *diffValidator) WeightDiff() (ValidatorWeightDiff, error) { } for _, staker := range d.deletedDelegators { - if err := weightDiff.Add(true, staker.Weight); err != nil { + if err := weightDiff.Sub(staker.Weight); err != nil { return ValidatorWeightDiff{}, fmt.Errorf("failed to decrease node weight diff: %w", err) } } @@ -294,7 +294,7 @@ func (d *diffValidator) WeightDiff() (ValidatorWeightDiff, error) { for addedDelegatorIterator.Next() { staker := addedDelegatorIterator.Value() - if err := weightDiff.Add(false, staker.Weight); err != nil { + if err := weightDiff.Add(staker.Weight); err != nil { return ValidatorWeightDiff{}, fmt.Errorf("failed to increase node weight diff: %w", err) } } diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 09a22f8c27de..1e45b6f07826 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -425,8 +425,16 @@ type ValidatorWeightDiff struct { Amount uint64 `serialize:"true"` } -func (v *ValidatorWeightDiff) Add(negative bool, amount uint64) error { - if v.Decrease == negative { +func (v *ValidatorWeightDiff) Add(amount uint64) error { + return v.addOrSub(false, amount) +} + +func (v *ValidatorWeightDiff) Sub(amount uint64) error { + return v.addOrSub(true, amount) +} + +func (v *ValidatorWeightDiff) addOrSub(sub bool, amount uint64) error { + if v.Decrease == sub { var err error v.Amount, err = safemath.Add(v.Amount, amount) return err @@ -436,7 +444,7 @@ func (v *ValidatorWeightDiff) Add(negative bool, amount uint64) error { v.Amount -= amount } else { v.Amount = safemath.AbsDiff(v.Amount, amount) - v.Decrease = negative + v.Decrease = sub } return nil } diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 143f673b4e0f..60b92c58dc44 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -671,135 +671,94 @@ func createPermissionlessDelegatorTx(subnetID ids.ID, delegatorData txs.Validato } func TestValidatorWeightDiff(t *testing.T) { + type op struct { + op func(*ValidatorWeightDiff, uint64) error + amount uint64 + } type test struct { name string - ops []func(*ValidatorWeightDiff) error + ops []op expected *ValidatorWeightDiff expectedErr error } + var ( + add = (*ValidatorWeightDiff).Add + sub = (*ValidatorWeightDiff).Sub + ) tests := []test{ { - name: "no ops", - ops: []func(*ValidatorWeightDiff) error{}, - expected: &ValidatorWeightDiff{}, - expectedErr: nil, + name: "no ops", + expected: &ValidatorWeightDiff{}, }, { name: "simple decrease", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) - }, + ops: []op{ + {sub, 1}, + {sub, 1}, }, expected: &ValidatorWeightDiff{ Decrease: true, Amount: 2, }, - expectedErr: nil, }, { name: "decrease overflow", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(true, math.MaxUint64) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) - }, + ops: []op{ + {sub, math.MaxUint64}, + {sub, 1}, }, - expected: &ValidatorWeightDiff{}, expectedErr: safemath.ErrOverflow, }, { name: "simple increase", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) - }, + ops: []op{ + {add, 1}, + {add, 1}, }, expected: &ValidatorWeightDiff{ Decrease: false, Amount: 2, }, - expectedErr: nil, }, { name: "increase overflow", - ops: []func(*ValidatorWeightDiff) error{ - func(d *ValidatorWeightDiff) error { - return d.Add(false, math.MaxUint64) - }, - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) - }, + ops: []op{ + {add, math.MaxUint64}, + {add, 1}, }, - expected: &ValidatorWeightDiff{}, expectedErr: safemath.ErrOverflow, }, { name: "varied use", - ops: []func(*ValidatorWeightDiff) error{ - // Add to 0 - func(d *ValidatorWeightDiff) error { - return d.Add(false, 2) // Value 2 - }, - // Subtract from positive number - func(d *ValidatorWeightDiff) error { - return d.Add(true, 1) // Value 1 - }, - // Subtract from positive number - // to make it negative - func(d *ValidatorWeightDiff) error { - return d.Add(true, 3) // Value -2 - }, - // Subtract from a negative number - func(d *ValidatorWeightDiff) error { - return d.Add(true, 3) // Value -5 - }, - // Add to a negative number - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) // Value -4 - }, - // Add to a negative number - // to make it positive - func(d *ValidatorWeightDiff) error { - return d.Add(false, 5) // Value 1 - }, - // Add to a positive number - func(d *ValidatorWeightDiff) error { - return d.Add(false, 1) // Value 2 - }, - // Get to zero - func(d *ValidatorWeightDiff) error { - return d.Add(true, 2) // Value 0 - }, - // Subtract from zero - func(d *ValidatorWeightDiff) error { - return d.Add(true, 2) // Value -2 - }, + ops: []op{ + {add, 2}, // = 2 + {sub, 1}, // = 1 + {sub, 3}, // = -2 + {sub, 3}, // = -5 + {add, 1}, // = -4 + {add, 5}, // = 1 + {add, 1}, // = 2 + {sub, 2}, // = 0 + {sub, 2}, // = -2 }, expected: &ValidatorWeightDiff{ Decrease: true, Amount: 2, }, - expectedErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require := require.New(t) - diff := &ValidatorWeightDiff{} - errs := wrappers.Errs{} + + var ( + diff = &ValidatorWeightDiff{} + errs = wrappers.Errs{} + ) for _, op := range tt.ops { - errs.Add(op(diff)) + errs.Add(op.op(diff, op.amount)) } require.ErrorIs(errs.Err, tt.expectedErr) if tt.expectedErr != nil { From 45b4507004fa348c990fae20997b45b212ab3c26 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 13:05:38 -0500 Subject: [PATCH 078/184] ACP-77: Add caching to SoV DB helpers (#3516) --- vms/platformvm/state/subnet_only_validator.go | 55 +++++- .../state/subnet_only_validator_test.go | 163 +++++++++++------- 2 files changed, 150 insertions(+), 68 deletions(-) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 79033297e18b..1e078e028ce2 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -10,9 +10,11 @@ import ( "github.com/google/btree" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/vms/platformvm/block" ) @@ -103,29 +105,64 @@ func (v SubnetOnlyValidator) immutableFieldsAreUnmodified(o SubnetOnlyValidator) v.StartTime == o.StartTime } -func getSubnetOnlyValidator(db database.KeyValueReader, validationID ids.ID) (SubnetOnlyValidator, error) { +func getSubnetOnlyValidator( + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]], + db database.KeyValueReader, + validationID ids.ID, +) (SubnetOnlyValidator, error) { + if maybeSOV, ok := cache.Get(validationID); ok { + if maybeSOV.IsNothing() { + return SubnetOnlyValidator{}, database.ErrNotFound + } + return maybeSOV.Value(), nil + } + bytes, err := db.Get(validationID[:]) + if err == database.ErrNotFound { + cache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]()) + return SubnetOnlyValidator{}, database.ErrNotFound + } if err != nil { return SubnetOnlyValidator{}, err } - vdr := SubnetOnlyValidator{ + sov := SubnetOnlyValidator{ ValidationID: validationID, } - if _, err := block.GenesisCodec.Unmarshal(bytes, &vdr); err != nil { + if _, err := block.GenesisCodec.Unmarshal(bytes, &sov); err != nil { return SubnetOnlyValidator{}, fmt.Errorf("failed to unmarshal SubnetOnlyValidator: %w", err) } - return vdr, nil + + cache.Put(validationID, maybe.Some(sov)) + return sov, nil } -func putSubnetOnlyValidator(db database.KeyValueWriter, vdr SubnetOnlyValidator) error { - bytes, err := block.GenesisCodec.Marshal(block.CodecVersion, vdr) +func putSubnetOnlyValidator( + db database.KeyValueWriter, + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]], + sov SubnetOnlyValidator, +) error { + bytes, err := block.GenesisCodec.Marshal(block.CodecVersion, sov) if err != nil { return fmt.Errorf("failed to marshal SubnetOnlyValidator: %w", err) } - return db.Put(vdr.ValidationID[:], bytes) + if err := db.Put(sov.ValidationID[:], bytes); err != nil { + return err + } + + cache.Put(sov.ValidationID, maybe.Some(sov)) + return nil } -func deleteSubnetOnlyValidator(db database.KeyValueDeleter, validationID ids.ID) error { - return db.Delete(validationID[:]) +func deleteSubnetOnlyValidator( + db database.KeyValueDeleter, + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]], + validationID ids.ID, +) error { + if err := db.Delete(validationID[:]); err != nil { + return err + } + + cache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]()) + return nil } diff --git a/vms/platformvm/state/subnet_only_validator_test.go b/vms/platformvm/state/subnet_only_validator_test.go index 4f73cc287194..3ffe080e0723 100644 --- a/vms/platformvm/state/subnet_only_validator_test.go +++ b/vms/platformvm/state/subnet_only_validator_test.go @@ -9,14 +9,14 @@ import ( "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/vms/platformvm/block" - "github.com/ava-labs/avalanchego/vms/platformvm/fx" - "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) func TestSubnetOnlyValidator_Compare(t *testing.T) { @@ -77,17 +77,6 @@ func TestSubnetOnlyValidator_Compare(t *testing.T) { func TestSubnetOnlyValidator_immutableFieldsAreUnmodified(t *testing.T) { var ( - randomSOV = func() SubnetOnlyValidator { - return SubnetOnlyValidator{ - ValidationID: ids.GenerateTestID(), - SubnetID: ids.GenerateTestID(), - NodeID: ids.GenerateTestNodeID(), - PublicKey: utils.RandomBytes(bls.PublicKeyLen), - RemainingBalanceOwner: utils.RandomBytes(32), - DeactivationOwner: utils.RandomBytes(32), - StartTime: rand.Uint64(), // #nosec G404 - } - } randomizeSOV = func(sov SubnetOnlyValidator) SubnetOnlyValidator { // Randomize unrelated fields sov.Weight = rand.Uint64() // #nosec G404 @@ -95,7 +84,7 @@ func TestSubnetOnlyValidator_immutableFieldsAreUnmodified(t *testing.T) { sov.EndAccumulatedFee = rand.Uint64() // #nosec G404 return sov } - sov = randomSOV() + sov = newSoV() ) t.Run("equal", func(t *testing.T) { @@ -103,7 +92,7 @@ func TestSubnetOnlyValidator_immutableFieldsAreUnmodified(t *testing.T) { require.True(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("everything is different", func(t *testing.T) { - v := randomizeSOV(randomSOV()) + v := randomizeSOV(newSoV()) require.True(t, sov.immutableFieldsAreUnmodified(v)) }) t.Run("different subnetID", func(t *testing.T) { @@ -138,64 +127,120 @@ func TestSubnetOnlyValidator_immutableFieldsAreUnmodified(t *testing.T) { }) } -func TestSubnetOnlyValidator_DatabaseHelpers(t *testing.T) { - require := require.New(t) - db := memdb.New() +func TestGetSubnetOnlyValidator(t *testing.T) { + var ( + sov = newSoV() + dbWithSoV = memdb.New() + dbWithoutSoV = memdb.New() + cacheWithSoV = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + cacheWithoutSoV = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + ) - sk, err := bls.NewSecretKey() - require.NoError(err) - pk := bls.PublicFromSecretKey(sk) - pkBytes := bls.PublicKeyToUncompressedBytes(pk) + require.NoError(t, putSubnetOnlyValidator(dbWithSoV, cacheWithSoV, sov)) + require.NoError(t, deleteSubnetOnlyValidator(dbWithoutSoV, cacheWithoutSoV, sov.ValidationID)) - var remainingBalanceOwner fx.Owner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - ids.GenerateTestShortID(), + tests := []struct { + name string + cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]] + db database.KeyValueReader + expectedSoV SubnetOnlyValidator + expectedErr error + expectedEntry maybe.Maybe[SubnetOnlyValidator] + }{ + { + name: "cached with validator", + cache: cacheWithSoV, + db: dbWithoutSoV, + expectedSoV: sov, + expectedEntry: maybe.Some(sov), + }, + { + name: "from disk with validator", + cache: &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10}, + db: dbWithSoV, + expectedSoV: sov, + expectedEntry: maybe.Some(sov), + }, + { + name: "cached without validator", + cache: cacheWithoutSoV, + db: dbWithSoV, + expectedErr: database.ErrNotFound, + }, + { + name: "from disk without validator", + cache: &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10}, + db: dbWithoutSoV, + expectedErr: database.ErrNotFound, }, } - remainingBalanceOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &remainingBalanceOwner) - require.NoError(err) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) - var deactivationOwner fx.Owner = &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - ids.GenerateTestShortID(), - }, + gotSoV, err := getSubnetOnlyValidator(test.cache, test.db, sov.ValidationID) + require.ErrorIs(err, test.expectedErr) + require.Equal(test.expectedSoV, gotSoV) + + cachedSoV, ok := test.cache.Get(sov.ValidationID) + require.True(ok) + require.Equal(test.expectedEntry, cachedSoV) + }) } - deactivationOwnerBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, &deactivationOwner) +} + +func TestPutSubnetOnlyValidator(t *testing.T) { + var ( + require = require.New(t) + sov = newSoV() + db = memdb.New() + cache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + ) + expectedSoVBytes, err := block.GenesisCodec.Marshal(block.CodecVersion, sov) + require.NoError(err) + + require.NoError(putSubnetOnlyValidator(db, cache, sov)) + + sovBytes, err := db.Get(sov.ValidationID[:]) require.NoError(err) + require.Equal(expectedSoVBytes, sovBytes) + + sovFromCache, ok := cache.Get(sov.ValidationID) + require.True(ok) + require.Equal(maybe.Some(sov), sovFromCache) +} + +func TestDeleteSubnetOnlyValidator(t *testing.T) { + var ( + require = require.New(t) + validationID = ids.GenerateTestID() + db = memdb.New() + cache = &cache.LRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{Size: 10} + ) + require.NoError(db.Put(validationID[:], nil)) + + require.NoError(deleteSubnetOnlyValidator(db, cache, validationID)) - vdr := SubnetOnlyValidator{ + hasSoV, err := db.Has(validationID[:]) + require.NoError(err) + require.False(hasSoV) + + sovFromCache, ok := cache.Get(validationID) + require.True(ok) + require.Equal(maybe.Nothing[SubnetOnlyValidator](), sovFromCache) +} + +func newSoV() SubnetOnlyValidator { + return SubnetOnlyValidator{ ValidationID: ids.GenerateTestID(), SubnetID: ids.GenerateTestID(), NodeID: ids.GenerateTestNodeID(), - PublicKey: pkBytes, - RemainingBalanceOwner: remainingBalanceOwnerBytes, - DeactivationOwner: deactivationOwnerBytes, + PublicKey: utils.RandomBytes(bls.PublicKeyLen), + RemainingBalanceOwner: utils.RandomBytes(32), + DeactivationOwner: utils.RandomBytes(32), StartTime: rand.Uint64(), // #nosec G404 Weight: rand.Uint64(), // #nosec G404 MinNonce: rand.Uint64(), // #nosec G404 EndAccumulatedFee: rand.Uint64(), // #nosec G404 } - - // Validator hasn't been put on disk yet - gotVdr, err := getSubnetOnlyValidator(db, vdr.ValidationID) - require.ErrorIs(err, database.ErrNotFound) - require.Zero(gotVdr) - - // Place the validator on disk - require.NoError(putSubnetOnlyValidator(db, vdr)) - - // Verify that the validator can be fetched from disk - gotVdr, err = getSubnetOnlyValidator(db, vdr.ValidationID) - require.NoError(err) - require.Equal(vdr, gotVdr) - - // Remove the validator from disk - require.NoError(deleteSubnetOnlyValidator(db, vdr.ValidationID)) - - // Verify that the validator has been removed from disk - gotVdr, err = getSubnetOnlyValidator(db, vdr.ValidationID) - require.ErrorIs(err, database.ErrNotFound) - require.Zero(gotVdr) } From 69837c104c0aca3874ed6badc220234eacebceb3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 14:31:34 -0500 Subject: [PATCH 079/184] improve caching --- vms/platformvm/state/state.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index ad09f28457b7..61a697ad31d0 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -850,7 +850,13 @@ func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { return weight, nil } - return database.WithDefault(database.GetUInt64, s.weightsDB, subnetID[:], 0) + weight, err := database.WithDefault(database.GetUInt64, s.weightsDB, subnetID[:], 0) + if err != nil { + return 0, err + } + + s.weightsCache.Put(subnetID, weight) + return weight, nil } func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { @@ -889,7 +895,13 @@ func (s *state) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool } key := subnetIDNodeID.Marshal() - return s.subnetIDNodeIDDB.Has(key) + has, err := s.subnetIDNodeIDDB.Has(key) + if err != nil { + return false, err + } + + s.subnetIDNodeIDCache.Put(subnetIDNodeID, has) + return has, nil } func (s *state) PutSubnetOnlyValidator(sov SubnetOnlyValidator) error { From a5d39308c93de8f758ef35fd08c833f5d70290ee Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 15:46:51 -0500 Subject: [PATCH 080/184] cleanup --- .../block/executor/standard_block_test.go | 2 + vms/platformvm/block/executor/verifier.go | 120 ++++++++++-------- .../block/executor/verifier_test.go | 1 + 3 files changed, 69 insertions(+), 54 deletions(-) diff --git a/vms/platformvm/block/executor/standard_block_test.go b/vms/platformvm/block/executor/standard_block_test.go index 303aad8fd282..b45eaf9fbf37 100644 --- a/vms/platformvm/block/executor/standard_block_test.go +++ b/vms/platformvm/block/executor/standard_block_test.go @@ -57,11 +57,13 @@ func TestApricotStandardBlockTimeVerification(t *testing.T) { env.blkManager.(*manager).lastAccepted = parentID chainTime := env.clk.Time().Truncate(time.Second) + onParentAccept.EXPECT().GetTimestamp().Return(chainTime).AnyTimes() onParentAccept.EXPECT().GetFeeState().Return(gas.State{}).AnyTimes() onParentAccept.EXPECT().GetSoVExcess().Return(gas.Gas(0)).AnyTimes() onParentAccept.EXPECT().GetAccruedFees().Return(uint64(0)).AnyTimes() onParentAccept.EXPECT().NumActiveSubnetOnlyValidators().Return(0).AnyTimes() + onParentAccept.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(&iterator.Empty[state.SubnetOnlyValidator]{}, nil).AnyTimes() // wrong height apricotChildBlk, err := block.NewApricotStandardBlock( diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index 56c030c05366..6f3c4fa22fb2 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -478,9 +478,7 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calcula error, ) { // Complexity is limited first to avoid processing too large of a block. - timestamp := diff.GetTimestamp() - isEtna := v.txExecutorBackend.Config.UpgradeConfig.IsEtnaActivated(timestamp) - if isEtna { + if timestamp := diff.GetTimestamp(); v.txExecutorBackend.Config.UpgradeConfig.IsEtnaActivated(timestamp) { var blockComplexity gas.Dimensions for _, tx := range txs { txComplexity, err := txfee.TxComplexity(tx.Unsigned) @@ -557,57 +555,6 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calcula } } - // After processing all the transactions, deactivate any SoVs that might not - // have sufficient fee to pay for the next second. - // - // This ensures that SoVs are not undercharged for the next second. - if isEtna { - var ( - accruedFees = diff.GetAccruedFees() - validatorFeeState = validatorfee.State{ - Current: gas.Gas(diff.NumActiveSubnetOnlyValidators()), - Excess: diff.GetSoVExcess(), - } - potentialCost = validatorFeeState.CostOf( - v.txExecutorBackend.Config.ValidatorFeeConfig, - 1, // 1 second - ) - ) - potentialAccruedFees, err := math.Add(accruedFees, potentialCost) - if err != nil { - return nil, nil, nil, err - } - - // Invariant: Proposal transactions do not impact SoV state. - sovIterator, err := diff.GetActiveSubnetOnlyValidatorsIterator() - if err != nil { - return nil, nil, nil, err - } - - var sovsToDeactivate []state.SubnetOnlyValidator - for sovIterator.Next() { - sov := sovIterator.Value() - // If the validator has exactly the right amount of fee for the next - // second we should not remove them here. - if sov.EndAccumulatedFee >= potentialAccruedFees { - break - } - - sovsToDeactivate = append(sovsToDeactivate, sov) - } - - // The iterator must be released prior to attempting to write to the - // diff. - sovIterator.Release() - - for _, sov := range sovsToDeactivate { - sov.EndAccumulatedFee = 0 - if err := diff.PutSubnetOnlyValidator(sov); err != nil { - return nil, nil, nil, err - } - } - } - if err := v.verifyUniqueInputs(parentID, inputs); err != nil { return nil, nil, nil, err } @@ -622,5 +569,70 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calcula } } + // After processing all the transactions, deactivate any SoVs that might not + // have sufficient fee to pay for the next second. + // + // This ensures that SoVs are not undercharged for the next second. + err := deactivateLowBalanceSoVs( + v.txExecutorBackend.Config.ValidatorFeeConfig, + diff, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to deactivate low balance SoVs: %w", err) + } + return inputs, atomicRequests, onAcceptFunc, nil } + +// deactivateLowBalanceSoVs deactivates any SoVs that might not have sufficient +// fees to pay for the next second. +func deactivateLowBalanceSoVs( + config validatorfee.Config, + diff state.Diff, +) error { + var ( + accruedFees = diff.GetAccruedFees() + validatorFeeState = validatorfee.State{ + Current: gas.Gas(diff.NumActiveSubnetOnlyValidators()), + Excess: diff.GetSoVExcess(), + } + potentialCost = validatorFeeState.CostOf( + config, + 1, // 1 second + ) + ) + potentialAccruedFees, err := math.Add(accruedFees, potentialCost) + if err != nil { + return fmt.Errorf("could not calculate potentially accrued fees: %w", err) + } + + // Invariant: Proposal transactions do not impact SoV state. + sovIterator, err := diff.GetActiveSubnetOnlyValidatorsIterator() + if err != nil { + return fmt.Errorf("could not iterate over active SoVs: %w", err) + } + + var sovsToDeactivate []state.SubnetOnlyValidator + for sovIterator.Next() { + sov := sovIterator.Value() + // If the validator has exactly the right amount of fee for the next + // second we should not remove them here. + if sov.EndAccumulatedFee >= potentialAccruedFees { + break + } + + sovsToDeactivate = append(sovsToDeactivate, sov) + } + + // The iterator must be released prior to attempting to write to the + // diff. + sovIterator.Release() + + for _, sov := range sovsToDeactivate { + sov.EndAccumulatedFee = 0 + if err := diff.PutSubnetOnlyValidator(sov); err != nil { + return fmt.Errorf("could not deactivate SoV %s: %w", sov.ValidationID, err) + } + } + return nil +} diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 043ec8f7caba..b4920818ce98 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -351,6 +351,7 @@ func TestVerifierVisitStandardBlock(t *testing.T) { parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) + parentState.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(&iterator.Empty[state.SubnetOnlyValidator]{}, nil).Times(1) parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) mempool.EXPECT().Remove(apricotBlk.Txs()).Times(1) From d74cdff1c55dbf62987ebc421d612dc9424fd2e0 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 17:12:25 -0500 Subject: [PATCH 081/184] Simplify test --- .../block/executor/verifier_test.go | 82 ++++--------------- 1 file changed, 15 insertions(+), 67 deletions(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index b4920818ce98..ceb848df0561 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -1217,7 +1217,7 @@ func TestBlockExecutionWithComplexity(t *testing.T) { } } -func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { +func TestDeactivateLowBalanceSoVs(t *testing.T) { sk, err := bls.NewSecretKey() require.NoError(t, err) @@ -1235,33 +1235,24 @@ func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { EndAccumulatedFee: endAccumulatedFee, } } - fractionalTimeSoV0 = newSoV(5 * units.NanoAvax) // lasts 2.5 seconds - fractionalTimeSoV1 = newSoV(5 * units.NanoAvax) // lasts 2.5 seconds - wholeTimeSoV = newSoV(8 * units.NanoAvax) // lasts 4 seconds - - fractionalSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(2 * time.Second) // evicts early rather than late - wholeSoVEvictedTime = genesistest.DefaultValidatorStartTime.Add(4 * time.Second) // evicts on time + fractionalTimeSoV0 = newSoV(1 * units.NanoAvax) // lasts .5 seconds + fractionalTimeSoV1 = newSoV(1 * units.NanoAvax) // lasts .5 seconds + wholeTimeSoV = newSoV(2 * units.NanoAvax) // lasts 1 second ) tests := []struct { name string initialSoVs []state.SubnetOnlyValidator - timestamp time.Time expectedSoVs []state.SubnetOnlyValidator }{ { - name: "no SoVs", - timestamp: genesistest.DefaultValidatorStartTime.Add(10 * time.Second), + name: "no SoVs", }, { - name: "fractional SoVs are not overcharged", + name: "fractional SoV is not undercharged", initialSoVs: []state.SubnetOnlyValidator{ fractionalTimeSoV0, }, - timestamp: fractionalSoVEvictedTime.Add(-time.Second), - expectedSoVs: []state.SubnetOnlyValidator{ - fractionalTimeSoV0, - }, }, { name: "fractional SoVs are not undercharged", @@ -1269,32 +1260,22 @@ func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { fractionalTimeSoV0, fractionalTimeSoV1, }, - timestamp: fractionalSoVEvictedTime, }, { name: "whole SoVs are not overcharged", initialSoVs: []state.SubnetOnlyValidator{ wholeTimeSoV, }, - timestamp: wholeSoVEvictedTime.Add(-time.Second), expectedSoVs: []state.SubnetOnlyValidator{ wholeTimeSoV, }, }, - { - name: "whole SoVs are not undercharged", - initialSoVs: []state.SubnetOnlyValidator{ - wholeTimeSoV, - }, - timestamp: wholeSoVEvictedTime, - }, { name: "partial eviction", initialSoVs: []state.SubnetOnlyValidator{ fractionalTimeSoV0, wholeTimeSoV, }, - timestamp: fractionalSoVEvictedTime, expectedSoVs: []state.SubnetOnlyValidator{ wholeTimeSoV, }, @@ -1309,51 +1290,18 @@ func TestBlockExecutionEvictsLowBalanceSoVs(t *testing.T) { require.NoError(s.PutSubnetOnlyValidator(sov)) } - verifier := newTestVerifier(t, s) - verifier.txExecutorBackend.Clk.Set(test.timestamp) - - wallet := txstest.NewWallet( - t, - verifier.ctx, - verifier.txExecutorBackend.Config, - s, - secp256k1fx.NewKeychain(genesis.EWOQKey), - nil, // subnetIDs - nil, // chainIDs - ) - - baseTx, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) + diff, err := state.NewDiffOn(s) require.NoError(err) - timestamp, _, err := state.NextBlockTime( - verifier.txExecutorBackend.Config.ValidatorFeeConfig, - s, - verifier.txExecutorBackend.Clk, - ) - require.NoError(err) - - lastAcceptedID := s.GetLastAccepted() - lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) - require.NoError(err) - - blk, err := block.NewBanffStandardBlock( - timestamp, - lastAcceptedID, - lastAccepted.Height()+1, - []*txs.Tx{ - baseTx, - }, - ) - require.NoError(err) - - require.NoError(blk.Visit(verifier)) - - blkID := blk.ID() - require.Contains(verifier.blkIDToState, blkID) - blockState := verifier.blkIDToState[blkID] - require.Equal(blk, blockState.statelessBlock) + config := validatorfee.Config{ + Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: gas.Price(2 * units.NanoAvax), // Min price is increased to allow fractional fees + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + } + require.NoError(deactivateLowBalanceSoVs(config, diff)) - sovs, err := blockState.onAcceptState.GetActiveSubnetOnlyValidatorsIterator() + sovs, err := diff.GetActiveSubnetOnlyValidatorsIterator() require.NoError(err) require.Equal( test.expectedSoVs, From 7cddfc0d5147ed8d0a120ac67809f1a6c09b55e8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 17:13:11 -0500 Subject: [PATCH 082/184] reduce diff --- vms/platformvm/block/executor/standard_block_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vms/platformvm/block/executor/standard_block_test.go b/vms/platformvm/block/executor/standard_block_test.go index b45eaf9fbf37..055e6c0e813a 100644 --- a/vms/platformvm/block/executor/standard_block_test.go +++ b/vms/platformvm/block/executor/standard_block_test.go @@ -57,7 +57,6 @@ func TestApricotStandardBlockTimeVerification(t *testing.T) { env.blkManager.(*manager).lastAccepted = parentID chainTime := env.clk.Time().Truncate(time.Second) - onParentAccept.EXPECT().GetTimestamp().Return(chainTime).AnyTimes() onParentAccept.EXPECT().GetFeeState().Return(gas.State{}).AnyTimes() onParentAccept.EXPECT().GetSoVExcess().Return(gas.Gas(0)).AnyTimes() From afc905487744dd89d7e78b172d771d98f4450cc8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 17:15:01 -0500 Subject: [PATCH 083/184] nit --- vms/platformvm/block/executor/verifier_test.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index ceb848df0561..d0b88c197de6 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -76,15 +76,10 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { }, txExecutorBackend: &executor.Backend{ Config: &config.Config{ - CreateAssetTxFee: genesis.LocalParams.CreateAssetTxFee, - StaticFeeConfig: genesis.LocalParams.StaticFeeConfig, - DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, - ValidatorFeeConfig: validatorfee.Config{ - Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, - Target: genesis.LocalParams.ValidatorFeeConfig.Target, - MinPrice: gas.Price(2 * units.NanoAvax), // Min price is increased to allow fractional fees - ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, - }, + CreateAssetTxFee: genesis.LocalParams.CreateAssetTxFee, + StaticFeeConfig: genesis.LocalParams.StaticFeeConfig, + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, SybilProtectionEnabled: true, UpgradeConfig: upgrades, }, From 3c8246ee5e2c0fb78893ac1c65dda21b2713ab43 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 17:16:34 -0500 Subject: [PATCH 084/184] nit --- vms/platformvm/state/chain_time_helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/state/chain_time_helpers.go b/vms/platformvm/state/chain_time_helpers.go index 17d466bcea40..f19e0491842b 100644 --- a/vms/platformvm/state/chain_time_helpers.go +++ b/vms/platformvm/state/chain_time_helpers.go @@ -49,7 +49,7 @@ func NextBlockTime( } // GetNextStakerChangeTime returns the next time a staker will be either added -// or removed to/from the validator set. If the next staker change time is +// to or removed from the validator set. If the next staker change time is // further in the future than [nextTime], then [nextTime] is returned. func GetNextStakerChangeTime( config validatorfee.Config, From 1268ed1419c9c7eefead0c5b9d878cf27a665a0f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 17:25:26 -0500 Subject: [PATCH 085/184] nit --- vms/platformvm/state/chain_time_helpers.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/state/chain_time_helpers.go b/vms/platformvm/state/chain_time_helpers.go index f19e0491842b..d31cacafb1a2 100644 --- a/vms/platformvm/state/chain_time_helpers.go +++ b/vms/platformvm/state/chain_time_helpers.go @@ -80,9 +80,17 @@ func GetNextStakerChangeTime( } } + return getNextSoVEvictionTime(config, state, nextTime) +} + +func getNextSoVEvictionTime( + config validatorfee.Config, + state Chain, + nextTime time.Time, +) (time.Time, error) { sovIterator, err := state.GetActiveSubnetOnlyValidatorsIterator() if err != nil { - return time.Time{}, err + return time.Time{}, fmt.Errorf("could not iterate over active SoVs: %w", err) } defer sovIterator.Release() @@ -98,7 +106,7 @@ func GetNextStakerChangeTime( ) remainingFunds, err := math.Sub(sov.EndAccumulatedFee, accruedFees) if err != nil { - return time.Time{}, err + return time.Time{}, fmt.Errorf("could not calculate remaining funds: %w", err) } // Calculate how many seconds the remaining funds can last for. From f7b75bc9e8efca30d86ee2932a604634fdff901e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 18:01:03 -0500 Subject: [PATCH 086/184] nits --- vms/platformvm/txs/executor/state_changes.go | 62 +++++++++++++------ .../txs/executor/state_changes_test.go | 21 ++++--- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/vms/platformvm/txs/executor/state_changes.go b/vms/platformvm/txs/executor/state_changes.go index d01675637b28..6bb1b5bce2f3 100644 --- a/vms/platformvm/txs/executor/state_changes.go +++ b/vms/platformvm/txs/executor/state_changes.go @@ -260,28 +260,56 @@ func advanceTimeTo( previousChainTime := changes.GetTimestamp() duration := uint64(newChainTime.Sub(previousChainTime) / time.Second) + advanceDynamicFeeState(backend.Config.DynamicFeeConfig, changes, duration) + sovsChanged, err := advanceValidatorFeeState( + backend.Config.ValidatorFeeConfig, + parentState, + changes, + duration, + ) + if err != nil { + return nil, false, fmt.Errorf("failed to advance validator fee state: %w", err) + } + changed = changed || sovsChanged + + changes.SetTimestamp(newChainTime) + return changes, changed, nil +} + +func advanceDynamicFeeState( + config gas.Config, + changes state.Diff, + seconds uint64, +) { dynamicFeeState := changes.GetFeeState() dynamicFeeState = dynamicFeeState.AdvanceTime( - backend.Config.DynamicFeeConfig.MaxCapacity, - backend.Config.DynamicFeeConfig.MaxPerSecond, - backend.Config.DynamicFeeConfig.TargetPerSecond, - duration, + config.MaxCapacity, + config.MaxPerSecond, + config.TargetPerSecond, + seconds, ) changes.SetFeeState(dynamicFeeState) +} +// advanceValidatorFeeState advances the validator fee state by [seconds]. SoVs +// are read from [parentState] and written to [changes] to avoid modifying state +// while an iterator is held. +func advanceValidatorFeeState( + config fee.Config, + parentState state.Chain, + changes state.Diff, + seconds uint64, +) (bool, error) { validatorFeeState := fee.State{ Current: gas.Gas(changes.NumActiveSubnetOnlyValidators()), Excess: changes.GetSoVExcess(), } - validatorCost := validatorFeeState.CostOf( - backend.Config.ValidatorFeeConfig, - duration, - ) + validatorCost := validatorFeeState.CostOf(config, seconds) accruedFees := changes.GetAccruedFees() - accruedFees, err = math.Add(accruedFees, validatorCost) + accruedFees, err := math.Add(accruedFees, validatorCost) if err != nil { - return nil, false, err + return false, fmt.Errorf("could not calculate accrued fees: %w", err) } // Invariant: It is not safe to modify the state while iterating over it, @@ -289,10 +317,11 @@ func advanceTimeTo( // ParentState must not be modified before this iterator is released. sovIterator, err := parentState.GetActiveSubnetOnlyValidatorsIterator() if err != nil { - return nil, false, err + return false, fmt.Errorf("could not iterate over active SoVs: %w", err) } defer sovIterator.Release() + var changed bool for sovIterator.Next() { sov := sovIterator.Value() if sov.EndAccumulatedFee > accruedFees { @@ -301,19 +330,16 @@ func advanceTimeTo( sov.EndAccumulatedFee = 0 // Deactivate the validator if err := changes.PutSubnetOnlyValidator(sov); err != nil { - return nil, false, err + return false, fmt.Errorf("could not deactivate SoV %s: %w", sov.ValidationID, err) } changed = true } - validatorFeeState = validatorFeeState.AdvanceTime( - backend.Config.ValidatorFeeConfig.Target, - duration, - ) + // + validatorFeeState = validatorFeeState.AdvanceTime(config.Target, seconds) changes.SetSoVExcess(validatorFeeState.Excess) changes.SetAccruedFees(accruedFees) - changes.SetTimestamp(newChainTime) - return changes, changed, nil + return changed, nil } func GetRewardsCalculator( diff --git a/vms/platformvm/txs/executor/state_changes_test.go b/vms/platformvm/txs/executor/state_changes_test.go index 944fd7253859..26088e1c8a59 100644 --- a/vms/platformvm/txs/executor/state_changes_test.go +++ b/vms/platformvm/txs/executor/state_changes_test.go @@ -256,6 +256,16 @@ func TestAdvanceTimeTo_UpdateSoVs(t *testing.T) { currentTime = genesistest.DefaultValidatorStartTime newTime = currentTime.Add(timeToAdvance) + + config = config.Config{ + ValidatorFeeConfig: fee.Config{ + Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, + Target: 1, + MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } ) tests := []struct { @@ -335,16 +345,7 @@ func TestAdvanceTimeTo_UpdateSoVs(t *testing.T) { validatorsModified, err := AdvanceTimeTo( &Backend{ - Config: &config.Config{ - // ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, - ValidatorFeeConfig: fee.Config{ - Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, - Target: 1, - MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, - ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, - }, - UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), - }, + Config: &config, }, s, newTime, From 82a249ba6557fbab08369e1debd0481e3215989c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 18:01:54 -0500 Subject: [PATCH 087/184] nit --- vms/platformvm/txs/executor/state_changes.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/txs/executor/state_changes.go b/vms/platformvm/txs/executor/state_changes.go index 6bb1b5bce2f3..392d3fc8b5a4 100644 --- a/vms/platformvm/txs/executor/state_changes.go +++ b/vms/platformvm/txs/executor/state_changes.go @@ -256,16 +256,16 @@ func advanceTimeTo( return changes, changed, nil } - // Advance the dynamic fees state + // Calculate number of seconds the time is advancing previousChainTime := changes.GetTimestamp() - duration := uint64(newChainTime.Sub(previousChainTime) / time.Second) + seconds := uint64(newChainTime.Sub(previousChainTime) / time.Second) - advanceDynamicFeeState(backend.Config.DynamicFeeConfig, changes, duration) + advanceDynamicFeeState(backend.Config.DynamicFeeConfig, changes, seconds) sovsChanged, err := advanceValidatorFeeState( backend.Config.ValidatorFeeConfig, parentState, changes, - duration, + seconds, ) if err != nil { return nil, false, fmt.Errorf("failed to advance validator fee state: %w", err) From d37e9f3b116146cf52d4e3f76944e268bdfb52d8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 19:00:33 -0500 Subject: [PATCH 088/184] nit --- vms/platformvm/state/state.go | 1 - 1 file changed, 1 deletion(-) diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 61a697ad31d0..67be6717fa44 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -1670,7 +1670,6 @@ func (s *state) loadExpiry() error { func (s *state) loadActiveSubnetOnlyValidators() error { it := s.activeDB.NewIterator() defer it.Release() - for it.Next() { key := it.Key() validationID, err := ids.ToID(key) From 9dc642a4c832f9a0305f60fcb37b8cf288253c54 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Mon, 4 Nov 2024 19:07:06 -0500 Subject: [PATCH 089/184] num -> net for possibly negative value --- vms/platformvm/state/diff.go | 2 +- vms/platformvm/state/state.go | 2 +- vms/platformvm/state/subnet_only_validator.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vms/platformvm/state/diff.go b/vms/platformvm/state/diff.go index 4b8922f74492..30ee112a41b9 100644 --- a/vms/platformvm/state/diff.go +++ b/vms/platformvm/state/diff.go @@ -213,7 +213,7 @@ func (d *diff) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[Subnet } func (d *diff) NumActiveSubnetOnlyValidators() int { - return d.parentActiveSOVs + d.sovDiff.numAddedActive + return d.parentActiveSOVs + d.sovDiff.netAddedActive } func (d *diff) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 67be6717fa44..3ec8527a258b 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -838,7 +838,7 @@ func (s *state) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[Subne } func (s *state) NumActiveSubnetOnlyValidators() int { - return s.activeSOVs.len() + s.sovDiff.numAddedActive + return s.activeSOVs.len() + s.sovDiff.netAddedActive } func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index bb9af0d78ca2..109fd23ea9f2 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -250,7 +250,7 @@ func deleteSubnetOnlyValidator( } type subnetOnlyValidatorsDiff struct { - numAddedActive int // May be negative + netAddedActive int // May be negative modifiedTotalWeight map[ids.ID]uint64 // subnetID -> totalWeight modified map[ids.ID]SubnetOnlyValidator modifiedHasNodeIDs map[subnetIDNodeID]bool @@ -344,9 +344,9 @@ func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov Subne switch { case prevActive && !newActive: - d.numAddedActive-- + d.netAddedActive-- case !prevActive && newActive: - d.numAddedActive++ + d.netAddedActive++ } if prevSOV, ok := d.modified[sov.ValidationID]; ok { From 3d81d5b85b1fd7c84d9a0c9a00eeb1b89c3efbfd Mon Sep 17 00:00:00 2001 From: marun Date: Tue, 5 Nov 2024 14:00:28 +0100 Subject: [PATCH 090/184] Add script to configure metrics and log collection from a local node (#3517) --- scripts/configure-local-metrics-collection.sh | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 scripts/configure-local-metrics-collection.sh diff --git a/scripts/configure-local-metrics-collection.sh b/scripts/configure-local-metrics-collection.sh new file mode 100755 index 000000000000..1b86f98b4050 --- /dev/null +++ b/scripts/configure-local-metrics-collection.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Configures prometheus and promtail to collect metrics and logs from +# a local node. + +API_PORT="${API_PORT:-9650}" + +LOGS_PATH="${LOGS_PATH:-${HOME}/.avalanchego/logs}" + +# Generate a uuid to uniquely identify the collected metrics +METRICS_UUID="$(uuidgen)" + +echo "Configuring metrics and log collection for a local node with API port ${API_PORT} and logs path ${LOGS_PATH}" + +PROMETHEUS_CONFIG_PATH="${HOME}/.tmpnet/prometheus/file_sd_configs" +PROMETHEUS_CONFIG_FILE="${PROMETHEUS_CONFIG_PATH}/local.json" +mkdir -p "${PROMETHEUS_CONFIG_PATH}" +cat > "${PROMETHEUS_CONFIG_FILE}" < "${PROMTAIL_CONFIG_FILE}" < Date: Tue, 5 Nov 2024 10:52:20 -0500 Subject: [PATCH 091/184] Address PR comments --- vms/platformvm/state/diff.go | 34 +++++++++++++++--------------- vms/platformvm/state/state.go | 10 +++++---- vms/platformvm/state/state_test.go | 24 +++++++++++++++++++++ 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/vms/platformvm/state/diff.go b/vms/platformvm/state/diff.go index 30ee112a41b9..317e4e210142 100644 --- a/vms/platformvm/state/diff.go +++ b/vms/platformvm/state/diff.go @@ -35,11 +35,11 @@ type diff struct { parentID ids.ID stateVersions Versions - timestamp time.Time - feeState gas.State - sovExcess gas.Gas - accruedFees uint64 - parentActiveSOVs int + timestamp time.Time + feeState gas.State + sovExcess gas.Gas + accruedFees uint64 + parentNumActiveSOVs int // Subnet ID --> supply of native asset of the subnet currentSupply map[ids.ID]uint64 @@ -79,17 +79,17 @@ func NewDiff( return nil, fmt.Errorf("%w: %s", ErrMissingParentState, parentID) } return &diff{ - parentID: parentID, - stateVersions: stateVersions, - timestamp: parentState.GetTimestamp(), - feeState: parentState.GetFeeState(), - sovExcess: parentState.GetSoVExcess(), - accruedFees: parentState.GetAccruedFees(), - parentActiveSOVs: parentState.NumActiveSubnetOnlyValidators(), - expiryDiff: newExpiryDiff(), - sovDiff: newSubnetOnlyValidatorsDiff(), - subnetOwners: make(map[ids.ID]fx.Owner), - subnetConversions: make(map[ids.ID]SubnetConversion), + parentID: parentID, + stateVersions: stateVersions, + timestamp: parentState.GetTimestamp(), + feeState: parentState.GetFeeState(), + sovExcess: parentState.GetSoVExcess(), + accruedFees: parentState.GetAccruedFees(), + parentNumActiveSOVs: parentState.NumActiveSubnetOnlyValidators(), + expiryDiff: newExpiryDiff(), + sovDiff: newSubnetOnlyValidatorsDiff(), + subnetOwners: make(map[ids.ID]fx.Owner), + subnetConversions: make(map[ids.ID]SubnetConversion), }, nil } @@ -213,7 +213,7 @@ func (d *diff) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[Subnet } func (d *diff) NumActiveSubnetOnlyValidators() int { - return d.parentActiveSOVs + d.sovDiff.netAddedActive + return d.parentNumActiveSOVs + d.sovDiff.netAddedActive } func (d *diff) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 3ec8527a258b..12eebd953132 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -872,7 +872,7 @@ func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator // getPersistedSubnetOnlyValidator returns the currently persisted // SubnetOnlyValidator with the given validationID. It is guaranteed that any -// returned validator is either active or inactive. +// returned validator is either active or inactive (not deleted). func (s *state) getPersistedSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { if sov, ok := s.activeSOVs.get(validationID); ok { return sov, nil @@ -2506,7 +2506,7 @@ func (s *state) calculateValidatorDiffs() (map[subnetIDNodeID]*validatorDiff, er subnetID: priorSOV.SubnetID, nodeID: priorSOV.effectiveNodeID(), } - diff := getOrDefault(changes, subnetIDNodeID) + diff := getOrSetDefault(changes, subnetIDNodeID) if err := diff.weightDiff.Sub(priorSOV.Weight); err != nil { return nil, err } @@ -2526,7 +2526,7 @@ func (s *state) calculateValidatorDiffs() (map[subnetIDNodeID]*validatorDiff, er subnetID: sov.SubnetID, nodeID: sov.effectiveNodeID(), } - diff := getOrDefault(changes, subnetIDNodeID) + diff := getOrSetDefault(changes, subnetIDNodeID) if err := diff.weightDiff.Add(sov.Weight); err != nil { return nil, err } @@ -2571,7 +2571,9 @@ func (s *state) writeValidatorDiffs(height uint64) error { return nil } -func getOrDefault[K comparable, V any](m map[K]*V, k K) *V { +// getOrSetDefault returns the value at k in m if it exists. If it doesn't +// exist, it sets m[k] to a new value and returns that value. +func getOrSetDefault[K comparable, V any](m map[K]*V, k K) *V { if v, ok := m[k]; ok { return v } diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 0a27b92d7dde..342365a6db46 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -1774,6 +1774,27 @@ func TestSubnetOnlyValidators(t *testing.T) { }, }, }, + { + name: "add multiple inactive", + sovs: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: sov.SubnetID, + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + }, + }, } for _, test := range tests { @@ -1788,6 +1809,9 @@ func TestSubnetOnlyValidators(t *testing.T) { subnetIDs set.Set[ids.ID] ) for _, sov := range test.initial { + // The codec creates zero length slices rather than leaving them + // as nil, so we need to populate the slices for later reflect + // based equality checks. sov.RemainingBalanceOwner = []byte{} sov.DeactivationOwner = []byte{} From cc0e0ee1ae8d3ba8cdac33eef062acfbfe4f0220 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 11:21:40 -0500 Subject: [PATCH 092/184] add comments --- vms/platformvm/block/executor/verifier.go | 3 + vms/platformvm/state/chain_time_helpers.go | 2 + vms/platformvm/txs/executor/state_changes.go | 71 +++++++++++--------- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index 6f3c4fa22fb2..1570a1b3c2ac 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -617,6 +617,9 @@ func deactivateLowBalanceSoVs( sov := sovIterator.Value() // If the validator has exactly the right amount of fee for the next // second we should not remove them here. + // + // GetActiveSubnetOnlyValidatorsIterator iterates in order of increasing + // EndAccumulatedFee, so we can break early. if sov.EndAccumulatedFee >= potentialAccruedFees { break } diff --git a/vms/platformvm/state/chain_time_helpers.go b/vms/platformvm/state/chain_time_helpers.go index d31cacafb1a2..fbf33b83e8d3 100644 --- a/vms/platformvm/state/chain_time_helpers.go +++ b/vms/platformvm/state/chain_time_helpers.go @@ -101,6 +101,8 @@ func getNextSoVEvictionTime( // Calculate the remaining funds that the next validator to evict has. var ( + // GetActiveSubnetOnlyValidatorsIterator iterates in order of increasing + // EndAccumulatedFee, so the first SoV is the next SoV to evict. sov = sovIterator.Value() accruedFees = state.GetAccruedFees() ) diff --git a/vms/platformvm/txs/executor/state_changes.go b/vms/platformvm/txs/executor/state_changes.go index 392d3fc8b5a4..da1ea5b9baaa 100644 --- a/vms/platformvm/txs/executor/state_changes.go +++ b/vms/platformvm/txs/executor/state_changes.go @@ -221,41 +221,16 @@ func advanceTimeTo( changed = true } - // Remove all expiries whose timestamp now implies they can never be - // re-issued. - // - // The expiry timestamp is the time at which it is no longer valid, so any - // expiry with a timestamp less than or equal to the new chain time can be - // removed. - // - // Ref: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/77-reinventing-subnets#registersubnetvalidatortx - // - // The expiry iterator is sorted in order of increasing timestamp. - // - // Invariant: It is not safe to modify the state while iterating over it, - // so we use the parentState's iterator rather than the changes iterator. - // ParentState must not be modified before this iterator is released. - expiryIterator, err := parentState.GetExpiryIterator() - if err != nil { - return nil, false, err - } - defer expiryIterator.Release() - - newChainTimeUnix := uint64(newChainTime.Unix()) - for expiryIterator.Next() { - expiry := expiryIterator.Value() - if expiry.Timestamp > newChainTimeUnix { - break - } - - changes.DeleteExpiry(expiry) - } - if !backend.Config.UpgradeConfig.IsEtnaActivated(newChainTime) { changes.SetTimestamp(newChainTime) return changes, changed, nil } + newChainTimeUnix := uint64(newChainTime.Unix()) + if err := removeStaleExpiries(parentState, changes, newChainTimeUnix); err != nil { + return nil, false, fmt.Errorf("failed to remove stale expiries: %w", err) + } + // Calculate number of seconds the time is advancing previousChainTime := changes.GetTimestamp() seconds := uint64(newChainTime.Sub(previousChainTime) / time.Second) @@ -276,6 +251,40 @@ func advanceTimeTo( return changes, changed, nil } +// Remove all expiries whose timestamp now implies they can never be re-issued. +// +// The expiry timestamp is the time at which it is no longer valid, so any +// expiry with a timestamp less than or equal to the new chain time can be +// removed. +// +// Ref: https://github.com/avalanche-foundation/ACPs/tree/e333b335c34c8692d84259d21bd07b2bb849dc2c/ACPs/77-reinventing-subnets#registerl1validatortx +// +// The expiry iterator is sorted in order of increasing timestamp. +func removeStaleExpiries( + parentState state.Chain, + changes state.Diff, + newChainTimeUnix uint64, +) error { + // Invariant: It is not safe to modify the state while iterating over it, so + // we use the parentState's iterator rather than the changes iterator. + // ParentState must not be modified before this iterator is released. + expiryIterator, err := parentState.GetExpiryIterator() + if err != nil { + return fmt.Errorf("could not iterate over expiries: %w", err) + } + defer expiryIterator.Release() + + for expiryIterator.Next() { + expiry := expiryIterator.Value() + if expiry.Timestamp > newChainTimeUnix { + break + } + + changes.DeleteExpiry(expiry) + } + return nil +} + func advanceDynamicFeeState( config gas.Config, changes state.Diff, @@ -324,6 +333,8 @@ func advanceValidatorFeeState( var changed bool for sovIterator.Next() { sov := sovIterator.Value() + // GetActiveSubnetOnlyValidatorsIterator iterates in order of increasing + // EndAccumulatedFee, so we can break early. if sov.EndAccumulatedFee > accruedFees { break } From ca5e0d5cff156deeed97e2974ead4838313e0a33 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 11:23:04 -0500 Subject: [PATCH 093/184] ACP-77: Implement validator state (#3388) Signed-off-by: Ceyhun Onur Signed-off-by: Ceyhun Onur Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Co-authored-by: Ceyhun Onur Co-authored-by: Darioush Jalali Co-authored-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- RELEASES.md | 9 + .../block/executor/proposal_block_test.go | 2 + .../block/executor/standard_block_test.go | 2 + .../block/executor/verifier_test.go | 5 + vms/platformvm/config/execution_config.go | 54 +- .../config/execution_config_test.go | 25 +- vms/platformvm/state/diff.go | 114 +++- vms/platformvm/state/diff_test.go | 112 ++++ vms/platformvm/state/mock_chain.go | 88 +++ vms/platformvm/state/mock_diff.go | 88 +++ vms/platformvm/state/mock_state.go | 88 +++ vms/platformvm/state/state.go | 445 ++++++++++++- vms/platformvm/state/state_test.go | 585 ++++++++++++++++++ vms/platformvm/state/subnet_only_validator.go | 265 +++++++- 14 files changed, 1830 insertions(+), 52 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 16b371b67a12..d5c4b453189e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,14 @@ # Release Notes +## Pending Release + +### Configs + +- Added P-chain configs + - `"l1-weights-cache-size"` + - `"l1-inactive-validators-cache-size"` + - `"l1-subnet-id-node-id-cache-size"` + ## [v1.11.11](https://github.com/ava-labs/avalanchego/releases/tag/v1.11.11) This version is backwards compatible to [v1.11.0](https://github.com/ava-labs/avalanchego/releases/tag/v1.11.0). It is optional, but encouraged. diff --git a/vms/platformvm/block/executor/proposal_block_test.go b/vms/platformvm/block/executor/proposal_block_test.go index c0a597d77335..44f644f8fa54 100644 --- a/vms/platformvm/block/executor/proposal_block_test.go +++ b/vms/platformvm/block/executor/proposal_block_test.go @@ -93,6 +93,7 @@ func TestApricotProposalBlockTimeVerification(t *testing.T) { onParentAccept.EXPECT().GetFeeState().Return(gas.State{}).AnyTimes() onParentAccept.EXPECT().GetSoVExcess().Return(gas.Gas(0)).AnyTimes() onParentAccept.EXPECT().GetAccruedFees().Return(uint64(0)).AnyTimes() + onParentAccept.EXPECT().NumActiveSubnetOnlyValidators().Return(0).AnyTimes() onParentAccept.EXPECT().GetCurrentStakerIterator().Return( iterator.FromSlice(&state.Staker{ @@ -165,6 +166,7 @@ func TestBanffProposalBlockTimeVerification(t *testing.T) { onParentAccept.EXPECT().GetFeeState().Return(gas.State{}).AnyTimes() onParentAccept.EXPECT().GetSoVExcess().Return(gas.Gas(0)).AnyTimes() onParentAccept.EXPECT().GetAccruedFees().Return(uint64(0)).AnyTimes() + onParentAccept.EXPECT().NumActiveSubnetOnlyValidators().Return(0).AnyTimes() onParentAccept.EXPECT().GetCurrentSupply(constants.PrimaryNetworkID).Return(uint64(1000), nil).AnyTimes() env.blkManager.(*manager).blkIDToState[parentID] = &blockState{ diff --git a/vms/platformvm/block/executor/standard_block_test.go b/vms/platformvm/block/executor/standard_block_test.go index d9ad860d3d3b..006287c04508 100644 --- a/vms/platformvm/block/executor/standard_block_test.go +++ b/vms/platformvm/block/executor/standard_block_test.go @@ -61,6 +61,7 @@ func TestApricotStandardBlockTimeVerification(t *testing.T) { onParentAccept.EXPECT().GetFeeState().Return(gas.State{}).AnyTimes() onParentAccept.EXPECT().GetSoVExcess().Return(gas.Gas(0)).AnyTimes() onParentAccept.EXPECT().GetAccruedFees().Return(uint64(0)).AnyTimes() + onParentAccept.EXPECT().NumActiveSubnetOnlyValidators().Return(0).AnyTimes() // wrong height apricotChildBlk, err := block.NewApricotStandardBlock( @@ -137,6 +138,7 @@ func TestBanffStandardBlockTimeVerification(t *testing.T) { onParentAccept.EXPECT().GetFeeState().Return(gas.State{}).AnyTimes() onParentAccept.EXPECT().GetSoVExcess().Return(gas.Gas(0)).AnyTimes() onParentAccept.EXPECT().GetAccruedFees().Return(uint64(0)).AnyTimes() + onParentAccept.EXPECT().NumActiveSubnetOnlyValidators().Return(0).AnyTimes() txID := ids.GenerateTestID() utxo := &avax.UTXO{ diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index a076616701f2..a04d93e7222d 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -105,6 +105,7 @@ func TestVerifierVisitProposalBlock(t *testing.T) { parentOnAcceptState.EXPECT().GetFeeState().Return(gas.State{}).Times(2) parentOnAcceptState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(2) parentOnAcceptState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(2) + parentOnAcceptState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(2) backend := &backend{ lastAccepted: parentID, @@ -338,6 +339,7 @@ func TestVerifierVisitStandardBlock(t *testing.T) { parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) mempool.EXPECT().Remove(apricotBlk.Txs()).Times(1) @@ -601,6 +603,7 @@ func TestBanffAbortBlockTimestampChecks(t *testing.T) { s.EXPECT().GetFeeState().Return(gas.State{}).Times(3) s.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(3) s.EXPECT().GetAccruedFees().Return(uint64(0)).Times(3) + s.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(3) onDecisionState, err := state.NewDiff(parentID, backend) require.NoError(err) @@ -700,6 +703,7 @@ func TestBanffCommitBlockTimestampChecks(t *testing.T) { s.EXPECT().GetFeeState().Return(gas.State{}).Times(3) s.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(3) s.EXPECT().GetAccruedFees().Return(uint64(0)).Times(3) + s.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(3) onDecisionState, err := state.NewDiff(parentID, backend) require.NoError(err) @@ -817,6 +821,7 @@ func TestVerifierVisitStandardBlockWithDuplicateInputs(t *testing.T) { parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) parentStatelessBlk.EXPECT().Parent().Return(grandParentID).Times(1) err = verifier.ApricotStandardBlock(blk) diff --git a/vms/platformvm/config/execution_config.go b/vms/platformvm/config/execution_config.go index e5bef1637d05..2c63c6299e58 100644 --- a/vms/platformvm/config/execution_config.go +++ b/vms/platformvm/config/execution_config.go @@ -12,34 +12,40 @@ import ( ) var DefaultExecutionConfig = ExecutionConfig{ - Network: network.DefaultConfig, - BlockCacheSize: 64 * units.MiB, - TxCacheSize: 128 * units.MiB, - TransformedSubnetTxCacheSize: 4 * units.MiB, - RewardUTXOsCacheSize: 2048, - ChainCacheSize: 2048, - ChainDBCacheSize: 2048, - BlockIDCacheSize: 8192, - FxOwnerCacheSize: 4 * units.MiB, - SubnetConversionCacheSize: 4 * units.MiB, - ChecksumsEnabled: false, - MempoolPruneFrequency: 30 * time.Minute, + Network: network.DefaultConfig, + BlockCacheSize: 64 * units.MiB, + TxCacheSize: 128 * units.MiB, + TransformedSubnetTxCacheSize: 4 * units.MiB, + RewardUTXOsCacheSize: 2048, + ChainCacheSize: 2048, + ChainDBCacheSize: 2048, + BlockIDCacheSize: 8192, + FxOwnerCacheSize: 4 * units.MiB, + SubnetConversionCacheSize: 4 * units.MiB, + L1WeightsCacheSize: 16 * units.KiB, + L1InactiveValidatorsCacheSize: 256 * units.KiB, + L1SubnetIDNodeIDCacheSize: 16 * units.KiB, + ChecksumsEnabled: false, + MempoolPruneFrequency: 30 * time.Minute, } // ExecutionConfig provides execution parameters of PlatformVM type ExecutionConfig struct { - Network network.Config `json:"network"` - BlockCacheSize int `json:"block-cache-size"` - TxCacheSize int `json:"tx-cache-size"` - TransformedSubnetTxCacheSize int `json:"transformed-subnet-tx-cache-size"` - RewardUTXOsCacheSize int `json:"reward-utxos-cache-size"` - ChainCacheSize int `json:"chain-cache-size"` - ChainDBCacheSize int `json:"chain-db-cache-size"` - BlockIDCacheSize int `json:"block-id-cache-size"` - FxOwnerCacheSize int `json:"fx-owner-cache-size"` - SubnetConversionCacheSize int `json:"subnet-conversion-cache-size"` - ChecksumsEnabled bool `json:"checksums-enabled"` - MempoolPruneFrequency time.Duration `json:"mempool-prune-frequency"` + Network network.Config `json:"network"` + BlockCacheSize int `json:"block-cache-size"` + TxCacheSize int `json:"tx-cache-size"` + TransformedSubnetTxCacheSize int `json:"transformed-subnet-tx-cache-size"` + RewardUTXOsCacheSize int `json:"reward-utxos-cache-size"` + ChainCacheSize int `json:"chain-cache-size"` + ChainDBCacheSize int `json:"chain-db-cache-size"` + BlockIDCacheSize int `json:"block-id-cache-size"` + FxOwnerCacheSize int `json:"fx-owner-cache-size"` + SubnetConversionCacheSize int `json:"subnet-conversion-cache-size"` + L1WeightsCacheSize int `json:"l1-weights-cache-size"` + L1InactiveValidatorsCacheSize int `json:"l1-inactive-validators-cache-size"` + L1SubnetIDNodeIDCacheSize int `json:"l1-subnet-id-node-id-cache-size"` + ChecksumsEnabled bool `json:"checksums-enabled"` + MempoolPruneFrequency time.Duration `json:"mempool-prune-frequency"` } // GetExecutionConfig returns an ExecutionConfig diff --git a/vms/platformvm/config/execution_config_test.go b/vms/platformvm/config/execution_config_test.go index c938c177add3..f4b077689b23 100644 --- a/vms/platformvm/config/execution_config_test.go +++ b/vms/platformvm/config/execution_config_test.go @@ -81,17 +81,20 @@ func TestExecutionConfigUnmarshal(t *testing.T) { ExpectedBloomFilterFalsePositiveProbability: 16, MaxBloomFilterFalsePositiveProbability: 17, }, - BlockCacheSize: 1, - TxCacheSize: 2, - TransformedSubnetTxCacheSize: 3, - RewardUTXOsCacheSize: 5, - ChainCacheSize: 6, - ChainDBCacheSize: 7, - BlockIDCacheSize: 8, - FxOwnerCacheSize: 9, - SubnetConversionCacheSize: 10, - ChecksumsEnabled: true, - MempoolPruneFrequency: time.Minute, + BlockCacheSize: 1, + TxCacheSize: 2, + TransformedSubnetTxCacheSize: 3, + RewardUTXOsCacheSize: 5, + ChainCacheSize: 6, + ChainDBCacheSize: 7, + BlockIDCacheSize: 8, + FxOwnerCacheSize: 9, + SubnetConversionCacheSize: 10, + L1WeightsCacheSize: 11, + L1InactiveValidatorsCacheSize: 12, + L1SubnetIDNodeIDCacheSize: 13, + ChecksumsEnabled: true, + MempoolPruneFrequency: time.Minute, } verifyInitializedStruct(t, *expected) verifyInitializedStruct(t, expected.Network) diff --git a/vms/platformvm/state/diff.go b/vms/platformvm/state/diff.go index da73854346ea..317e4e210142 100644 --- a/vms/platformvm/state/diff.go +++ b/vms/platformvm/state/diff.go @@ -35,15 +35,17 @@ type diff struct { parentID ids.ID stateVersions Versions - timestamp time.Time - feeState gas.State - sovExcess gas.Gas - accruedFees uint64 + timestamp time.Time + feeState gas.State + sovExcess gas.Gas + accruedFees uint64 + parentNumActiveSOVs int // Subnet ID --> supply of native asset of the subnet currentSupply map[ids.ID]uint64 expiryDiff *expiryDiff + sovDiff *subnetOnlyValidatorsDiff currentStakerDiffs diffStakers // map of subnetID -> nodeID -> total accrued delegatee rewards @@ -77,15 +79,17 @@ func NewDiff( return nil, fmt.Errorf("%w: %s", ErrMissingParentState, parentID) } return &diff{ - parentID: parentID, - stateVersions: stateVersions, - timestamp: parentState.GetTimestamp(), - feeState: parentState.GetFeeState(), - sovExcess: parentState.GetSoVExcess(), - accruedFees: parentState.GetAccruedFees(), - expiryDiff: newExpiryDiff(), - subnetOwners: make(map[ids.ID]fx.Owner), - subnetConversions: make(map[ids.ID]SubnetConversion), + parentID: parentID, + stateVersions: stateVersions, + timestamp: parentState.GetTimestamp(), + feeState: parentState.GetFeeState(), + sovExcess: parentState.GetSoVExcess(), + accruedFees: parentState.GetAccruedFees(), + parentNumActiveSOVs: parentState.NumActiveSubnetOnlyValidators(), + expiryDiff: newExpiryDiff(), + sovDiff: newSubnetOnlyValidatorsDiff(), + subnetOwners: make(map[ids.ID]fx.Owner), + subnetConversions: make(map[ids.ID]SubnetConversion), }, nil } @@ -194,6 +198,70 @@ func (d *diff) DeleteExpiry(entry ExpiryEntry) { d.expiryDiff.DeleteExpiry(entry) } +func (d *diff) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) { + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return nil, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + parentIterator, err := parentState.GetActiveSubnetOnlyValidatorsIterator() + if err != nil { + return nil, err + } + + return d.sovDiff.getActiveSubnetOnlyValidatorsIterator(parentIterator), nil +} + +func (d *diff) NumActiveSubnetOnlyValidators() int { + return d.parentNumActiveSOVs + d.sovDiff.netAddedActive +} + +func (d *diff) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { + if weight, modified := d.sovDiff.modifiedTotalWeight[subnetID]; modified { + return weight, nil + } + + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return 0, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + return parentState.WeightOfSubnetOnlyValidators(subnetID) +} + +func (d *diff) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { + if sov, modified := d.sovDiff.modified[validationID]; modified { + if sov.isDeleted() { + return SubnetOnlyValidator{}, database.ErrNotFound + } + return sov, nil + } + + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return SubnetOnlyValidator{}, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + return parentState.GetSubnetOnlyValidator(validationID) +} + +func (d *diff) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) { + if has, modified := d.sovDiff.hasSubnetOnlyValidator(subnetID, nodeID); modified { + return has, nil + } + + parentState, ok := d.stateVersions.GetState(d.parentID) + if !ok { + return false, fmt.Errorf("%w: %s", ErrMissingParentState, d.parentID) + } + + return parentState.HasSubnetOnlyValidator(subnetID, nodeID) +} + +func (d *diff) PutSubnetOnlyValidator(sov SubnetOnlyValidator) error { + return d.sovDiff.putSubnetOnlyValidator(d, sov) +} + func (d *diff) GetCurrentValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, error) { // If the validator was modified in this diff, return the modified // validator. @@ -504,6 +572,26 @@ func (d *diff) Apply(baseState Chain) error { baseState.DeleteExpiry(entry) } } + // Ensure that all sov deletions happen before any sov additions. This + // ensures that a subnetID+nodeID pair that was deleted and then re-added in + // a single diff can't get reordered into the addition happening first; + // which would return an error. + for _, sov := range d.sovDiff.modified { + if !sov.isDeleted() { + continue + } + if err := baseState.PutSubnetOnlyValidator(sov); err != nil { + return err + } + } + for _, sov := range d.sovDiff.modified { + if sov.isDeleted() { + continue + } + if err := baseState.PutSubnetOnlyValidator(sov); err != nil { + return err + } + } for _, subnetValidatorDiffs := range d.currentStakerDiffs.validatorDiffs { for _, validatorDiff := range subnetValidatorDiffs { switch validatorDiff.validatorStatus { diff --git a/vms/platformvm/state/diff_test.go b/vms/platformvm/state/diff_test.go index 82e376c3b5f5..9cc67ff8d9ef 100644 --- a/vms/platformvm/state/diff_test.go +++ b/vms/platformvm/state/diff_test.go @@ -4,6 +4,7 @@ package state import ( + "math/rand" "testing" "time" @@ -282,6 +283,99 @@ func TestDiffExpiry(t *testing.T) { } } +func TestDiffSubnetOnlyValidatorsErrors(t *testing.T) { + sov := SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + Weight: 1, // Not removed + } + + tests := []struct { + name string + initialEndAccumulatedFee uint64 + sov SubnetOnlyValidator + expectedErr error + }{ + { + name: "mutate active constants", + initialEndAccumulatedFee: 1, + sov: SubnetOnlyValidator{ + ValidationID: sov.ValidationID, + NodeID: ids.GenerateTestNodeID(), + }, + expectedErr: ErrMutatedSubnetOnlyValidator, + }, + { + name: "mutate inactive constants", + initialEndAccumulatedFee: 0, + sov: SubnetOnlyValidator{ + ValidationID: sov.ValidationID, + NodeID: ids.GenerateTestNodeID(), + }, + expectedErr: ErrMutatedSubnetOnlyValidator, + }, + { + name: "conflicting legacy subnetID and nodeID pair", + initialEndAccumulatedFee: 1, + sov: SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + NodeID: defaultValidatorNodeID, + }, + expectedErr: ErrConflictingSubnetOnlyValidator, + }, + { + name: "duplicate active subnetID and nodeID pair", + initialEndAccumulatedFee: 1, + sov: SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + NodeID: sov.NodeID, + }, + expectedErr: ErrDuplicateSubnetOnlyValidator, + }, + { + name: "duplicate inactive subnetID and nodeID pair", + initialEndAccumulatedFee: 0, + sov: SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + NodeID: sov.NodeID, + }, + expectedErr: ErrDuplicateSubnetOnlyValidator, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + state := newTestState(t, memdb.New()) + + require.NoError(state.PutCurrentValidator(&Staker{ + TxID: ids.GenerateTestID(), + SubnetID: sov.SubnetID, + NodeID: defaultValidatorNodeID, + })) + + sov.EndAccumulatedFee = test.initialEndAccumulatedFee + require.NoError(state.PutSubnetOnlyValidator(sov)) + + d, err := NewDiffOn(state) + require.NoError(err) + + // Initialize subnetID, weight, and endAccumulatedFee as they are + // constant among all tests. + test.sov.SubnetID = sov.SubnetID + test.sov.Weight = 1 // Not removed + test.sov.EndAccumulatedFee = rand.Uint64() //#nosec G404 + err = d.PutSubnetOnlyValidator(test.sov) + require.ErrorIs(err, test.expectedErr) + + // The invalid addition should not have modified the diff. + assertChainsEqual(t, state, d) + }) + } +} + func TestDiffCurrentValidator(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) @@ -292,6 +386,7 @@ func TestDiffCurrentValidator(t *testing.T) { state.EXPECT().GetFeeState().Return(gas.State{}).Times(1) state.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) state.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + state.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) d, err := NewDiffOn(state) require.NoError(err) @@ -328,6 +423,7 @@ func TestDiffPendingValidator(t *testing.T) { state.EXPECT().GetFeeState().Return(gas.State{}).Times(1) state.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) state.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + state.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) d, err := NewDiffOn(state) require.NoError(err) @@ -370,6 +466,7 @@ func TestDiffCurrentDelegator(t *testing.T) { state.EXPECT().GetFeeState().Return(gas.State{}).Times(1) state.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) state.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + state.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) d, err := NewDiffOn(state) require.NoError(err) @@ -415,6 +512,7 @@ func TestDiffPendingDelegator(t *testing.T) { state.EXPECT().GetFeeState().Return(gas.State{}).Times(1) state.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) state.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + state.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) d, err := NewDiffOn(state) require.NoError(err) @@ -554,6 +652,7 @@ func TestDiffTx(t *testing.T) { state.EXPECT().GetFeeState().Return(gas.State{}).Times(1) state.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) state.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + state.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) d, err := NewDiffOn(state) require.NoError(err) @@ -653,6 +752,7 @@ func TestDiffUTXO(t *testing.T) { state.EXPECT().GetFeeState().Return(gas.State{}).Times(1) state.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) state.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) + state.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) d, err := NewDiffOn(state) require.NoError(err) @@ -707,6 +807,18 @@ func assertChainsEqual(t *testing.T, expected, actual Chain) { ) } + expectedActiveSOVsIterator, expectedErr := expected.GetActiveSubnetOnlyValidatorsIterator() + actualActiveSOVsIterator, actualErr := actual.GetActiveSubnetOnlyValidatorsIterator() + require.Equal(expectedErr, actualErr) + if expectedErr == nil { + require.Equal( + iterator.ToSlice(expectedActiveSOVsIterator), + iterator.ToSlice(actualActiveSOVsIterator), + ) + } + + require.Equal(expected.NumActiveSubnetOnlyValidators(), actual.NumActiveSubnetOnlyValidators()) + expectedCurrentStakerIterator, expectedErr := expected.GetCurrentStakerIterator() actualCurrentStakerIterator, actualErr := actual.GetCurrentStakerIterator() require.Equal(expectedErr, actualErr) diff --git a/vms/platformvm/state/mock_chain.go b/vms/platformvm/state/mock_chain.go index 56c495924511..5a6d21b2dd12 100644 --- a/vms/platformvm/state/mock_chain.go +++ b/vms/platformvm/state/mock_chain.go @@ -204,6 +204,21 @@ func (mr *MockChainMockRecorder) GetAccruedFees() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccruedFees", reflect.TypeOf((*MockChain)(nil).GetAccruedFees)) } +// GetActiveSubnetOnlyValidatorsIterator mocks base method. +func (m *MockChain) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActiveSubnetOnlyValidatorsIterator") + ret0, _ := ret[0].(iterator.Iterator[SubnetOnlyValidator]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetActiveSubnetOnlyValidatorsIterator indicates an expected call of GetActiveSubnetOnlyValidatorsIterator. +func (mr *MockChainMockRecorder) GetActiveSubnetOnlyValidatorsIterator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSubnetOnlyValidatorsIterator", reflect.TypeOf((*MockChain)(nil).GetActiveSubnetOnlyValidatorsIterator)) +} + // GetCurrentDelegatorIterator mocks base method. func (m *MockChain) GetCurrentDelegatorIterator(subnetID ids.ID, nodeID ids.NodeID) (iterator.Iterator[*Staker], error) { m.ctrl.T.Helper() @@ -382,6 +397,21 @@ func (mr *MockChainMockRecorder) GetSubnetConversion(subnetID any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnetConversion", reflect.TypeOf((*MockChain)(nil).GetSubnetConversion), subnetID) } +// GetSubnetOnlyValidator mocks base method. +func (m *MockChain) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubnetOnlyValidator", validationID) + ret0, _ := ret[0].(SubnetOnlyValidator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubnetOnlyValidator indicates an expected call of GetSubnetOnlyValidator. +func (mr *MockChainMockRecorder) GetSubnetOnlyValidator(validationID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnetOnlyValidator", reflect.TypeOf((*MockChain)(nil).GetSubnetOnlyValidator), validationID) +} + // GetSubnetOwner mocks base method. func (m *MockChain) GetSubnetOwner(subnetID ids.ID) (fx.Owner, error) { m.ctrl.T.Helper() @@ -472,6 +502,35 @@ func (mr *MockChainMockRecorder) HasExpiry(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasExpiry", reflect.TypeOf((*MockChain)(nil).HasExpiry), arg0) } +// HasSubnetOnlyValidator mocks base method. +func (m *MockChain) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasSubnetOnlyValidator", subnetID, nodeID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HasSubnetOnlyValidator indicates an expected call of HasSubnetOnlyValidator. +func (mr *MockChainMockRecorder) HasSubnetOnlyValidator(subnetID, nodeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasSubnetOnlyValidator", reflect.TypeOf((*MockChain)(nil).HasSubnetOnlyValidator), subnetID, nodeID) +} + +// NumActiveSubnetOnlyValidators mocks base method. +func (m *MockChain) NumActiveSubnetOnlyValidators() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NumActiveSubnetOnlyValidators") + ret0, _ := ret[0].(int) + return ret0 +} + +// NumActiveSubnetOnlyValidators indicates an expected call of NumActiveSubnetOnlyValidators. +func (mr *MockChainMockRecorder) NumActiveSubnetOnlyValidators() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumActiveSubnetOnlyValidators", reflect.TypeOf((*MockChain)(nil).NumActiveSubnetOnlyValidators)) +} + // PutCurrentDelegator mocks base method. func (m *MockChain) PutCurrentDelegator(staker *Staker) { m.ctrl.T.Helper() @@ -536,6 +595,20 @@ func (mr *MockChainMockRecorder) PutPendingValidator(staker any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutPendingValidator", reflect.TypeOf((*MockChain)(nil).PutPendingValidator), staker) } +// PutSubnetOnlyValidator mocks base method. +func (m *MockChain) PutSubnetOnlyValidator(sov SubnetOnlyValidator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutSubnetOnlyValidator", sov) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutSubnetOnlyValidator indicates an expected call of PutSubnetOnlyValidator. +func (mr *MockChainMockRecorder) PutSubnetOnlyValidator(sov any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutSubnetOnlyValidator", reflect.TypeOf((*MockChain)(nil).PutSubnetOnlyValidator), sov) +} + // SetAccruedFees mocks base method. func (m *MockChain) SetAccruedFees(f uint64) { m.ctrl.T.Helper() @@ -633,3 +706,18 @@ func (mr *MockChainMockRecorder) SetTimestamp(tm any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTimestamp", reflect.TypeOf((*MockChain)(nil).SetTimestamp), tm) } + +// WeightOfSubnetOnlyValidators mocks base method. +func (m *MockChain) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WeightOfSubnetOnlyValidators", subnetID) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WeightOfSubnetOnlyValidators indicates an expected call of WeightOfSubnetOnlyValidators. +func (mr *MockChainMockRecorder) WeightOfSubnetOnlyValidators(subnetID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WeightOfSubnetOnlyValidators", reflect.TypeOf((*MockChain)(nil).WeightOfSubnetOnlyValidators), subnetID) +} diff --git a/vms/platformvm/state/mock_diff.go b/vms/platformvm/state/mock_diff.go index b8362386af96..30f17a781227 100644 --- a/vms/platformvm/state/mock_diff.go +++ b/vms/platformvm/state/mock_diff.go @@ -218,6 +218,21 @@ func (mr *MockDiffMockRecorder) GetAccruedFees() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccruedFees", reflect.TypeOf((*MockDiff)(nil).GetAccruedFees)) } +// GetActiveSubnetOnlyValidatorsIterator mocks base method. +func (m *MockDiff) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActiveSubnetOnlyValidatorsIterator") + ret0, _ := ret[0].(iterator.Iterator[SubnetOnlyValidator]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetActiveSubnetOnlyValidatorsIterator indicates an expected call of GetActiveSubnetOnlyValidatorsIterator. +func (mr *MockDiffMockRecorder) GetActiveSubnetOnlyValidatorsIterator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSubnetOnlyValidatorsIterator", reflect.TypeOf((*MockDiff)(nil).GetActiveSubnetOnlyValidatorsIterator)) +} + // GetCurrentDelegatorIterator mocks base method. func (m *MockDiff) GetCurrentDelegatorIterator(subnetID ids.ID, nodeID ids.NodeID) (iterator.Iterator[*Staker], error) { m.ctrl.T.Helper() @@ -396,6 +411,21 @@ func (mr *MockDiffMockRecorder) GetSubnetConversion(subnetID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnetConversion", reflect.TypeOf((*MockDiff)(nil).GetSubnetConversion), subnetID) } +// GetSubnetOnlyValidator mocks base method. +func (m *MockDiff) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubnetOnlyValidator", validationID) + ret0, _ := ret[0].(SubnetOnlyValidator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubnetOnlyValidator indicates an expected call of GetSubnetOnlyValidator. +func (mr *MockDiffMockRecorder) GetSubnetOnlyValidator(validationID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnetOnlyValidator", reflect.TypeOf((*MockDiff)(nil).GetSubnetOnlyValidator), validationID) +} + // GetSubnetOwner mocks base method. func (m *MockDiff) GetSubnetOwner(subnetID ids.ID) (fx.Owner, error) { m.ctrl.T.Helper() @@ -486,6 +516,35 @@ func (mr *MockDiffMockRecorder) HasExpiry(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasExpiry", reflect.TypeOf((*MockDiff)(nil).HasExpiry), arg0) } +// HasSubnetOnlyValidator mocks base method. +func (m *MockDiff) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasSubnetOnlyValidator", subnetID, nodeID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HasSubnetOnlyValidator indicates an expected call of HasSubnetOnlyValidator. +func (mr *MockDiffMockRecorder) HasSubnetOnlyValidator(subnetID, nodeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasSubnetOnlyValidator", reflect.TypeOf((*MockDiff)(nil).HasSubnetOnlyValidator), subnetID, nodeID) +} + +// NumActiveSubnetOnlyValidators mocks base method. +func (m *MockDiff) NumActiveSubnetOnlyValidators() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NumActiveSubnetOnlyValidators") + ret0, _ := ret[0].(int) + return ret0 +} + +// NumActiveSubnetOnlyValidators indicates an expected call of NumActiveSubnetOnlyValidators. +func (mr *MockDiffMockRecorder) NumActiveSubnetOnlyValidators() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumActiveSubnetOnlyValidators", reflect.TypeOf((*MockDiff)(nil).NumActiveSubnetOnlyValidators)) +} + // PutCurrentDelegator mocks base method. func (m *MockDiff) PutCurrentDelegator(staker *Staker) { m.ctrl.T.Helper() @@ -550,6 +609,20 @@ func (mr *MockDiffMockRecorder) PutPendingValidator(staker any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutPendingValidator", reflect.TypeOf((*MockDiff)(nil).PutPendingValidator), staker) } +// PutSubnetOnlyValidator mocks base method. +func (m *MockDiff) PutSubnetOnlyValidator(sov SubnetOnlyValidator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutSubnetOnlyValidator", sov) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutSubnetOnlyValidator indicates an expected call of PutSubnetOnlyValidator. +func (mr *MockDiffMockRecorder) PutSubnetOnlyValidator(sov any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutSubnetOnlyValidator", reflect.TypeOf((*MockDiff)(nil).PutSubnetOnlyValidator), sov) +} + // SetAccruedFees mocks base method. func (m *MockDiff) SetAccruedFees(f uint64) { m.ctrl.T.Helper() @@ -647,3 +720,18 @@ func (mr *MockDiffMockRecorder) SetTimestamp(tm any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTimestamp", reflect.TypeOf((*MockDiff)(nil).SetTimestamp), tm) } + +// WeightOfSubnetOnlyValidators mocks base method. +func (m *MockDiff) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WeightOfSubnetOnlyValidators", subnetID) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WeightOfSubnetOnlyValidators indicates an expected call of WeightOfSubnetOnlyValidators. +func (mr *MockDiffMockRecorder) WeightOfSubnetOnlyValidators(subnetID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WeightOfSubnetOnlyValidators", reflect.TypeOf((*MockDiff)(nil).WeightOfSubnetOnlyValidators), subnetID) +} diff --git a/vms/platformvm/state/mock_state.go b/vms/platformvm/state/mock_state.go index 5f88902cf9eb..3deadddbe14e 100644 --- a/vms/platformvm/state/mock_state.go +++ b/vms/platformvm/state/mock_state.go @@ -319,6 +319,21 @@ func (mr *MockStateMockRecorder) GetAccruedFees() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccruedFees", reflect.TypeOf((*MockState)(nil).GetAccruedFees)) } +// GetActiveSubnetOnlyValidatorsIterator mocks base method. +func (m *MockState) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetActiveSubnetOnlyValidatorsIterator") + ret0, _ := ret[0].(iterator.Iterator[SubnetOnlyValidator]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetActiveSubnetOnlyValidatorsIterator indicates an expected call of GetActiveSubnetOnlyValidatorsIterator. +func (mr *MockStateMockRecorder) GetActiveSubnetOnlyValidatorsIterator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSubnetOnlyValidatorsIterator", reflect.TypeOf((*MockState)(nil).GetActiveSubnetOnlyValidatorsIterator)) +} + // GetBlockIDAtHeight mocks base method. func (m *MockState) GetBlockIDAtHeight(height uint64) (ids.ID, error) { m.ctrl.T.Helper() @@ -616,6 +631,21 @@ func (mr *MockStateMockRecorder) GetSubnetIDs() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnetIDs", reflect.TypeOf((*MockState)(nil).GetSubnetIDs)) } +// GetSubnetOnlyValidator mocks base method. +func (m *MockState) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubnetOnlyValidator", validationID) + ret0, _ := ret[0].(SubnetOnlyValidator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubnetOnlyValidator indicates an expected call of GetSubnetOnlyValidator. +func (mr *MockStateMockRecorder) GetSubnetOnlyValidator(validationID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnetOnlyValidator", reflect.TypeOf((*MockState)(nil).GetSubnetOnlyValidator), validationID) +} + // GetSubnetOwner mocks base method. func (m *MockState) GetSubnetOwner(subnetID ids.ID) (fx.Owner, error) { m.ctrl.T.Helper() @@ -722,6 +752,35 @@ func (mr *MockStateMockRecorder) HasExpiry(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasExpiry", reflect.TypeOf((*MockState)(nil).HasExpiry), arg0) } +// HasSubnetOnlyValidator mocks base method. +func (m *MockState) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasSubnetOnlyValidator", subnetID, nodeID) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HasSubnetOnlyValidator indicates an expected call of HasSubnetOnlyValidator. +func (mr *MockStateMockRecorder) HasSubnetOnlyValidator(subnetID, nodeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasSubnetOnlyValidator", reflect.TypeOf((*MockState)(nil).HasSubnetOnlyValidator), subnetID, nodeID) +} + +// NumActiveSubnetOnlyValidators mocks base method. +func (m *MockState) NumActiveSubnetOnlyValidators() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NumActiveSubnetOnlyValidators") + ret0, _ := ret[0].(int) + return ret0 +} + +// NumActiveSubnetOnlyValidators indicates an expected call of NumActiveSubnetOnlyValidators. +func (mr *MockStateMockRecorder) NumActiveSubnetOnlyValidators() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumActiveSubnetOnlyValidators", reflect.TypeOf((*MockState)(nil).NumActiveSubnetOnlyValidators)) +} + // PutCurrentDelegator mocks base method. func (m *MockState) PutCurrentDelegator(staker *Staker) { m.ctrl.T.Helper() @@ -786,6 +845,20 @@ func (mr *MockStateMockRecorder) PutPendingValidator(staker any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutPendingValidator", reflect.TypeOf((*MockState)(nil).PutPendingValidator), staker) } +// PutSubnetOnlyValidator mocks base method. +func (m *MockState) PutSubnetOnlyValidator(sov SubnetOnlyValidator) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutSubnetOnlyValidator", sov) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutSubnetOnlyValidator indicates an expected call of PutSubnetOnlyValidator. +func (mr *MockStateMockRecorder) PutSubnetOnlyValidator(sov any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutSubnetOnlyValidator", reflect.TypeOf((*MockState)(nil).PutSubnetOnlyValidator), sov) +} + // ReindexBlocks mocks base method. func (m *MockState) ReindexBlocks(lock sync.Locker, log logging.Logger) error { m.ctrl.T.Helper() @@ -950,3 +1023,18 @@ func (mr *MockStateMockRecorder) UTXOIDs(addr, previous, limit any) *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UTXOIDs", reflect.TypeOf((*MockState)(nil).UTXOIDs), addr, previous, limit) } + +// WeightOfSubnetOnlyValidators mocks base method. +func (m *MockState) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WeightOfSubnetOnlyValidators", subnetID) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WeightOfSubnetOnlyValidators indicates an expected call of WeightOfSubnetOnlyValidators. +func (mr *MockStateMockRecorder) WeightOfSubnetOnlyValidators(subnetID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WeightOfSubnetOnlyValidators", reflect.TypeOf((*MockState)(nil).WeightOfSubnetOnlyValidators), subnetID) +} diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 1e45b6f07826..12eebd953132 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -34,6 +34,7 @@ import ( "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/utils/timer" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -86,6 +87,11 @@ var ( SupplyPrefix = []byte("supply") ChainPrefix = []byte("chain") ExpiryReplayProtectionPrefix = []byte("expiryReplayProtection") + SubnetOnlyPrefix = []byte("subnetOnly") + WeightsPrefix = []byte("weights") + SubnetIDNodeIDPrefix = []byte("subnetIDNodeID") + ActivePrefix = []byte("active") + InactivePrefix = []byte("inactive") SingletonPrefix = []byte("singleton") EtnaHeightKey = []byte("etna height") @@ -98,12 +104,15 @@ var ( HeightsIndexedKey = []byte("heights indexed") InitializedKey = []byte("initialized") BlocksReindexedKey = []byte("blocks reindexed") + + emptySoVCache = &cache.Empty[ids.ID, maybe.Maybe[SubnetOnlyValidator]]{} ) // Chain collects all methods to manage the state of the chain for block // execution. type Chain interface { Expiry + SubnetOnlyValidators Stakers avax.UTXOAdder avax.UTXOGetter @@ -267,6 +276,15 @@ type stateBlk struct { * | | '-. subnetDelegator * | | '-. list * | | '-- txID -> nil + * | |-. subnetOnly + * | | |-. weights + * | | | '-- subnetID -> weight + * | | |-. subnetIDNodeID + * | | | '-- subnetID+nodeID -> validationID + * | | |-. active + * | | | '-- validationID -> subnetOnlyValidator + * | | '-. inactive + * | | '-- validationID -> subnetOnlyValidator * | |-. weight diffs * | | '-- subnet+height+nodeID -> weightChange * | '-. pub key diffs @@ -323,6 +341,17 @@ type state struct { expiryDiff *expiryDiff expiryDB database.Database + activeSOVs *activeSubnetOnlyValidators + sovDiff *subnetOnlyValidatorsDiff + subnetOnlyValidatorsDB database.Database + weightsCache cache.Cacher[ids.ID, uint64] // subnetID -> total SoV weight + weightsDB database.Database + subnetIDNodeIDCache cache.Cacher[subnetIDNodeID, bool] // subnetID+nodeID -> is validator + subnetIDNodeIDDB database.Database + activeDB database.Database + inactiveCache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]] // validationID -> SubnetOnlyValidator + inactiveDB database.Database + currentStakers *baseStakers pendingStakers *baseStakers @@ -536,9 +565,57 @@ func New( pendingSubnetValidatorBaseDB := prefixdb.New(SubnetValidatorPrefix, pendingValidatorsDB) pendingSubnetDelegatorBaseDB := prefixdb.New(SubnetDelegatorPrefix, pendingValidatorsDB) + subnetOnlyValidatorsDB := prefixdb.New(SubnetOnlyPrefix, validatorsDB) + validatorWeightDiffsDB := prefixdb.New(ValidatorWeightDiffsPrefix, validatorsDB) validatorPublicKeyDiffsDB := prefixdb.New(ValidatorPublicKeyDiffsPrefix, validatorsDB) + weightsCache, err := metercacher.New( + "sov_weights_cache", + metricsReg, + cache.NewSizedLRU[ids.ID, uint64](execCfg.L1WeightsCacheSize, func(ids.ID, uint64) int { + return ids.IDLen + wrappers.LongLen + }), + ) + if err != nil { + return nil, err + } + + inactiveSOVsCache, err := metercacher.New( + "sov_inactive_cache", + metricsReg, + cache.NewSizedLRU[ids.ID, maybe.Maybe[SubnetOnlyValidator]]( + execCfg.L1InactiveValidatorsCacheSize, + func(_ ids.ID, maybeSOV maybe.Maybe[SubnetOnlyValidator]) int { + const ( + sovOverhead = ids.IDLen + ids.NodeIDLen + 4*wrappers.LongLen + 3*constants.PointerOverhead + maybeSOVOverhead = wrappers.BoolLen + sovOverhead + entryOverhead = ids.IDLen + maybeSOVOverhead + ) + if maybeSOV.IsNothing() { + return entryOverhead + } + + sov := maybeSOV.Value() + return entryOverhead + len(sov.PublicKey) + len(sov.RemainingBalanceOwner) + len(sov.DeactivationOwner) + }, + ), + ) + if err != nil { + return nil, err + } + + subnetIDNodeIDCache, err := metercacher.New( + "sov_subnet_id_node_id_cache", + metricsReg, + cache.NewSizedLRU[subnetIDNodeID, bool](execCfg.L1SubnetIDNodeIDCacheSize, func(subnetIDNodeID, bool) int { + return ids.IDLen + ids.NodeIDLen + wrappers.BoolLen + }), + ) + if err != nil { + return nil, err + } + txCache, err := metercacher.New( "tx_cache", metricsReg, @@ -648,6 +725,17 @@ func New( expiryDiff: newExpiryDiff(), expiryDB: prefixdb.New(ExpiryReplayProtectionPrefix, baseDB), + activeSOVs: newActiveSubnetOnlyValidators(), + sovDiff: newSubnetOnlyValidatorsDiff(), + subnetOnlyValidatorsDB: subnetOnlyValidatorsDB, + weightsCache: weightsCache, + weightsDB: prefixdb.New(WeightsPrefix, subnetOnlyValidatorsDB), + subnetIDNodeIDCache: subnetIDNodeIDCache, + subnetIDNodeIDDB: prefixdb.New(SubnetIDNodeIDPrefix, subnetOnlyValidatorsDB), + activeDB: prefixdb.New(ActivePrefix, subnetOnlyValidatorsDB), + inactiveCache: inactiveSOVsCache, + inactiveDB: prefixdb.New(InactivePrefix, subnetOnlyValidatorsDB), + currentStakers: newBaseStakers(), pendingStakers: newBaseStakers(), @@ -743,6 +831,83 @@ func (s *state) DeleteExpiry(entry ExpiryEntry) { s.expiryDiff.DeleteExpiry(entry) } +func (s *state) GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) { + return s.sovDiff.getActiveSubnetOnlyValidatorsIterator( + s.activeSOVs.newIterator(), + ), nil +} + +func (s *state) NumActiveSubnetOnlyValidators() int { + return s.activeSOVs.len() + s.sovDiff.netAddedActive +} + +func (s *state) WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) { + if weight, modified := s.sovDiff.modifiedTotalWeight[subnetID]; modified { + return weight, nil + } + + if weight, ok := s.weightsCache.Get(subnetID); ok { + return weight, nil + } + + weight, err := database.WithDefault(database.GetUInt64, s.weightsDB, subnetID[:], 0) + if err != nil { + return 0, err + } + + s.weightsCache.Put(subnetID, weight) + return weight, nil +} + +func (s *state) GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { + if sov, modified := s.sovDiff.modified[validationID]; modified { + if sov.isDeleted() { + return SubnetOnlyValidator{}, database.ErrNotFound + } + return sov, nil + } + + return s.getPersistedSubnetOnlyValidator(validationID) +} + +// getPersistedSubnetOnlyValidator returns the currently persisted +// SubnetOnlyValidator with the given validationID. It is guaranteed that any +// returned validator is either active or inactive (not deleted). +func (s *state) getPersistedSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) { + if sov, ok := s.activeSOVs.get(validationID); ok { + return sov, nil + } + + return getSubnetOnlyValidator(s.inactiveCache, s.inactiveDB, validationID) +} + +func (s *state) HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) { + if has, modified := s.sovDiff.hasSubnetOnlyValidator(subnetID, nodeID); modified { + return has, nil + } + + subnetIDNodeID := subnetIDNodeID{ + subnetID: subnetID, + nodeID: nodeID, + } + if has, ok := s.subnetIDNodeIDCache.Get(subnetIDNodeID); ok { + return has, nil + } + + key := subnetIDNodeID.Marshal() + has, err := s.subnetIDNodeIDDB.Has(key) + if err != nil { + return false, err + } + + s.subnetIDNodeIDCache.Put(subnetIDNodeID, has) + return has, nil +} + +func (s *state) PutSubnetOnlyValidator(sov SubnetOnlyValidator) error { + return s.sovDiff.putSubnetOnlyValidator(s, sov) +} + func (s *state) GetCurrentValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, error) { return s.currentStakers.GetValidator(subnetID, nodeID) } @@ -1406,6 +1571,7 @@ func (s *state) load() error { return errors.Join( s.loadMetadata(), s.loadExpiry(), + s.loadActiveSubnetOnlyValidators(), s.loadCurrentValidators(), s.loadPendingValidators(), s.initValidatorSets(), @@ -1501,6 +1667,32 @@ func (s *state) loadExpiry() error { return nil } +func (s *state) loadActiveSubnetOnlyValidators() error { + it := s.activeDB.NewIterator() + defer it.Release() + for it.Next() { + key := it.Key() + validationID, err := ids.ToID(key) + if err != nil { + return fmt.Errorf("failed to unmarshal ValidationID during load: %w", err) + } + + var ( + value = it.Value() + sov = SubnetOnlyValidator{ + ValidationID: validationID, + } + ) + if _, err := block.GenesisCodec.Unmarshal(value, &sov); err != nil { + return fmt.Errorf("failed to unmarshal SubnetOnlyValidator: %w", err) + } + + s.activeSOVs.put(sov) + } + + return nil +} + func (s *state) loadCurrentValidators() error { s.currentStakers = newBaseStakers() @@ -1755,14 +1947,59 @@ func (s *state) loadPendingValidators() error { ) } -// Invariant: initValidatorSets requires loadCurrentValidators to have already -// been called. +// Invariant: initValidatorSets requires loadActiveSubnetOnlyValidators and +// loadCurrentValidators to have already been called. func (s *state) initValidatorSets() error { if s.validators.NumSubnets() != 0 { // Enforce the invariant that the validator set is empty here. return errValidatorSetAlreadyPopulated } + // Load active ACP-77 validators + if err := s.activeSOVs.addStakersToValidatorManager(s.validators); err != nil { + return err + } + + // Load inactive ACP-77 validator weights + // + // TODO: L1s with no active weight should not be held in memory. + it := s.weightsDB.NewIterator() + defer it.Release() + + for it.Next() { + subnetID, err := ids.ToID(it.Key()) + if err != nil { + return err + } + + totalWeight, err := database.ParseUInt64(it.Value()) + if err != nil { + return err + } + + // It is required for the SoVs to be loaded first so that the total + // weight is equal to the active weights here. + activeWeight, err := s.validators.TotalWeight(subnetID) + if err != nil { + return err + } + + inactiveWeight, err := safemath.Sub(totalWeight, activeWeight) + if err != nil { + // This should never happen, as the total weight should always be at + // least the sum of the active weights. + return err + } + if inactiveWeight == 0 { + continue + } + + if err := s.validators.AddStaker(subnetID, ids.EmptyNodeID, nil, ids.Empty, inactiveWeight); err != nil { + return err + } + } + + // Load primary network and non-ACP77 validators primaryNetworkValidators := s.currentStakers.validators[constants.PrimaryNetworkID] for subnetID, subnetValidators := range s.currentStakers.validators { for nodeID, subnetValidator := range subnetValidators { @@ -1816,6 +2053,7 @@ func (s *state) write(updateValidators bool, height uint64) error { s.writeCurrentStakers(codecVersion), s.writePendingStakers(), s.WriteValidatorMetadata(s.currentValidatorList, s.currentSubnetValidatorList, codecVersion), // Must be called after writeCurrentStakers + s.writeSubnetOnlyValidators(), s.writeTXs(), s.writeRewardUTXOs(), s.writeUTXOs(), @@ -1832,6 +2070,11 @@ func (s *state) write(updateValidators bool, height uint64) error { func (s *state) Close() error { return errors.Join( s.expiryDB.Close(), + s.weightsDB.Close(), + s.subnetIDNodeIDDB.Close(), + s.activeDB.Close(), + s.inactiveDB.Close(), + s.subnetOnlyValidatorsDB.Close(), s.pendingSubnetValidatorBaseDB.Close(), s.pendingSubnetDelegatorBaseDB.Close(), s.pendingDelegatorBaseDB.Close(), @@ -2080,7 +2323,10 @@ func (s *state) getInheritedPublicKey(nodeID ids.NodeID) (*bls.PublicKey, error) // updateValidatorManager updates the validator manager with the pending // validator set changes. // -// This function must be called prior to writeCurrentStakers. +// This function must be called prior to writeCurrentStakers and +// writeSubnetOnlyValidators. +// +// TODO: L1s with no active weight should not be held in memory. func (s *state) updateValidatorManager(updateValidators bool) error { if !updateValidators { return nil @@ -2132,6 +2378,65 @@ func (s *state) updateValidatorManager(updateValidators bool) error { } } + // Remove all deleted SoV validators. This must be done before adding new + // SoV validators to support the case where a validator is removed and then + // immediately re-added with a different validationID. + for validationID, sov := range s.sovDiff.modified { + if !sov.isDeleted() { + continue + } + + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) + if err == database.ErrNotFound { + // Deleting a non-existent validator is a noop. This can happen if + // the validator was added and then immediately removed. + continue + } + if err != nil { + return err + } + + if err := s.validators.RemoveWeight(priorSOV.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight); err != nil { + return err + } + } + + // Now that the removed SoV validators have been deleted, perform additions + // and modifications. + for validationID, sov := range s.sovDiff.modified { + if sov.isDeleted() { + continue + } + + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) + switch err { + case nil: + // Modifying an existing validator + if priorSOV.isActive() == sov.isActive() { + // This validator's active status isn't changing. This means + // the effectiveNodeIDs are equal. + nodeID := sov.effectiveNodeID() + if priorSOV.Weight < sov.Weight { + err = s.validators.AddWeight(sov.SubnetID, nodeID, sov.Weight-priorSOV.Weight) + } else if priorSOV.Weight > sov.Weight { + err = s.validators.RemoveWeight(sov.SubnetID, nodeID, priorSOV.Weight-sov.Weight) + } + } else { + // This validator's active status is changing. + err = errors.Join( + s.validators.RemoveWeight(sov.SubnetID, priorSOV.effectiveNodeID(), priorSOV.Weight), + addSoVToValidatorManager(s.validators, sov), + ) + } + case database.ErrNotFound: + // Adding a new validator + err = addSoVToValidatorManager(s.validators, sov) + } + if err != nil { + return err + } + } + // Update the stake metrics totalWeight, err := s.validators.TotalWeight(constants.PrimaryNetworkID) if err != nil { @@ -2192,6 +2497,42 @@ func (s *state) calculateValidatorDiffs() (map[subnetIDNodeID]*validatorDiff, er } } + // Calculate the changes to the ACP-77 validator set + for validationID, sov := range s.sovDiff.modified { + priorSOV, err := s.getPersistedSubnetOnlyValidator(validationID) + if err == nil { + // Delete the prior validator + subnetIDNodeID := subnetIDNodeID{ + subnetID: priorSOV.SubnetID, + nodeID: priorSOV.effectiveNodeID(), + } + diff := getOrSetDefault(changes, subnetIDNodeID) + if err := diff.weightDiff.Sub(priorSOV.Weight); err != nil { + return nil, err + } + diff.prevPublicKey = priorSOV.effectivePublicKeyBytes() + } + if err != database.ErrNotFound && err != nil { + return nil, err + } + + // If the validator is being removed, we shouldn't work to re-add it. + if sov.isDeleted() { + continue + } + + // Add the new validator + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.effectiveNodeID(), + } + diff := getOrSetDefault(changes, subnetIDNodeID) + if err := diff.weightDiff.Add(sov.Weight); err != nil { + return nil, err + } + diff.newPublicKey = sov.effectivePublicKeyBytes() + } + return changes, nil } @@ -2230,6 +2571,18 @@ func (s *state) writeValidatorDiffs(height uint64) error { return nil } +// getOrSetDefault returns the value at k in m if it exists. If it doesn't +// exist, it sets m[k] to a new value and returns that value. +func getOrSetDefault[K comparable, V any](m map[K]*V, k K) *V { + if v, ok := m[k]; ok { + return v + } + + v := new(V) + m[k] = v + return v +} + func (s *state) writeCurrentStakers(codecVersion uint16) error { for subnetID, validatorDiffs := range s.currentStakers.validatorDiffs { // Select db to write to @@ -2384,6 +2737,92 @@ func writePendingDiff( return nil } +func (s *state) writeSubnetOnlyValidators() error { + // Write modified weights + for subnetID, weight := range s.sovDiff.modifiedTotalWeight { + var err error + if weight == 0 { + err = s.weightsDB.Delete(subnetID[:]) + } else { + err = database.PutUInt64(s.weightsDB, subnetID[:], weight) + } + if err != nil { + return err + } + + s.weightsCache.Put(subnetID, weight) + } + + // The SoV diff application is split into two loops to ensure that all + // deletions to the subnetIDNodeIDDB happen prior to any additions. + // Otherwise replacing an SoV by deleting it and then re-adding it with a + // different validationID could result in an inconsistent state. + for validationID, sov := range s.sovDiff.modified { + // Delete the prior validator if it exists + var err error + if s.activeSOVs.delete(validationID) { + err = deleteSubnetOnlyValidator(s.activeDB, emptySoVCache, validationID) + } else { + err = deleteSubnetOnlyValidator(s.inactiveDB, s.inactiveCache, validationID) + } + if err != nil { + return err + } + + if !sov.isDeleted() { + continue + } + + var ( + subnetIDNodeID = subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + subnetIDNodeIDKey = subnetIDNodeID.Marshal() + ) + if err := s.subnetIDNodeIDDB.Delete(subnetIDNodeIDKey); err != nil { + return err + } + + s.subnetIDNodeIDCache.Put(subnetIDNodeID, false) + } + + for validationID, sov := range s.sovDiff.modified { + if sov.isDeleted() { + continue + } + + // Update the subnetIDNodeID mapping + var ( + subnetIDNodeID = subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + subnetIDNodeIDKey = subnetIDNodeID.Marshal() + ) + if err := s.subnetIDNodeIDDB.Put(subnetIDNodeIDKey, validationID[:]); err != nil { + return err + } + + s.subnetIDNodeIDCache.Put(subnetIDNodeID, true) + + // Add the new validator + var err error + if sov.isActive() { + s.activeSOVs.put(sov) + err = putSubnetOnlyValidator(s.activeDB, emptySoVCache, sov) + } else { + err = putSubnetOnlyValidator(s.inactiveDB, s.inactiveCache, sov) + } + if err != nil { + return err + } + } + + s.sovDiff = newSubnetOnlyValidatorsDiff() + return nil +} + func (s *state) writeTXs() error { for txID, txStatus := range s.addedTxs { txID := txID diff --git a/vms/platformvm/state/state_test.go b/vms/platformvm/state/state_test.go index 60b92c58dc44..342365a6db46 100644 --- a/vms/platformvm/state/state_test.go +++ b/vms/platformvm/state/state_test.go @@ -6,6 +6,7 @@ package state import ( "bytes" "context" + "maps" "math" "math/rand" "sync" @@ -24,6 +25,7 @@ import ( "github.com/ava-labs/avalanchego/snow/choices" "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" @@ -1503,3 +1505,586 @@ func TestStateExpiryCommitAndLoad(t *testing.T) { require.NoError(err) require.False(has) } + +func TestSubnetOnlyValidators(t *testing.T) { + sov := SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + } + + sk, err := bls.NewSecretKey() + require.NoError(t, err) + pk := bls.PublicFromSecretKey(sk) + pkBytes := bls.PublicKeyToUncompressedBytes(pk) + + otherSK, err := bls.NewSecretKey() + require.NoError(t, err) + otherPK := bls.PublicFromSecretKey(otherSK) + otherPKBytes := bls.PublicKeyToUncompressedBytes(otherPK) + + tests := []struct { + name string + initial []SubnetOnlyValidator + sovs []SubnetOnlyValidator + }{ + { + name: "empty noop", + }, + { + name: "initially active not modified", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + }, + { + name: "initially inactive not modified", + initial: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + }, + }, + { + name: "initially active removed", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 0, // Removed + }, + }, + }, + { + name: "initially inactive removed", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 0, // Removed + }, + }, + }, + { + name: "increase active weight", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 2, // Increased + EndAccumulatedFee: 1, // Active + }, + }, + }, + { + name: "decrease active weight", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 2, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Decreased + EndAccumulatedFee: 1, // Active + }, + }, + }, + { + name: "deactivate", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + }, + }, + { + name: "reactivate", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + }, + { + name: "update multiple times", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 2, // Not removed + EndAccumulatedFee: 1, // Active + }, + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 3, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + }, + { + name: "change validationID", + initial: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 0, // Removed + }, + { + ValidationID: ids.GenerateTestID(), + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: otherPKBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + }, + }, + { + name: "added and removed", + sovs: []SubnetOnlyValidator{ + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 1, // Active + }, + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 0, // Removed + }, + }, + }, + { + name: "add multiple inactive", + sovs: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: sov.SubnetID, + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + { + ValidationID: sov.ValidationID, + SubnetID: sov.SubnetID, + NodeID: sov.NodeID, + PublicKey: pkBytes, + Weight: 1, // Not removed + EndAccumulatedFee: 0, // Inactive + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + db := memdb.New() + state := newTestState(t, db) + + var ( + initialSOVs = make(map[ids.ID]SubnetOnlyValidator) + subnetIDs set.Set[ids.ID] + ) + for _, sov := range test.initial { + // The codec creates zero length slices rather than leaving them + // as nil, so we need to populate the slices for later reflect + // based equality checks. + sov.RemainingBalanceOwner = []byte{} + sov.DeactivationOwner = []byte{} + + require.NoError(state.PutSubnetOnlyValidator(sov)) + initialSOVs[sov.ValidationID] = sov + subnetIDs.Add(sov.SubnetID) + } + + state.SetHeight(0) + require.NoError(state.Commit()) + + d, err := NewDiffOn(state) + require.NoError(err) + + expectedSOVs := maps.Clone(initialSOVs) + for _, sov := range test.sovs { + sov.RemainingBalanceOwner = []byte{} + sov.DeactivationOwner = []byte{} + + require.NoError(d.PutSubnetOnlyValidator(sov)) + expectedSOVs[sov.ValidationID] = sov + subnetIDs.Add(sov.SubnetID) + } + + verifyChain := func(chain Chain) { + for _, expectedSOV := range expectedSOVs { + if !expectedSOV.isDeleted() { + continue + } + + sov, err := chain.GetSubnetOnlyValidator(expectedSOV.ValidationID) + require.ErrorIs(err, database.ErrNotFound) + require.Zero(sov) + } + + var ( + weights = make(map[ids.ID]uint64) + expectedActive []SubnetOnlyValidator + ) + for _, expectedSOV := range expectedSOVs { + if expectedSOV.isDeleted() { + continue + } + + sov, err := chain.GetSubnetOnlyValidator(expectedSOV.ValidationID) + require.NoError(err) + require.Equal(expectedSOV, sov) + + has, err := chain.HasSubnetOnlyValidator(expectedSOV.SubnetID, expectedSOV.NodeID) + require.NoError(err) + require.True(has) + + weights[sov.SubnetID] += sov.Weight + if expectedSOV.isActive() { + expectedActive = append(expectedActive, expectedSOV) + } + } + utils.Sort(expectedActive) + + activeIterator, err := chain.GetActiveSubnetOnlyValidatorsIterator() + require.NoError(err) + require.Equal( + expectedActive, + iterator.ToSlice(activeIterator), + ) + + require.Equal(len(expectedActive), chain.NumActiveSubnetOnlyValidators()) + + for subnetID, expectedWeight := range weights { + weight, err := chain.WeightOfSubnetOnlyValidators(subnetID) + require.NoError(err) + require.Equal(expectedWeight, weight) + } + } + + verifyChain(d) + require.NoError(d.Apply(state)) + verifyChain(d) + verifyChain(state) + assertChainsEqual(t, state, d) + + state.SetHeight(1) + require.NoError(state.Commit()) + verifyChain(d) + verifyChain(state) + assertChainsEqual(t, state, d) + + // Verify that the subnetID+nodeID -> validationID mapping is correct. + var populatedSubnetIDNodeIDs set.Set[subnetIDNodeID] + for _, sov := range expectedSOVs { + if sov.isDeleted() { + continue + } + + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + populatedSubnetIDNodeIDs.Add(subnetIDNodeID) + + subnetIDNodeIDKey := subnetIDNodeID.Marshal() + validatorID, err := database.GetID(state.subnetIDNodeIDDB, subnetIDNodeIDKey) + require.NoError(err) + require.Equal(sov.ValidationID, validatorID) + } + for _, sov := range expectedSOVs { + if !sov.isDeleted() { + continue + } + + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + if populatedSubnetIDNodeIDs.Contains(subnetIDNodeID) { + continue + } + + subnetIDNodeIDKey := subnetIDNodeID.Marshal() + has, err := state.subnetIDNodeIDDB.Has(subnetIDNodeIDKey) + require.NoError(err) + require.False(has) + } + + sovsToValidatorSet := func( + sovs map[ids.ID]SubnetOnlyValidator, + subnetID ids.ID, + ) map[ids.NodeID]*validators.GetValidatorOutput { + validatorSet := make(map[ids.NodeID]*validators.GetValidatorOutput) + for _, sov := range sovs { + if sov.SubnetID != subnetID || sov.isDeleted() { + continue + } + + nodeID := sov.effectiveNodeID() + vdr, ok := validatorSet[nodeID] + if !ok { + vdr = &validators.GetValidatorOutput{ + NodeID: nodeID, + PublicKey: sov.effectivePublicKey(), + } + validatorSet[nodeID] = vdr + } + vdr.Weight += sov.Weight + } + return validatorSet + } + + reloadedState := newTestState(t, db) + for subnetID := range subnetIDs { + expectedEndValidatorSet := sovsToValidatorSet(expectedSOVs, subnetID) + endValidatorSet := state.validators.GetMap(subnetID) + require.Equal(expectedEndValidatorSet, endValidatorSet) + + reloadedEndValidatorSet := reloadedState.validators.GetMap(subnetID) + require.Equal(expectedEndValidatorSet, reloadedEndValidatorSet) + + require.NoError(state.ApplyValidatorWeightDiffs(context.Background(), endValidatorSet, 1, 1, subnetID)) + require.NoError(state.ApplyValidatorPublicKeyDiffs(context.Background(), endValidatorSet, 1, 1, subnetID)) + + initialValidatorSet := sovsToValidatorSet(initialSOVs, subnetID) + require.Equal(initialValidatorSet, endValidatorSet) + } + }) + } +} + +// TestLoadSubnetOnlyValidatorAndLegacy tests that the state can be loaded when +// there is a mix of legacy validators and subnet-only validators in the same +// subnet. +func TestLoadSubnetOnlyValidatorAndLegacy(t *testing.T) { + var ( + require = require.New(t) + db = memdb.New() + state = newTestState(t, db) + subnetID = ids.GenerateTestID() + weight uint64 = 1 + ) + + unsignedAddSubnetValidator := createPermissionlessValidatorTx( + t, + subnetID, + txs.Validator{ + NodeID: defaultValidatorNodeID, + End: genesistest.DefaultValidatorEndTimeUnix, + Wght: weight, + }, + ) + addSubnetValidator := &txs.Tx{Unsigned: unsignedAddSubnetValidator} + require.NoError(addSubnetValidator.Initialize(txs.Codec)) + state.AddTx(addSubnetValidator, status.Committed) + + legacyStaker := &Staker{ + TxID: addSubnetValidator.ID(), + NodeID: defaultValidatorNodeID, + PublicKey: nil, + SubnetID: subnetID, + Weight: weight, + StartTime: genesistest.DefaultValidatorStartTime, + EndTime: genesistest.DefaultValidatorEndTime, + PotentialReward: 0, + } + require.NoError(state.PutCurrentValidator(legacyStaker)) + + sk, err := bls.NewSecretKey() + require.NoError(err) + pk := bls.PublicFromSecretKey(sk) + pkBytes := bls.PublicKeyToUncompressedBytes(pk) + + sov := SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: legacyStaker.SubnetID, + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + RemainingBalanceOwner: utils.RandomBytes(32), + DeactivationOwner: utils.RandomBytes(32), + StartTime: 1, + Weight: 2, + MinNonce: 3, + EndAccumulatedFee: 4, + } + require.NoError(state.PutSubnetOnlyValidator(sov)) + + state.SetHeight(1) + require.NoError(state.Commit()) + + expectedValidatorSet := state.validators.GetMap(subnetID) + + state = newTestState(t, db) + + validatorSet := state.validators.GetMap(subnetID) + require.Equal(expectedValidatorSet, validatorSet) +} + +// TestSubnetOnlyValidatorAfterLegacyRemoval verifies that a legacy validator +// can be replaced by an SoV in the same block. +func TestSubnetOnlyValidatorAfterLegacyRemoval(t *testing.T) { + require := require.New(t) + + db := memdb.New() + state := newTestState(t, db) + + legacyStaker := &Staker{ + TxID: ids.GenerateTestID(), + NodeID: defaultValidatorNodeID, + PublicKey: nil, + SubnetID: ids.GenerateTestID(), + Weight: 1, + StartTime: genesistest.DefaultValidatorStartTime, + EndTime: genesistest.DefaultValidatorEndTime, + PotentialReward: 0, + } + require.NoError(state.PutCurrentValidator(legacyStaker)) + + state.SetHeight(1) + require.NoError(state.Commit()) + + state.DeleteCurrentValidator(legacyStaker) + + sov := SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: legacyStaker.SubnetID, + NodeID: legacyStaker.NodeID, + PublicKey: utils.RandomBytes(bls.PublicKeyLen), + RemainingBalanceOwner: utils.RandomBytes(32), + DeactivationOwner: utils.RandomBytes(32), + StartTime: 1, + Weight: 2, + MinNonce: 3, + EndAccumulatedFee: 4, + } + require.NoError(state.PutSubnetOnlyValidator(sov)) + + state.SetHeight(2) + require.NoError(state.Commit()) +} diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 1e078e028ce2..109fd23ea9f2 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -13,7 +13,11 @@ import ( "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/iterator" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/maybe" "github.com/ava-labs/avalanchego/vms/platformvm/block" ) @@ -22,9 +26,51 @@ var ( _ btree.LessFunc[SubnetOnlyValidator] = SubnetOnlyValidator.Less _ utils.Sortable[SubnetOnlyValidator] = SubnetOnlyValidator{} - ErrMutatedSubnetOnlyValidator = errors.New("subnet only validator contains mutated constant fields") + ErrMutatedSubnetOnlyValidator = errors.New("subnet-only validator contains mutated constant fields") + ErrConflictingSubnetOnlyValidator = errors.New("subnet-only validator contains conflicting subnetID + nodeID pair") + ErrDuplicateSubnetOnlyValidator = errors.New("subnet-only validator contains duplicate subnetID + nodeID pair") ) +type SubnetOnlyValidators interface { + // GetActiveSubnetOnlyValidatorsIterator returns an iterator of all the + // active subnet-only validators in increasing order of EndAccumulatedFee. + GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) + + // NumActiveSubnetOnlyValidators returns the number of currently active + // subnet-only validators. + NumActiveSubnetOnlyValidators() int + + // WeightOfSubnetOnlyValidators returns the total active and inactive weight + // of subnet-only validators on [subnetID]. + WeightOfSubnetOnlyValidators(subnetID ids.ID) (uint64, error) + + // GetSubnetOnlyValidator returns the validator with [validationID] if it + // exists. If the validator does not exist, [err] will equal + // [database.ErrNotFound]. + GetSubnetOnlyValidator(validationID ids.ID) (SubnetOnlyValidator, error) + + // HasSubnetOnlyValidator returns the validator with [validationID] if it + // exists. + HasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, error) + + // PutSubnetOnlyValidator inserts [sov] as a validator. If the weight of the + // validator is 0, the validator is removed. + // + // If inserting this validator attempts to modify any of the constant fields + // of the subnet-only validator struct, an error will be returned. + // + // If inserting this validator would cause the total weight of subnet-only + // validators on a subnet to overflow MaxUint64, an error will be returned. + // + // If inserting this validator would cause there to be multiple validators + // with the same subnetID and nodeID pair to exist at the same time, an + // error will be returned. + // + // If an SoV with the same validationID as a previously removed SoV is + // added, the behavior is undefined. + PutSubnetOnlyValidator(sov SubnetOnlyValidator) error +} + // SubnetOnlyValidator defines an ACP-77 validator. For a given ValidationID, it // is expected for SubnetID, NodeID, PublicKey, RemainingBalanceOwner, and // StartTime to be constant. @@ -105,6 +151,42 @@ func (v SubnetOnlyValidator) immutableFieldsAreUnmodified(o SubnetOnlyValidator) v.StartTime == o.StartTime } +func (v SubnetOnlyValidator) isDeleted() bool { + return v.Weight == 0 +} + +func (v SubnetOnlyValidator) isActive() bool { + return v.Weight != 0 && v.EndAccumulatedFee != 0 +} + +func (v SubnetOnlyValidator) effectiveValidationID() ids.ID { + if v.isActive() { + return v.ValidationID + } + return ids.Empty +} + +func (v SubnetOnlyValidator) effectiveNodeID() ids.NodeID { + if v.isActive() { + return v.NodeID + } + return ids.EmptyNodeID +} + +func (v SubnetOnlyValidator) effectivePublicKey() *bls.PublicKey { + if v.isActive() { + return bls.PublicKeyFromValidUncompressedBytes(v.PublicKey) + } + return nil +} + +func (v SubnetOnlyValidator) effectivePublicKeyBytes() []byte { + if v.isActive() { + return v.PublicKey + } + return nil +} + func getSubnetOnlyValidator( cache cache.Cacher[ids.ID, maybe.Maybe[SubnetOnlyValidator]], db database.KeyValueReader, @@ -166,3 +248,184 @@ func deleteSubnetOnlyValidator( cache.Put(validationID, maybe.Nothing[SubnetOnlyValidator]()) return nil } + +type subnetOnlyValidatorsDiff struct { + netAddedActive int // May be negative + modifiedTotalWeight map[ids.ID]uint64 // subnetID -> totalWeight + modified map[ids.ID]SubnetOnlyValidator + modifiedHasNodeIDs map[subnetIDNodeID]bool + active *btree.BTreeG[SubnetOnlyValidator] +} + +func newSubnetOnlyValidatorsDiff() *subnetOnlyValidatorsDiff { + return &subnetOnlyValidatorsDiff{ + modifiedTotalWeight: make(map[ids.ID]uint64), + modified: make(map[ids.ID]SubnetOnlyValidator), + modifiedHasNodeIDs: make(map[subnetIDNodeID]bool), + active: btree.NewG(defaultTreeDegree, SubnetOnlyValidator.Less), + } +} + +// getActiveSubnetOnlyValidatorsIterator takes in the parent iterator, removes +// all modified validators, and then adds all modified active validators. +func (d *subnetOnlyValidatorsDiff) getActiveSubnetOnlyValidatorsIterator(parentIterator iterator.Iterator[SubnetOnlyValidator]) iterator.Iterator[SubnetOnlyValidator] { + return iterator.Merge( + SubnetOnlyValidator.Less, + iterator.Filter(parentIterator, func(sov SubnetOnlyValidator) bool { + _, ok := d.modified[sov.ValidationID] + return ok + }), + iterator.FromTree(d.active), + ) +} + +func (d *subnetOnlyValidatorsDiff) hasSubnetOnlyValidator(subnetID ids.ID, nodeID ids.NodeID) (bool, bool) { + subnetIDNodeID := subnetIDNodeID{ + subnetID: subnetID, + nodeID: nodeID, + } + has, modified := d.modifiedHasNodeIDs[subnetIDNodeID] + return has, modified +} + +func (d *subnetOnlyValidatorsDiff) putSubnetOnlyValidator(state Chain, sov SubnetOnlyValidator) error { + var ( + prevWeight uint64 + prevActive bool + newActive = sov.isActive() + ) + switch priorSOV, err := state.GetSubnetOnlyValidator(sov.ValidationID); err { + case nil: + if !priorSOV.immutableFieldsAreUnmodified(sov) { + return ErrMutatedSubnetOnlyValidator + } + + prevWeight = priorSOV.Weight + prevActive = priorSOV.isActive() + case database.ErrNotFound: + // Verify that there is not a legacy subnet validator with the same + // subnetID+nodeID as this L1 validator. + _, err := state.GetCurrentValidator(sov.SubnetID, sov.NodeID) + if err == nil { + return ErrConflictingSubnetOnlyValidator + } + if err != database.ErrNotFound { + return err + } + + has, err := state.HasSubnetOnlyValidator(sov.SubnetID, sov.NodeID) + if err != nil { + return err + } + if has { + return ErrDuplicateSubnetOnlyValidator + } + default: + return err + } + + if prevWeight != sov.Weight { + weight, err := state.WeightOfSubnetOnlyValidators(sov.SubnetID) + if err != nil { + return err + } + + weight, err = math.Sub(weight, prevWeight) + if err != nil { + return err + } + weight, err = math.Add(weight, sov.Weight) + if err != nil { + return err + } + + d.modifiedTotalWeight[sov.SubnetID] = weight + } + + switch { + case prevActive && !newActive: + d.netAddedActive-- + case !prevActive && newActive: + d.netAddedActive++ + } + + if prevSOV, ok := d.modified[sov.ValidationID]; ok { + d.active.Delete(prevSOV) + } + d.modified[sov.ValidationID] = sov + + subnetIDNodeID := subnetIDNodeID{ + subnetID: sov.SubnetID, + nodeID: sov.NodeID, + } + d.modifiedHasNodeIDs[subnetIDNodeID] = !sov.isDeleted() + if sov.isActive() { + d.active.ReplaceOrInsert(sov) + } + return nil +} + +type activeSubnetOnlyValidators struct { + lookup map[ids.ID]SubnetOnlyValidator + tree *btree.BTreeG[SubnetOnlyValidator] +} + +func newActiveSubnetOnlyValidators() *activeSubnetOnlyValidators { + return &activeSubnetOnlyValidators{ + lookup: make(map[ids.ID]SubnetOnlyValidator), + tree: btree.NewG(defaultTreeDegree, SubnetOnlyValidator.Less), + } +} + +func (a *activeSubnetOnlyValidators) get(validationID ids.ID) (SubnetOnlyValidator, bool) { + sov, ok := a.lookup[validationID] + return sov, ok +} + +func (a *activeSubnetOnlyValidators) put(sov SubnetOnlyValidator) { + a.lookup[sov.ValidationID] = sov + a.tree.ReplaceOrInsert(sov) +} + +func (a *activeSubnetOnlyValidators) delete(validationID ids.ID) bool { + sov, ok := a.lookup[validationID] + if !ok { + return false + } + + delete(a.lookup, validationID) + a.tree.Delete(sov) + return true +} + +func (a *activeSubnetOnlyValidators) len() int { + return len(a.lookup) +} + +func (a *activeSubnetOnlyValidators) newIterator() iterator.Iterator[SubnetOnlyValidator] { + return iterator.FromTree(a.tree) +} + +func (a *activeSubnetOnlyValidators) addStakersToValidatorManager(vdrs validators.Manager) error { + for validationID, sov := range a.lookup { + pk := bls.PublicKeyFromValidUncompressedBytes(sov.PublicKey) + if err := vdrs.AddStaker(sov.SubnetID, sov.NodeID, pk, validationID, sov.Weight); err != nil { + return err + } + } + return nil +} + +func addSoVToValidatorManager(vdrs validators.Manager, sov SubnetOnlyValidator) error { + nodeID := sov.effectiveNodeID() + if vdrs.GetWeight(sov.SubnetID, nodeID) != 0 { + return vdrs.AddWeight(sov.SubnetID, nodeID, sov.Weight) + } + return vdrs.AddStaker( + sov.SubnetID, + nodeID, + sov.effectivePublicKey(), + sov.effectiveValidationID(), + sov.Weight, + ) +} From 915eb715bb54db64eddc09d08500c166af2abc5c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 11:28:40 -0500 Subject: [PATCH 094/184] nit --- vms/platformvm/txs/executor/state_changes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/txs/executor/state_changes.go b/vms/platformvm/txs/executor/state_changes.go index da1ea5b9baaa..0992fd178fcb 100644 --- a/vms/platformvm/txs/executor/state_changes.go +++ b/vms/platformvm/txs/executor/state_changes.go @@ -258,8 +258,6 @@ func advanceTimeTo( // removed. // // Ref: https://github.com/avalanche-foundation/ACPs/tree/e333b335c34c8692d84259d21bd07b2bb849dc2c/ACPs/77-reinventing-subnets#registerl1validatortx -// -// The expiry iterator is sorted in order of increasing timestamp. func removeStaleExpiries( parentState state.Chain, changes state.Diff, @@ -276,6 +274,8 @@ func removeStaleExpiries( for expiryIterator.Next() { expiry := expiryIterator.Value() + // The expiry iterator is sorted in order of increasing timestamp. Once + // we find a non-expired expiry, we can break. if expiry.Timestamp > newChainTimeUnix { break } From 02e43ba147d5d1859606acff92ba842f74f2adb6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 12:11:40 -0500 Subject: [PATCH 095/184] ACP-77: Reduce block gossip log level (#3519) --- snow/engine/snowman/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snow/engine/snowman/engine.go b/snow/engine/snowman/engine.go index 2e454402dad5..6f1ff9960f32 100644 --- a/snow/engine/snowman/engine.go +++ b/snow/engine/snowman/engine.go @@ -176,7 +176,7 @@ func (e *Engine) Gossip(ctx context.Context) error { // nodes with a large amount of stake weight. vdrID, ok := e.ConnectedValidators.SampleValidator() if !ok { - e.Ctx.Log.Warn("skipping block gossip", + e.Ctx.Log.Debug("skipping block gossip", zap.String("reason", "no connected validators"), ) return nil From 7171c2ab7b87595cc2324c80bd8be36a9b9f46f2 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 12:17:27 -0500 Subject: [PATCH 096/184] ACP-77: Implement ids.ID#Append (#3518) --- ids/id.go | 19 +++++++++++ ids/id_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/ids/id.go b/ids/id.go index 91eacfdbd08f..ea0993ef25d7 100644 --- a/ids/id.go +++ b/ids/id.go @@ -107,6 +107,25 @@ func (id ID) Prefix(prefixes ...uint64) ID { return hashing.ComputeHash256Array(packer.Bytes) } +// Append this id with the provided suffixes and re-hash the result. This +// returns a new ID and does not modify the original ID. +// +// This is used to generate ACP-77 validationIDs. +// +// Ref: https://github.com/avalanche-foundation/ACPs/tree/e333b335c34c8692d84259d21bd07b2bb849dc2c/ACPs/77-reinventing-subnets#convertsubnettol1tx +func (id ID) Append(suffixes ...uint32) ID { + packer := wrappers.Packer{ + Bytes: make([]byte, IDLen+len(suffixes)*wrappers.IntLen), + } + + packer.PackFixedBytes(id[:]) + for _, suffix := range suffixes { + packer.PackInt(suffix) + } + + return hashing.ComputeHash256Array(packer.Bytes) +} + // XOR this id and the provided id and return the resulting id. // // Note: this id is not modified. diff --git a/ids/id_test.go b/ids/id_test.go index 930a323e614c..b062aaa0b0c2 100644 --- a/ids/id_test.go +++ b/ids/id_test.go @@ -6,12 +6,14 @@ package ids import ( "encoding/json" "fmt" + "slices" "testing" "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/cb58" + "github.com/ava-labs/avalanchego/utils/hashing" ) func TestID(t *testing.T) { @@ -25,6 +27,90 @@ func TestID(t *testing.T) { require.Equal(prefixed, id.Prefix(0)) } +func TestIDPrefix(t *testing.T) { + id := GenerateTestID() + tests := []struct { + name string + id ID + prefix []uint64 + expectedPreimage []byte + }{ + { + name: "empty prefix", + id: id, + prefix: []uint64{}, + expectedPreimage: id[:], + }, + { + name: "1 prefix", + id: id, + prefix: []uint64{1}, + expectedPreimage: slices.Concat( + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, + id[:], + ), + }, + { + name: "multiple prefixes", + id: id, + prefix: []uint64{1, 256}, + expectedPreimage: slices.Concat( + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}, + id[:], + ), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expected := ID(hashing.ComputeHash256Array(test.expectedPreimage)) + require.Equal(t, expected, test.id.Prefix(test.prefix...)) + }) + } +} + +func TestIDAppend(t *testing.T) { + id := GenerateTestID() + tests := []struct { + name string + id ID + suffix []uint32 + expectedPreimage []byte + }{ + { + name: "empty suffix", + id: id, + suffix: []uint32{}, + expectedPreimage: id[:], + }, + { + name: "1 suffix", + id: id, + suffix: []uint32{1}, + expectedPreimage: slices.Concat( + id[:], + []byte{0x00, 0x00, 0x00, 0x01}, + ), + }, + { + name: "multiple suffixes", + id: id, + suffix: []uint32{1, 256}, + expectedPreimage: slices.Concat( + id[:], + []byte{0x00, 0x00, 0x00, 0x01}, + []byte{0x00, 0x00, 0x01, 0x00}, + ), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expected := ID(hashing.ComputeHash256Array(test.expectedPreimage)) + require.Equal(t, expected, test.id.Append(test.suffix...)) + }) + } +} + func TestIDXOR(t *testing.T) { require := require.New(t) From f832985412ee38ea5581abb9d99f116d35f48330 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 12:58:23 -0500 Subject: [PATCH 097/184] ACP-103: Document and update genesis test fee configs (#3520) --- genesis/genesis_fuji.go | 14 ++++++++----- genesis/genesis_local.go | 14 ++++++++----- genesis/genesis_mainnet.go | 14 ++++++++----- .../block/executor/verifier_test.go | 21 +++++++++++++------ 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/genesis/genesis_fuji.go b/genesis/genesis_fuji.go index 509396c5af8f..b6644dbdb52d 100644 --- a/genesis/genesis_fuji.go +++ b/genesis/genesis_fuji.go @@ -42,11 +42,15 @@ var ( gas.DBWrite: 1, gas.Compute: 1, }, - MaxCapacity: 1_000_000, - MaxPerSecond: 1_000, - TargetPerSecond: 500, - MinPrice: 1, - ExcessConversionConstant: 5_000, + MaxCapacity: 1_000_000, // Max block size ~1MB + MaxPerSecond: 250_000, + TargetPerSecond: 125_000, // Target block size ~125KB + MinPrice: 1, + // ExcessConversionConstant = (MaxPerSecond - TargetPerSecond) * NumberOfSecondsPerDoubling / ln(2) + // + // ln(2) is a float and the result is consensus critical, so we + // hardcode the result. + ExcessConversionConstant: 5_410_106, // Double every 30s }, ValidatorFeeConfig: validatorfee.Config{ Capacity: 20_000, diff --git a/genesis/genesis_local.go b/genesis/genesis_local.go index f40d866caa65..444142f138d2 100644 --- a/genesis/genesis_local.go +++ b/genesis/genesis_local.go @@ -60,11 +60,15 @@ var ( gas.DBWrite: 1, gas.Compute: 1, }, - MaxCapacity: 1_000_000, - MaxPerSecond: 1_000, - TargetPerSecond: 500, - MinPrice: 1, - ExcessConversionConstant: 5_000, + MaxCapacity: 1_000_000, // Max block size ~1MB + MaxPerSecond: 250_000, + TargetPerSecond: 125_000, // Target block size ~125KB + MinPrice: 1, + // ExcessConversionConstant = (MaxPerSecond - TargetPerSecond) * NumberOfSecondsPerDoubling / ln(2) + // + // ln(2) is a float and the result is consensus critical, so we + // hardcode the result. + ExcessConversionConstant: 5_410_106, // Double every 30s }, ValidatorFeeConfig: validatorfee.Config{ Capacity: 20_000, diff --git a/genesis/genesis_mainnet.go b/genesis/genesis_mainnet.go index 1927762cda8f..3d4a6d4c91f0 100644 --- a/genesis/genesis_mainnet.go +++ b/genesis/genesis_mainnet.go @@ -42,11 +42,15 @@ var ( gas.DBWrite: 1, gas.Compute: 1, }, - MaxCapacity: 1_000_000, - MaxPerSecond: 1_000, - TargetPerSecond: 500, - MinPrice: 1, - ExcessConversionConstant: 5_000, + MaxCapacity: 1_000_000, // Max block size ~1MB + MaxPerSecond: 250_000, + TargetPerSecond: 125_000, // Target block size ~125KB + MinPrice: 1, + // ExcessConversionConstant = (MaxPerSecond - TargetPerSecond) * NumberOfSecondsPerDoubling / ln(2) + // + // ln(2) is a float and the result is consensus critical, so we + // hardcode the result. + ExcessConversionConstant: 5_410_106, // Double every 30s }, ValidatorFeeConfig: validatorfee.Config{ Capacity: 20_000, diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index a04d93e7222d..3df54ccbf586 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -1139,6 +1139,18 @@ func TestBlockExecutionWithComplexity(t *testing.T) { blockGas, err := blockComplexity.ToGas(verifier.txExecutorBackend.Config.DynamicFeeConfig.Weights) require.NoError(t, err) + const secondsToAdvance = 10 + + initialFeeState := gas.State{} + feeStateAfterTimeAdvanced := initialFeeState.AdvanceTime( + verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxCapacity, + verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxPerSecond, + verifier.txExecutorBackend.Config.DynamicFeeConfig.TargetPerSecond, + secondsToAdvance, + ) + feeStateAfterGasConsumed, err := feeStateAfterTimeAdvanced.ConsumeGas(blockGas) + require.NoError(t, err) + tests := []struct { name string timestamp time.Time @@ -1151,12 +1163,9 @@ func TestBlockExecutionWithComplexity(t *testing.T) { expectedErr: gas.ErrInsufficientCapacity, }, { - name: "updates fee state", - timestamp: genesistest.DefaultValidatorStartTime.Add(10 * time.Second), - expectedFeeState: gas.State{ - Capacity: gas.Gas(0).AddPerSecond(verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxPerSecond, 10) - blockGas, - Excess: blockGas, - }, + name: "updates fee state", + timestamp: genesistest.DefaultValidatorStartTime.Add(secondsToAdvance * time.Second), + expectedFeeState: feeStateAfterGasConsumed, }, } for _, test := range tests { From 9185ae794386ea060c0e24bbd148c259f95b4914 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 13:32:03 -0500 Subject: [PATCH 098/184] ACP-77: Deactivate SoVs without sufficient fees (#3412) Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Co-authored-by: Ceyhun Onur Co-authored-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- vms/platformvm/block/builder/builder.go | 18 +- vms/platformvm/block/executor/manager.go | 6 +- .../block/executor/proposal_block_test.go | 7 +- .../block/executor/standard_block_test.go | 4 +- vms/platformvm/block/executor/verifier.go | 92 +++++++++-- .../block/executor/verifier_test.go | 111 ++++++++++++- vms/platformvm/state/chain_time_helpers.go | 106 +++++++++--- .../state/chain_time_helpers_test.go | 93 ++++++++++- vms/platformvm/state/subnet_only_validator.go | 6 + .../txs/executor/proposal_tx_executor.go | 1 + vms/platformvm/txs/executor/state_changes.go | 149 +++++++++++++---- .../txs/executor/state_changes_test.go | 155 +++++++++++++++++- 12 files changed, 668 insertions(+), 80 deletions(-) diff --git a/vms/platformvm/block/builder/builder.go b/vms/platformvm/block/builder/builder.go index aa4804cae6d1..3c88e8277929 100644 --- a/vms/platformvm/block/builder/builder.go +++ b/vms/platformvm/block/builder/builder.go @@ -182,7 +182,11 @@ func (b *builder) durationToSleep() (time.Duration, error) { now := b.txExecutorBackend.Clk.Time() maxTimeToAwake := now.Add(maxTimeToSleep) - nextStakerChangeTime, err := state.GetNextStakerChangeTime(preferredState, maxTimeToAwake) + nextStakerChangeTime, err := state.GetNextStakerChangeTime( + b.txExecutorBackend.Config.ValidatorFeeConfig, + preferredState, + maxTimeToAwake, + ) if err != nil { return 0, fmt.Errorf("%w of %s: %w", errCalculatingNextStakerTime, preferredID, err) } @@ -226,7 +230,11 @@ func (b *builder) BuildBlock(context.Context) (snowman.Block, error) { return nil, fmt.Errorf("%w: %s", state.ErrMissingParentState, preferredID) } - timestamp, timeWasCapped, err := state.NextBlockTime(preferredState, b.txExecutorBackend.Clk) + timestamp, timeWasCapped, err := state.NextBlockTime( + b.txExecutorBackend.Config.ValidatorFeeConfig, + preferredState, + b.txExecutorBackend.Clk, + ) if err != nil { return nil, fmt.Errorf("could not calculate next staker change time: %w", err) } @@ -253,7 +261,11 @@ func (b *builder) PackAllBlockTxs() ([]*txs.Tx, error) { return nil, fmt.Errorf("%w: %s", errMissingPreferredState, preferredID) } - timestamp, _, err := state.NextBlockTime(preferredState, b.txExecutorBackend.Clk) + timestamp, _, err := state.NextBlockTime( + b.txExecutorBackend.Config.ValidatorFeeConfig, + preferredState, + b.txExecutorBackend.Clk, + ) if err != nil { return nil, fmt.Errorf("could not calculate next staker change time: %w", err) } diff --git a/vms/platformvm/block/executor/manager.go b/vms/platformvm/block/executor/manager.go index 2cf3f1f988ff..5c419500e5f7 100644 --- a/vms/platformvm/block/executor/manager.go +++ b/vms/platformvm/block/executor/manager.go @@ -127,7 +127,11 @@ func (m *manager) VerifyTx(tx *txs.Tx) error { return err } - nextBlkTime, _, err := state.NextBlockTime(stateDiff, m.txExecutorBackend.Clk) + nextBlkTime, _, err := state.NextBlockTime( + m.txExecutorBackend.Config.ValidatorFeeConfig, + stateDiff, + m.txExecutorBackend.Clk, + ) if err != nil { return err } diff --git a/vms/platformvm/block/executor/proposal_block_test.go b/vms/platformvm/block/executor/proposal_block_test.go index 44f644f8fa54..28735e4ea187 100644 --- a/vms/platformvm/block/executor/proposal_block_test.go +++ b/vms/platformvm/block/executor/proposal_block_test.go @@ -219,6 +219,7 @@ func TestBanffProposalBlockTimeVerification(t *testing.T) { ), nil }).AnyTimes() onParentAccept.EXPECT().GetPendingStakerIterator().Return(iterator.Empty[*state.Staker]{}, nil).AnyTimes() + onParentAccept.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(iterator.Empty[state.SubnetOnlyValidator]{}, nil).AnyTimes() onParentAccept.EXPECT().GetExpiryIterator().Return(iterator.Empty[state.ExpiryEntry]{}, nil).AnyTimes() onParentAccept.EXPECT().GetDelegateeReward(constants.PrimaryNetworkID, unsignedNextStakerTx.NodeID()).Return(uint64(0), nil).AnyTimes() @@ -1378,7 +1379,11 @@ func TestAddValidatorProposalBlock(t *testing.T) { // Advance time until next staker change time is [validatorEndTime] for { - nextStakerChangeTime, err := state.GetNextStakerChangeTime(env.state, mockable.MaxTime) + nextStakerChangeTime, err := state.GetNextStakerChangeTime( + env.config.ValidatorFeeConfig, + env.state, + mockable.MaxTime, + ) require.NoError(err) if nextStakerChangeTime.Equal(validatorEndTime) { break diff --git a/vms/platformvm/block/executor/standard_block_test.go b/vms/platformvm/block/executor/standard_block_test.go index 006287c04508..055e6c0e813a 100644 --- a/vms/platformvm/block/executor/standard_block_test.go +++ b/vms/platformvm/block/executor/standard_block_test.go @@ -62,6 +62,7 @@ func TestApricotStandardBlockTimeVerification(t *testing.T) { onParentAccept.EXPECT().GetSoVExcess().Return(gas.Gas(0)).AnyTimes() onParentAccept.EXPECT().GetAccruedFees().Return(uint64(0)).AnyTimes() onParentAccept.EXPECT().NumActiveSubnetOnlyValidators().Return(0).AnyTimes() + onParentAccept.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(&iterator.Empty[state.SubnetOnlyValidator]{}, nil).AnyTimes() // wrong height apricotChildBlk, err := block.NewApricotStandardBlock( @@ -129,9 +130,8 @@ func TestBanffStandardBlockTimeVerification(t *testing.T) { ), nil }).AnyTimes() - // no pending stakers onParentAccept.EXPECT().GetPendingStakerIterator().Return(iterator.Empty[*state.Staker]{}, nil).AnyTimes() - // no expiries + onParentAccept.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(iterator.Empty[state.SubnetOnlyValidator]{}, nil).AnyTimes() onParentAccept.EXPECT().GetExpiryIterator().Return(iterator.Empty[state.ExpiryEntry]{}, nil).AnyTimes() onParentAccept.EXPECT().GetTimestamp().Return(chainTime).AnyTimes() diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index 38320050af21..1570a1b3c2ac 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/block" @@ -16,7 +17,9 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) var ( @@ -300,6 +303,7 @@ func (v *verifier) banffNonOptionBlock(b block.BanffBlock) error { newChainTime := b.Timestamp() now := v.txExecutorBackend.Clk.Time() return executor.VerifyNewChainTime( + v.txExecutorBackend.Config.ValidatorFeeConfig, newChainTime, now, parentState, @@ -386,7 +390,7 @@ func (v *verifier) proposalBlock( onDecisionState state.Diff, onCommitState state.Diff, onAbortState state.Diff, - feeCalculator fee.Calculator, + feeCalculator txfee.Calculator, inputs set.Set[ids.ID], atomicRequests map[ids.ID]*atomic.Requests, onAcceptFunc func(), @@ -437,7 +441,7 @@ func (v *verifier) proposalBlock( func (v *verifier) standardBlock( b block.Block, txs []*txs.Tx, - feeCalculator fee.Calculator, + feeCalculator txfee.Calculator, onAcceptState state.Diff, ) error { inputs, atomicRequests, onAcceptFunc, err := v.processStandardTxs( @@ -467,17 +471,17 @@ func (v *verifier) standardBlock( return nil } -func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculator, state state.Diff, parentID ids.ID) ( +func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calculator, diff state.Diff, parentID ids.ID) ( set.Set[ids.ID], map[ids.ID]*atomic.Requests, func(), error, ) { // Complexity is limited first to avoid processing too large of a block. - if timestamp := state.GetTimestamp(); v.txExecutorBackend.Config.UpgradeConfig.IsEtnaActivated(timestamp) { + if timestamp := diff.GetTimestamp(); v.txExecutorBackend.Config.UpgradeConfig.IsEtnaActivated(timestamp) { var blockComplexity gas.Dimensions for _, tx := range txs { - txComplexity, err := fee.TxComplexity(tx.Unsigned) + txComplexity, err := txfee.TxComplexity(tx.Unsigned) if err != nil { txID := tx.ID() v.MarkDropped(txID, err) @@ -497,7 +501,7 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculato // If this block exceeds the available capacity, ConsumeGas will return // an error. - feeState := state.GetFeeState() + feeState := diff.GetFeeState() feeState, err = feeState.ConsumeGas(blockGas) if err != nil { return nil, nil, nil, err @@ -505,7 +509,7 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculato // Updating the fee state prior to executing the transactions is fine // because the fee calculator was already created. - state.SetFeeState(feeState) + diff.SetFeeState(feeState) } var ( @@ -517,7 +521,7 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculato for _, tx := range txs { txExecutor := executor.StandardTxExecutor{ Backend: v.txExecutorBackend, - State: state, + State: diff, FeeCalculator: feeCalculator, Tx: tx, } @@ -533,7 +537,7 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculato // Add UTXOs to batch inputs.Union(txExecutor.Inputs) - state.AddTx(tx, status.Committed) + diff.AddTx(tx, status.Committed) if txExecutor.OnAccept != nil { funcs = append(funcs, txExecutor.OnAccept) } @@ -565,5 +569,73 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator fee.Calculato } } + // After processing all the transactions, deactivate any SoVs that might not + // have sufficient fee to pay for the next second. + // + // This ensures that SoVs are not undercharged for the next second. + err := deactivateLowBalanceSoVs( + v.txExecutorBackend.Config.ValidatorFeeConfig, + diff, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to deactivate low balance SoVs: %w", err) + } + return inputs, atomicRequests, onAcceptFunc, nil } + +// deactivateLowBalanceSoVs deactivates any SoVs that might not have sufficient +// fees to pay for the next second. +func deactivateLowBalanceSoVs( + config validatorfee.Config, + diff state.Diff, +) error { + var ( + accruedFees = diff.GetAccruedFees() + validatorFeeState = validatorfee.State{ + Current: gas.Gas(diff.NumActiveSubnetOnlyValidators()), + Excess: diff.GetSoVExcess(), + } + potentialCost = validatorFeeState.CostOf( + config, + 1, // 1 second + ) + ) + potentialAccruedFees, err := math.Add(accruedFees, potentialCost) + if err != nil { + return fmt.Errorf("could not calculate potentially accrued fees: %w", err) + } + + // Invariant: Proposal transactions do not impact SoV state. + sovIterator, err := diff.GetActiveSubnetOnlyValidatorsIterator() + if err != nil { + return fmt.Errorf("could not iterate over active SoVs: %w", err) + } + + var sovsToDeactivate []state.SubnetOnlyValidator + for sovIterator.Next() { + sov := sovIterator.Value() + // If the validator has exactly the right amount of fee for the next + // second we should not remove them here. + // + // GetActiveSubnetOnlyValidatorsIterator iterates in order of increasing + // EndAccumulatedFee, so we can break early. + if sov.EndAccumulatedFee >= potentialAccruedFees { + break + } + + sovsToDeactivate = append(sovsToDeactivate, sov) + } + + // The iterator must be released prior to attempting to write to the + // diff. + sovIterator.Release() + + for _, sov := range sovsToDeactivate { + sov.EndAccumulatedFee = 0 + if err := diff.PutSubnetOnlyValidator(sov); err != nil { + return fmt.Errorf("could not deactivate SoV %s: %w", sov.ValidationID, err) + } + } + return nil +} diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 3df54ccbf586..0a5d9e3c662f 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -21,9 +21,12 @@ import ( "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -35,13 +38,15 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool/mempoolmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txsmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) func newTestVerifier(t testing.TB, s state.State) *verifier { @@ -74,6 +79,7 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { CreateAssetTxFee: genesis.LocalParams.CreateAssetTxFee, StaticFeeConfig: genesis.LocalParams.StaticFeeConfig, DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, SybilProtectionEnabled: true, UpgradeConfig: upgrades, }, @@ -340,6 +346,7 @@ func TestVerifierVisitStandardBlock(t *testing.T) { parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) + parentState.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(&iterator.Empty[state.SubnetOnlyValidator]{}, nil).Times(1) parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) mempool.EXPECT().Remove(apricotBlk.Txs()).Times(1) @@ -1134,7 +1141,7 @@ func TestBlockExecutionWithComplexity(t *testing.T) { baseTx1, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) require.NoError(t, err) - blockComplexity, err := fee.TxComplexity(baseTx0.Unsigned, baseTx1.Unsigned) + blockComplexity, err := txfee.TxComplexity(baseTx0.Unsigned, baseTx1.Unsigned) require.NoError(t, err) blockGas, err := blockComplexity.ToGas(verifier.txExecutorBackend.Config.DynamicFeeConfig.Weights) require.NoError(t, err) @@ -1176,7 +1183,11 @@ func TestBlockExecutionWithComplexity(t *testing.T) { clear(verifier.blkIDToState) verifier.txExecutorBackend.Clk.Set(test.timestamp) - timestamp, _, err := state.NextBlockTime(s, verifier.txExecutorBackend.Clk) + timestamp, _, err := state.NextBlockTime( + verifier.txExecutorBackend.Config.ValidatorFeeConfig, + s, + verifier.txExecutorBackend.Clk, + ) require.NoError(err) lastAcceptedID := s.GetLastAccepted() @@ -1209,3 +1220,97 @@ func TestBlockExecutionWithComplexity(t *testing.T) { }) } } + +func TestDeactivateLowBalanceSoVs(t *testing.T) { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + var ( + pk = bls.PublicFromSecretKey(sk) + pkBytes = bls.PublicKeyToUncompressedBytes(pk) + + newSoV = func(endAccumulatedFee uint64) state.SubnetOnlyValidator { + return state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, + EndAccumulatedFee: endAccumulatedFee, + } + } + fractionalTimeSoV0 = newSoV(1 * units.NanoAvax) // lasts .5 seconds + fractionalTimeSoV1 = newSoV(1 * units.NanoAvax) // lasts .5 seconds + wholeTimeSoV = newSoV(2 * units.NanoAvax) // lasts 1 second + ) + + tests := []struct { + name string + initialSoVs []state.SubnetOnlyValidator + expectedSoVs []state.SubnetOnlyValidator + }{ + { + name: "no SoVs", + }, + { + name: "fractional SoV is not undercharged", + initialSoVs: []state.SubnetOnlyValidator{ + fractionalTimeSoV0, + }, + }, + { + name: "fractional SoVs are not undercharged", + initialSoVs: []state.SubnetOnlyValidator{ + fractionalTimeSoV0, + fractionalTimeSoV1, + }, + }, + { + name: "whole SoVs are not overcharged", + initialSoVs: []state.SubnetOnlyValidator{ + wholeTimeSoV, + }, + expectedSoVs: []state.SubnetOnlyValidator{ + wholeTimeSoV, + }, + }, + { + name: "partial eviction", + initialSoVs: []state.SubnetOnlyValidator{ + fractionalTimeSoV0, + wholeTimeSoV, + }, + expectedSoVs: []state.SubnetOnlyValidator{ + wholeTimeSoV, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + s := statetest.New(t, statetest.Config{}) + for _, sov := range test.initialSoVs { + require.NoError(s.PutSubnetOnlyValidator(sov)) + } + + diff, err := state.NewDiffOn(s) + require.NoError(err) + + config := validatorfee.Config{ + Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: gas.Price(2 * units.NanoAvax), // Min price is increased to allow fractional fees + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + } + require.NoError(deactivateLowBalanceSoVs(config, diff)) + + sovs, err := diff.GetActiveSubnetOnlyValidatorsIterator() + require.NoError(err) + require.Equal( + test.expectedSoVs, + iterator.ToSlice(sovs), + ) + }) + } +} diff --git a/vms/platformvm/state/chain_time_helpers.go b/vms/platformvm/state/chain_time_helpers.go index 0a2fbfa8ba17..fbf33b83e8d3 100644 --- a/vms/platformvm/state/chain_time_helpers.go +++ b/vms/platformvm/state/chain_time_helpers.go @@ -8,13 +8,20 @@ import ( "time" "github.com/ava-labs/avalanchego/utils/iterator" + "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/config" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) -func NextBlockTime(state Chain, clk *mockable.Clock) (time.Time, bool, error) { +func NextBlockTime( + config validatorfee.Config, + state Chain, + clk *mockable.Clock, +) (time.Time, bool, error) { var ( timestamp = clk.Time() parentTime = state.GetTimestamp() @@ -27,7 +34,7 @@ func NextBlockTime(state Chain, clk *mockable.Clock) (time.Time, bool, error) { // If the NextStakerChangeTime is after timestamp, then we shouldn't return // that the time was capped. nextStakerChangeTimeCap := timestamp.Add(time.Second) - nextStakerChangeTime, err := GetNextStakerChangeTime(state, nextStakerChangeTimeCap) + nextStakerChangeTime, err := GetNextStakerChangeTime(config, state, nextStakerChangeTimeCap) if err != nil { return time.Time{}, false, fmt.Errorf("failed getting next staker change time: %w", err) } @@ -42,9 +49,13 @@ func NextBlockTime(state Chain, clk *mockable.Clock) (time.Time, bool, error) { } // GetNextStakerChangeTime returns the next time a staker will be either added -// or removed to/from the current validator set. If the next staker change time -// is further in the future than [defaultTime], then [defaultTime] is returned. -func GetNextStakerChangeTime(state Chain, defaultTime time.Time) (time.Time, error) { +// to or removed from the validator set. If the next staker change time is +// further in the future than [nextTime], then [nextTime] is returned. +func GetNextStakerChangeTime( + config validatorfee.Config, + state Chain, + nextTime time.Time, +) (time.Time, error) { currentIterator, err := state.GetCurrentStakerIterator() if err != nil { return time.Time{}, err @@ -64,42 +75,93 @@ func GetNextStakerChangeTime(state Chain, defaultTime time.Time) (time.Time, err } time := it.Value().NextTime - if time.Before(defaultTime) { - defaultTime = time + if time.Before(nextTime) { + nextTime = time } } - return defaultTime, nil + + return getNextSoVEvictionTime(config, state, nextTime) +} + +func getNextSoVEvictionTime( + config validatorfee.Config, + state Chain, + nextTime time.Time, +) (time.Time, error) { + sovIterator, err := state.GetActiveSubnetOnlyValidatorsIterator() + if err != nil { + return time.Time{}, fmt.Errorf("could not iterate over active SoVs: %w", err) + } + defer sovIterator.Release() + + // If there are no SoVs, return + if !sovIterator.Next() { + return nextTime, nil + } + + // Calculate the remaining funds that the next validator to evict has. + var ( + // GetActiveSubnetOnlyValidatorsIterator iterates in order of increasing + // EndAccumulatedFee, so the first SoV is the next SoV to evict. + sov = sovIterator.Value() + accruedFees = state.GetAccruedFees() + ) + remainingFunds, err := math.Sub(sov.EndAccumulatedFee, accruedFees) + if err != nil { + return time.Time{}, fmt.Errorf("could not calculate remaining funds: %w", err) + } + + // Calculate how many seconds the remaining funds can last for. + var ( + currentTime = state.GetTimestamp() + maxSeconds = uint64(nextTime.Sub(currentTime) / time.Second) + ) + feeState := validatorfee.State{ + Current: gas.Gas(state.NumActiveSubnetOnlyValidators()), + Excess: state.GetSoVExcess(), + } + remainingSeconds := feeState.SecondsRemaining( + config, + maxSeconds, + remainingFunds, + ) + + deactivationTime := currentTime.Add(time.Duration(remainingSeconds) * time.Second) + if deactivationTime.Before(nextTime) { + nextTime = deactivationTime + } + return nextTime, nil } // PickFeeCalculator creates either a static or a dynamic fee calculator, // depending on the active upgrade. // // PickFeeCalculator does not modify [state]. -func PickFeeCalculator(cfg *config.Config, state Chain) fee.Calculator { +func PickFeeCalculator(config *config.Config, state Chain) txfee.Calculator { timestamp := state.GetTimestamp() - if !cfg.UpgradeConfig.IsEtnaActivated(timestamp) { - return NewStaticFeeCalculator(cfg, timestamp) + if !config.UpgradeConfig.IsEtnaActivated(timestamp) { + return NewStaticFeeCalculator(config, timestamp) } feeState := state.GetFeeState() gasPrice := gas.CalculatePrice( - cfg.DynamicFeeConfig.MinPrice, + config.DynamicFeeConfig.MinPrice, feeState.Excess, - cfg.DynamicFeeConfig.ExcessConversionConstant, + config.DynamicFeeConfig.ExcessConversionConstant, ) - return fee.NewDynamicCalculator( - cfg.DynamicFeeConfig.Weights, + return txfee.NewDynamicCalculator( + config.DynamicFeeConfig.Weights, gasPrice, ) } // NewStaticFeeCalculator creates a static fee calculator, with the config set // to either the pre-AP3 or post-AP3 config. -func NewStaticFeeCalculator(cfg *config.Config, timestamp time.Time) fee.Calculator { - config := cfg.StaticFeeConfig - if !cfg.UpgradeConfig.IsApricotPhase3Activated(timestamp) { - config.CreateSubnetTxFee = cfg.CreateAssetTxFee - config.CreateBlockchainTxFee = cfg.CreateAssetTxFee +func NewStaticFeeCalculator(config *config.Config, timestamp time.Time) txfee.Calculator { + feeConfig := config.StaticFeeConfig + if !config.UpgradeConfig.IsApricotPhase3Activated(timestamp) { + feeConfig.CreateSubnetTxFee = config.CreateAssetTxFee + feeConfig.CreateBlockchainTxFee = config.CreateAssetTxFee } - return fee.NewStaticCalculator(config) + return txfee.NewStaticCalculator(feeConfig) } diff --git a/vms/platformvm/state/chain_time_helpers_test.go b/vms/platformvm/state/chain_time_helpers_test.go index 7e1a1786eb61..66ba236e498c 100644 --- a/vms/platformvm/state/chain_time_helpers_test.go +++ b/vms/platformvm/state/chain_time_helpers_test.go @@ -15,10 +15,14 @@ import ( "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) func TestNextBlockTime(t *testing.T) { @@ -69,7 +73,11 @@ func TestNextBlockTime(t *testing.T) { s.SetTimestamp(test.chainTime) clk.Set(test.now) - actualTime, actualCapped, err := NextBlockTime(s, &clk) + actualTime, actualCapped, err := NextBlockTime( + genesis.LocalParams.ValidatorFeeConfig, + s, + &clk, + ) require.NoError(err) require.Equal(test.expectedTime.Local(), actualTime.Local()) require.Equal(test.expectedCapped, actualCapped) @@ -78,9 +86,17 @@ func TestNextBlockTime(t *testing.T) { } func TestGetNextStakerChangeTime(t *testing.T) { + config := validatorfee.Config{ + Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: gas.Price(2 * units.NanoAvax), // Increase minimum price to test fractional seconds + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + } + tests := []struct { name string pending []*Staker + sovs []SubnetOnlyValidator maxTime time.Time expected time.Time }{ @@ -107,6 +123,62 @@ func TestGetNextStakerChangeTime(t *testing.T) { maxTime: mockable.MaxTime, expected: genesistest.DefaultValidatorStartTime.Add(time.Second), }, + { + name: "subnet only validator with less than 1 second of fees", + sovs: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + Weight: 1, + EndAccumulatedFee: 1, // This validator should be evicted in .5 seconds, which is rounded to 0. + }, + }, + maxTime: mockable.MaxTime, + expected: genesistest.DefaultValidatorStartTime, + }, + { + name: "subnet only validator with 1 second of fees", + sovs: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + Weight: 1, + EndAccumulatedFee: 2, // This validator should be evicted in 1 second. + }, + }, + maxTime: mockable.MaxTime, + expected: genesistest.DefaultValidatorStartTime.Add(time.Second), + }, + { + name: "subnet only validator with less than 2 seconds of fees", + sovs: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + Weight: 1, + EndAccumulatedFee: 3, // This validator should be evicted in 1.5 seconds, which is rounded to 1. + }, + }, + maxTime: mockable.MaxTime, + expected: genesistest.DefaultValidatorStartTime.Add(time.Second), + }, + { + name: "current and subnet only validator with high balance", + sovs: []SubnetOnlyValidator{ + { + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + Weight: 1, + EndAccumulatedFee: units.Avax, // This validator won't be evicted soon. + }, + }, + maxTime: mockable.MaxTime, + expected: genesistest.DefaultValidatorEndTime, + }, { name: "restricted timestamp", maxTime: genesistest.DefaultValidatorStartTime, @@ -122,8 +194,15 @@ func TestGetNextStakerChangeTime(t *testing.T) { for _, staker := range test.pending { require.NoError(s.PutPendingValidator(staker)) } + for _, sov := range test.sovs { + require.NoError(s.PutSubnetOnlyValidator(sov)) + } - actual, err := GetNextStakerChangeTime(s, test.maxTime) + actual, err := GetNextStakerChangeTime( + config, + s, + test.maxTime, + ) require.NoError(err) require.Equal(test.expected.Local(), actual.Local()) }) @@ -143,19 +222,19 @@ func TestPickFeeCalculator(t *testing.T) { tests := []struct { fork upgradetest.Fork - expected fee.Calculator + expected txfee.Calculator }{ { fork: upgradetest.ApricotPhase2, - expected: fee.NewStaticCalculator(apricotPhase2StaticFeeConfig), + expected: txfee.NewStaticCalculator(apricotPhase2StaticFeeConfig), }, { fork: upgradetest.ApricotPhase3, - expected: fee.NewStaticCalculator(staticFeeConfig), + expected: txfee.NewStaticCalculator(staticFeeConfig), }, { fork: upgradetest.Etna, - expected: fee.NewDynamicCalculator( + expected: txfee.NewDynamicCalculator( dynamicFeeConfig.Weights, dynamicFeeConfig.MinPrice, ), diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 109fd23ea9f2..352c65e3a27d 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -34,6 +34,12 @@ var ( type SubnetOnlyValidators interface { // GetActiveSubnetOnlyValidatorsIterator returns an iterator of all the // active subnet-only validators in increasing order of EndAccumulatedFee. + // + // It is the caller's responsibility to call [Release] on the iterator after + // use. + // + // It is not guaranteed to be safe to modify the state while using the + // iterator. After releasing the iterator, the state may be safely modified. GetActiveSubnetOnlyValidatorsIterator() (iterator.Iterator[SubnetOnlyValidator], error) // NumActiveSubnetOnlyValidators returns the number of currently active diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index aa8064f2fcd8..4c4c1f5c2b91 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -271,6 +271,7 @@ func (e *ProposalTxExecutor) AdvanceTimeTx(tx *txs.AdvanceTimeTx) error { now := e.Clk.Time() if err := VerifyNewChainTime( + e.Config.ValidatorFeeConfig, newChainTime, now, e.OnCommitState, diff --git a/vms/platformvm/txs/executor/state_changes.go b/vms/platformvm/txs/executor/state_changes.go index 0dda483228de..a9ce2b44876b 100644 --- a/vms/platformvm/txs/executor/state_changes.go +++ b/vms/platformvm/txs/executor/state_changes.go @@ -10,9 +10,12 @@ 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/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/reward" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) var ( @@ -30,6 +33,7 @@ var ( // - [newChainTime] <= [nextStakerChangeTime]: so that no staking set changes // are skipped. func VerifyNewChainTime( + config fee.Config, newChainTime time.Time, now time.Time, currentState state.Chain, @@ -57,7 +61,11 @@ func VerifyNewChainTime( // nextStakerChangeTime is calculated last to ensure that the function is // able to be calculated efficiently. - nextStakerChangeTime, err := state.GetNextStakerChangeTime(currentState, newChainTime) + nextStakerChangeTime, err := state.GetNextStakerChangeTime( + config, + currentState, + newChainTime, + ) if err != nil { return fmt.Errorf("could not verify block timestamp: %w", err) } @@ -213,52 +221,135 @@ func advanceTimeTo( changed = true } - if backend.Config.UpgradeConfig.IsEtnaActivated(newChainTime) { - previousChainTime := changes.GetTimestamp() - duration := uint64(newChainTime.Sub(previousChainTime) / time.Second) + if !backend.Config.UpgradeConfig.IsEtnaActivated(newChainTime) { + changes.SetTimestamp(newChainTime) + return changes, changed, nil + } - feeState := changes.GetFeeState() - feeState = feeState.AdvanceTime( - backend.Config.DynamicFeeConfig.MaxCapacity, - backend.Config.DynamicFeeConfig.MaxPerSecond, - backend.Config.DynamicFeeConfig.TargetPerSecond, - duration, - ) - changes.SetFeeState(feeState) + newChainTimeUnix := uint64(newChainTime.Unix()) + if err := removeStaleExpiries(parentState, changes, newChainTimeUnix); err != nil { + return nil, false, fmt.Errorf("failed to remove stale expiries: %w", err) } - // Remove all expiries whose timestamp now implies they can never be - // re-issued. - // - // The expiry timestamp is the time at which it is no longer valid, so any - // expiry with a timestamp less than or equal to the new chain time can be - // removed. - // - // Ref: https://github.com/avalanche-foundation/ACPs/tree/main/ACPs/77-reinventing-subnets#registersubnetvalidatortx - // - // The expiry iterator is sorted in order of increasing timestamp. - // - // Invariant: It is not safe to modify the state while iterating over it, - // so we use the parentState's iterator rather than the changes iterator. + // Calculate number of seconds the time is advancing + previousChainTime := changes.GetTimestamp() + seconds := uint64(newChainTime.Sub(previousChainTime) / time.Second) + + advanceDynamicFeeState(backend.Config.DynamicFeeConfig, changes, seconds) + sovsChanged, err := advanceValidatorFeeState( + backend.Config.ValidatorFeeConfig, + parentState, + changes, + seconds, + ) + if err != nil { + return nil, false, fmt.Errorf("failed to advance validator fee state: %w", err) + } + changed = changed || sovsChanged + + changes.SetTimestamp(newChainTime) + return changes, changed, nil +} + +// Remove all expiries whose timestamp now implies they can never be re-issued. +// +// The expiry timestamp is the time at which it is no longer valid, so any +// expiry with a timestamp less than or equal to the new chain time can be +// removed. +// +// Ref: https://github.com/avalanche-foundation/ACPs/tree/e333b335c34c8692d84259d21bd07b2bb849dc2c/ACPs/77-reinventing-subnets#registerl1validatortx +func removeStaleExpiries( + parentState state.Chain, + changes state.Diff, + newChainTimeUnix uint64, +) error { + // Invariant: It is not safe to modify the state while iterating over it, so + // we use the parentState's iterator rather than the changes iterator. // ParentState must not be modified before this iterator is released. expiryIterator, err := parentState.GetExpiryIterator() if err != nil { - return nil, false, err + return fmt.Errorf("could not iterate over expiries: %w", err) } defer expiryIterator.Release() - newChainTimeUnix := uint64(newChainTime.Unix()) for expiryIterator.Next() { expiry := expiryIterator.Value() + // The expiry iterator is sorted in order of increasing timestamp. Once + // we find a non-expired expiry, we can break. if expiry.Timestamp > newChainTimeUnix { break } changes.DeleteExpiry(expiry) } + return nil +} - changes.SetTimestamp(newChainTime) - return changes, changed, nil +func advanceDynamicFeeState( + config gas.Config, + changes state.Diff, + seconds uint64, +) { + dynamicFeeState := changes.GetFeeState() + dynamicFeeState = dynamicFeeState.AdvanceTime( + config.MaxCapacity, + config.MaxPerSecond, + config.TargetPerSecond, + seconds, + ) + changes.SetFeeState(dynamicFeeState) +} + +// advanceValidatorFeeState advances the validator fee state by [seconds]. SoVs +// are read from [parentState] and written to [changes] to avoid modifying state +// while an iterator is held. +func advanceValidatorFeeState( + config fee.Config, + parentState state.Chain, + changes state.Diff, + seconds uint64, +) (bool, error) { + validatorFeeState := fee.State{ + Current: gas.Gas(changes.NumActiveSubnetOnlyValidators()), + Excess: changes.GetSoVExcess(), + } + validatorCost := validatorFeeState.CostOf(config, seconds) + + accruedFees := changes.GetAccruedFees() + accruedFees, err := math.Add(accruedFees, validatorCost) + if err != nil { + return false, fmt.Errorf("could not calculate accrued fees: %w", err) + } + + // Invariant: It is not safe to modify the state while iterating over it, + // so we use the parentState's iterator rather than the changes iterator. + // ParentState must not be modified before this iterator is released. + sovIterator, err := parentState.GetActiveSubnetOnlyValidatorsIterator() + if err != nil { + return false, fmt.Errorf("could not iterate over active SoVs: %w", err) + } + defer sovIterator.Release() + + var changed bool + for sovIterator.Next() { + sov := sovIterator.Value() + // GetActiveSubnetOnlyValidatorsIterator iterates in order of increasing + // EndAccumulatedFee, so we can break early. + if sov.EndAccumulatedFee > accruedFees { + break + } + + sov.EndAccumulatedFee = 0 // Deactivate the validator + if err := changes.PutSubnetOnlyValidator(sov); err != nil { + return false, fmt.Errorf("could not deactivate SoV %s: %w", sov.ValidationID, err) + } + changed = true + } + + validatorFeeState = validatorFeeState.AdvanceTime(config.Target, seconds) + changes.SetSoVExcess(validatorFeeState.Excess) + changes.SetAccruedFees(accruedFees) + return changed, nil } func GetRewardsCalculator( diff --git a/vms/platformvm/txs/executor/state_changes_test.go b/vms/platformvm/txs/executor/state_changes_test.go index 15576b306ec9..acd997e1bdf0 100644 --- a/vms/platformvm/txs/executor/state_changes_test.go +++ b/vms/platformvm/txs/executor/state_changes_test.go @@ -9,15 +9,19 @@ import ( "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/upgrade/upgradetest" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/iterator" "github.com/ava-labs/avalanchego/utils/timer/mockable" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/genesis/genesistest" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" + "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) func TestAdvanceTimeTo_UpdatesFeeState(t *testing.T) { @@ -79,7 +83,11 @@ func TestAdvanceTimeTo_UpdatesFeeState(t *testing.T) { // Ensure the invariant that [nextTime <= nextStakerChangeTime] on // AdvanceTimeTo is maintained. - nextStakerChangeTime, err := state.GetNextStakerChangeTime(s, mockable.MaxTime) + nextStakerChangeTime, err := state.GetNextStakerChangeTime( + genesis.LocalParams.ValidatorFeeConfig, + s, + mockable.MaxTime, + ) require.NoError(err) require.False(nextTime.After(nextStakerChangeTime)) @@ -185,7 +193,11 @@ func TestAdvanceTimeTo_RemovesStaleExpiries(t *testing.T) { // Ensure the invariant that [newTime <= nextStakerChangeTime] on // AdvanceTimeTo is maintained. - nextStakerChangeTime, err := state.GetNextStakerChangeTime(s, mockable.MaxTime) + nextStakerChangeTime, err := state.GetNextStakerChangeTime( + genesis.LocalParams.ValidatorFeeConfig, + s, + mockable.MaxTime, + ) require.NoError(err) require.False(newTime.After(nextStakerChangeTime)) @@ -214,3 +226,142 @@ func TestAdvanceTimeTo_RemovesStaleExpiries(t *testing.T) { }) } } + +func TestAdvanceTimeTo_UpdateSoVs(t *testing.T) { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + const ( + secondsToAdvance = 3 + timeToAdvance = secondsToAdvance * time.Second + ) + + var ( + pk = bls.PublicFromSecretKey(sk) + pkBytes = bls.PublicKeyToUncompressedBytes(pk) + + newSoV = func(endAccumulatedFee uint64) state.SubnetOnlyValidator { + return state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: ids.GenerateTestID(), + NodeID: ids.GenerateTestNodeID(), + PublicKey: pkBytes, + Weight: 1, + EndAccumulatedFee: endAccumulatedFee, + } + } + sovToEvict0 = newSoV(3 * units.NanoAvax) // lasts 3 seconds + sovToEvict1 = newSoV(3 * units.NanoAvax) // lasts 3 seconds + sovToKeep = newSoV(units.Avax) + + currentTime = genesistest.DefaultValidatorStartTime + newTime = currentTime.Add(timeToAdvance) + + config = config.Config{ + ValidatorFeeConfig: fee.Config{ + Capacity: genesis.LocalParams.ValidatorFeeConfig.Capacity, + Target: 1, + MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + ) + + tests := []struct { + name string + initialSoVs []state.SubnetOnlyValidator + expectedModified bool + expectedSoVs []state.SubnetOnlyValidator + expectedExcess gas.Gas + }{ + { + name: "no SoVs", + expectedModified: false, + expectedExcess: 0, + }, + { + name: "evicted one", + initialSoVs: []state.SubnetOnlyValidator{ + sovToEvict0, + }, + expectedModified: true, + expectedExcess: 0, + }, + { + name: "evicted all", + initialSoVs: []state.SubnetOnlyValidator{ + sovToEvict0, + sovToEvict1, + }, + expectedModified: true, + expectedExcess: 3, + }, + { + name: "evicted 2 of 3", + initialSoVs: []state.SubnetOnlyValidator{ + sovToEvict0, + sovToEvict1, + sovToKeep, + }, + expectedModified: true, + expectedSoVs: []state.SubnetOnlyValidator{ + sovToKeep, + }, + expectedExcess: 6, + }, + { + name: "no evictions", + initialSoVs: []state.SubnetOnlyValidator{ + sovToKeep, + }, + expectedModified: false, + expectedSoVs: []state.SubnetOnlyValidator{ + sovToKeep, + }, + expectedExcess: 0, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var ( + require = require.New(t) + s = statetest.New(t, statetest.Config{}) + ) + + for _, sov := range test.initialSoVs { + require.NoError(s.PutSubnetOnlyValidator(sov)) + } + + // Ensure the invariant that [newTime <= nextStakerChangeTime] on + // AdvanceTimeTo is maintained. + nextStakerChangeTime, err := state.GetNextStakerChangeTime( + genesis.LocalParams.ValidatorFeeConfig, + s, + mockable.MaxTime, + ) + require.NoError(err) + require.False(newTime.After(nextStakerChangeTime)) + + validatorsModified, err := AdvanceTimeTo( + &Backend{ + Config: &config, + }, + s, + newTime, + ) + require.NoError(err) + require.Equal(test.expectedModified, validatorsModified) + + activeSoVs, err := s.GetActiveSubnetOnlyValidatorsIterator() + require.NoError(err) + require.Equal( + test.expectedSoVs, + iterator.ToSlice(activeSoVs), + ) + + require.Equal(test.expectedExcess, s.GetSoVExcess()) + require.Equal(uint64(secondsToAdvance), s.GetAccruedFees()) + }) + } +} From 74edd4f3614725d18208a345fb8f6d4db8246650 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 14:55:27 -0500 Subject: [PATCH 099/184] reduce diff --- wallet/subnet/primary/examples/create-chain/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/subnet/primary/examples/create-chain/main.go b/wallet/subnet/primary/examples/create-chain/main.go index 61526d9cf38a..9e32490d9aef 100644 --- a/wallet/subnet/primary/examples/create-chain/main.go +++ b/wallet/subnet/primary/examples/create-chain/main.go @@ -22,7 +22,7 @@ func main() { key := genesis.EWOQKey uri := primary.LocalAPIURI kc := secp256k1fx.NewKeychain(key) - subnetIDStr := "2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof" + subnetIDStr := "29uVeLPJB1eQJkzRemU8g8wZDw5uJRqpab5U2mX9euieVwiEbL" genesis := &xsgenesis.Genesis{ Timestamp: time.Now().Unix(), Allocations: []xsgenesis.Allocation{ From 7d31e7a155055cdd2263cb7c699dbce8cbec3476 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 14:59:22 -0500 Subject: [PATCH 100/184] nit --- wallet/chain/p/builder/builder.go | 2 +- wallet/chain/p/wallet/wallet.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index d4c2b75e827d..ef23585ac92e 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -155,7 +155,7 @@ type Builder interface { // - [subnetID] specifies the subnet to be converted // - [chainID] specifies which chain the manager is deployed on // - [address] specifies the address of the manager - // - [validators] specifies the initial SoVs of the converted subnet + // - [validators] specifies the initial SoVs of the L1 NewConvertSubnetTx( subnetID ids.ID, chainID ids.ID, diff --git a/wallet/chain/p/wallet/wallet.go b/wallet/chain/p/wallet/wallet.go index 1e0c520b5eca..a36522cc6b3a 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -141,7 +141,7 @@ type Wallet interface { // - [subnetID] specifies the subnet to be converted // - [chainID] specifies which chain the manager is deployed on // - [address] specifies the address of the manager - // - [validators] specifies the initial SoVs of the converted subnet + // - [validators] specifies the initial SoVs of the L1 IssueConvertSubnetTx( subnetID ids.ID, chainID ids.ID, From fca9460ac18705900bb8dbcc7593c0909692a86d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 15:00:36 -0500 Subject: [PATCH 101/184] nit --- wallet/chain/p/builder_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/chain/p/builder_test.go b/wallet/chain/p/builder_test.go index ed71768b0969..f56a0eb0896b 100644 --- a/wallet/chain/p/builder_test.go +++ b/wallet/chain/p/builder_test.go @@ -740,7 +740,7 @@ func TestConvertSubnetTx(t *testing.T) { nil, nil, map[ids.ID]uint64{ - e.context.AVAXAssetID: 3 * units.Avax, + e.context.AVAXAssetID: 3 * units.Avax, // Balance of the validators }, ) }) From 158621cb65c750243abeb07ac90ef271e233bce9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 15:03:26 -0500 Subject: [PATCH 102/184] nit --- wallet/chain/p/builder/builder.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index ef23585ac92e..e58e0931b290 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -788,19 +788,23 @@ func (b *builder) NewConvertSubnetTx( options ...common.Option, ) (*txs.ConvertSubnetTx, error) { var ( - toBurn = map[ids.ID]uint64{} - err error - avaxAssetID = b.context.AVAXAssetID + avaxToBurn uint64 + err error ) for _, vdr := range validators { - toBurn[avaxAssetID], err = math.Add(toBurn[avaxAssetID], vdr.Balance) + avaxToBurn, err = math.Add(avaxToBurn, vdr.Balance) if err != nil { return nil, err } } - toStake := map[ids.ID]uint64{} - ops := common.NewOptions(options) + var ( + toBurn = map[ids.ID]uint64{ + b.context.AVAXAssetID: avaxToBurn, + } + toStake = map[ids.ID]uint64{} + ops = common.NewOptions(options) + ) subnetAuth, err := b.authorizeSubnet(subnetID, ops) if err != nil { return nil, err From 4c6462cd7d42692988c44df3f3c590cd98ba29be Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 15:04:30 -0500 Subject: [PATCH 103/184] nit --- wallet/subnet/primary/examples/convert-subnet/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/subnet/primary/examples/convert-subnet/main.go b/wallet/subnet/primary/examples/convert-subnet/main.go index b7133488f6ef..8602c26ffb64 100644 --- a/wallet/subnet/primary/examples/convert-subnet/main.go +++ b/wallet/subnet/primary/examples/convert-subnet/main.go @@ -21,7 +21,7 @@ import ( func main() { key := genesis.EWOQKey - uri := "http://localhost:9700" + uri := primary.LocalAPIURI kc := secp256k1fx.NewKeychain(key) subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") chainID := ids.FromStringOrPanic("E8nTR9TtRwfkS7XFjTYUYHENQ91mkPMtDUwwCeu7rNgBBtkqu") From a217d1b160d20fdfeb6751288696d0e61474cee8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 15:05:55 -0500 Subject: [PATCH 104/184] nit --- wallet/chain/p/builder/builder.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index e58e0931b290..e8762d7b2a68 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -787,11 +787,9 @@ func (b *builder) NewConvertSubnetTx( validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.ConvertSubnetTx, error) { - var ( - avaxToBurn uint64 - err error - ) + var avaxToBurn uint64 for _, vdr := range validators { + var err error avaxToBurn, err = math.Add(avaxToBurn, vdr.Balance) if err != nil { return nil, err From 3987922fbcdd98185564a63c49e6bb7c3de08fd6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 15:28:10 -0500 Subject: [PATCH 105/184] nit --- .../txs/executor/standard_tx_executor.go | 2 - .../txs/executor/standard_tx_executor_test.go | 51 +++++++++++++++---- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 8c549b677923..d30049cc92c3 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -539,8 +539,6 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { } ) for i, vdr := range tx.Validators { - vdr := vdr - nodeID, err := ids.ToNodeID(vdr.NodeID) if err != nil { return err diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index d32c4e84ea95..62b3120d046e 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -2602,23 +2602,24 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { []ids.ID{subnetID}, nil, // chainIDs ) - chainID = ids.GenerateTestID() - address = utils.RandomBytes(32) - pop = signer.NewProofOfPossession(sk) + chainID = ids.GenerateTestID() + address = utils.RandomBytes(32) + pop = signer.NewProofOfPossession(sk) + validator = &txs.ConvertSubnetValidator{ + NodeID: nodeID.Bytes(), + Weight: weight, + Balance: balance, + Signer: *pop, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + } ) convertSubnetTx, err := wallet.IssueConvertSubnetTx( subnetID, chainID, address, []*txs.ConvertSubnetValidator{ - { - NodeID: nodeID.Bytes(), - Weight: weight, - Balance: balance, - Signer: *pop, - RemainingBalanceOwner: message.PChainOwner{}, - DeactivationOwner: message.PChainOwner{}, - }, + validator, }, test.builderOptions..., ) @@ -2685,6 +2686,34 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, stateConversion, ) + + var ( + validationID = subnetID.Append(0) + pkBytes = bls.PublicKeyToUncompressedBytes(bls.PublicFromSecretKey(sk)) + ) + remainingBalanceOwner, err := txs.Codec.Marshal(txs.CodecVersion, &validator.RemainingBalanceOwner) + require.NoError(err) + + deactivationOwner, err := txs.Codec.Marshal(txs.CodecVersion, &validator.DeactivationOwner) + require.NoError(err) + + sov, err := diff.GetSubnetOnlyValidator(validationID) + require.NoError(err) + require.Equal( + state.SubnetOnlyValidator{ + ValidationID: validationID, + SubnetID: subnetID, + NodeID: nodeID, + PublicKey: pkBytes, + RemainingBalanceOwner: remainingBalanceOwner, + DeactivationOwner: deactivationOwner, + StartTime: uint64(diff.GetTimestamp().Unix()), + Weight: validator.Weight, + MinNonce: 0, + EndAccumulatedFee: validator.Balance + diff.GetAccruedFees(), + }, + sov, + ) }) } } From 56475cf2078bc9e82492e760780fd2769e189995 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 15:51:25 -0500 Subject: [PATCH 106/184] Add nodeID tests --- vms/platformvm/txs/convert_subnet_tx_test.go | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/vms/platformvm/txs/convert_subnet_tx_test.go b/vms/platformvm/txs/convert_subnet_tx_test.go index c627c9711632..d2e91acae0af 100644 --- a/vms/platformvm/txs/convert_subnet_tx_test.go +++ b/vms/platformvm/txs/convert_subnet_tx_test.go @@ -18,6 +18,7 @@ import ( "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/signer" @@ -677,6 +678,42 @@ func TestConvertSubnetTxSyntacticVerify(t *testing.T) { }, expectedErr: ErrZeroWeight, }, + { + name: "invalid validator nodeID length", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen + 1), + Weight: 1, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: hashing.ErrInvalidHashLen, + }, + { + name: "invalid validator nodeID", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: ids.EmptyNodeID[:], + Weight: 1, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: errEmptyNodeID, + }, { name: "invalid validator pop", tx: &ConvertSubnetTx{ From db85cf1fc5a77baaadc1712ea7aaa0d96a3a25fc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 16:01:04 -0500 Subject: [PATCH 107/184] ACP-77: Refactor e2e test --- tests/e2e/p/l1.go | 147 ++++++++++++++++++++++++ tests/e2e/p/permissionless_layer_one.go | 102 ---------------- 2 files changed, 147 insertions(+), 102 deletions(-) create mode 100644 tests/e2e/p/l1.go delete mode 100644 tests/e2e/p/permissionless_layer_one.go diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go new file mode 100644 index 000000000000..7466f82323b8 --- /dev/null +++ b/tests/e2e/p/l1.go @@ -0,0 +1,147 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "math" + "time" + + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + platformvmsdk "github.com/ava-labs/avalanchego/vms/platformvm" +) + +var _ = e2e.DescribePChain("[L1]", func() { + tc := e2e.NewTestContext() + require := require.New(tc) + + ginkgo.It("creates and updates L1 validators", func() { + env := e2e.GetEnv(tc) + nodeURI := env.GetRandomNodeURI() + + tc.By("verifying Etna is activated", func() { + infoClient := info.NewClient(nodeURI.URI) + upgrades, err := infoClient.Upgrades(tc.DefaultContext()) + require.NoError(err) + + now := time.Now() + if !upgrades.IsEtnaActivated(now) { + ginkgo.Skip("Etna is not activated. L1s are enabled post-Etna, skipping test.") + } + }) + + tc.By("loading the wallet") + var ( + keychain = env.NewKeychain() + baseWallet = e2e.NewWallet(tc, keychain, nodeURI) + pWallet = baseWallet.P() + pClient = platformvmsdk.NewClient(nodeURI.URI) + owner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + } + ) + + tc.By("creating the chain genesis") + genesisKey, err := secp256k1.NewPrivateKey() + require.NoError(err) + + genesisBytes, err := genesis.Codec.Marshal(genesis.CodecVersion, &genesis.Genesis{ + Timestamp: time.Now().Unix(), + Allocations: []genesis.Allocation{ + { + Address: genesisKey.Address(), + Balance: math.MaxUint64, + }, + }, + }) + require.NoError(err) + + var subnetID ids.ID + tc.By("issuing a CreateSubnetTx", func() { + subnetTx, err := pWallet.IssueCreateSubnetTx( + owner, + tc.WithDefaultContext(), + ) + require.NoError(err) + + subnetID = subnetTx.ID() + }) + + tc.By("verifying a Permissioned Subnet was successfully created", func() { + require.NotEqual(constants.PrimaryNetworkID, subnetID) + + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvmsdk.GetSubnetClientResponse{ + IsPermissioned: true, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + }, + subnet, + ) + }) + + var chainID ids.ID + tc.By("issuing a CreateChainTx", func() { + chainTx, err := pWallet.IssueCreateChainTx( + subnetID, + genesisBytes, + constants.XSVMID, + nil, + "No Permissions", + tc.WithDefaultContext(), + ) + require.NoError(err) + + chainID = chainTx.ID() + }) + + address := []byte{} + tc.By("issuing a ConvertSubnetTx", func() { + _, err := pWallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + tc.WithDefaultContext(), + ) + require.NoError(err) + }) + + tc.By("verifying the Permissioned Subnet was converted to an L1", func() { + tc.By("verifying the subnet reports as being converted", func() { + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvmsdk.GetSubnetClientResponse{ + IsPermissioned: false, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + ManagerChainID: chainID, + ManagerAddress: address, + }, + subnet, + ) + }) + }) + + _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) + }) +}) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go deleted file mode 100644 index edd1dbf7f9e4..000000000000 --- a/tests/e2e/p/permissionless_layer_one.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package p - -import ( - "time" - - "github.com/onsi/ginkgo/v2" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/tests/fixture/e2e" - "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/vms/platformvm" - "github.com/ava-labs/avalanchego/vms/secp256k1fx" -) - -var _ = e2e.DescribePChain("[Permissionless L1]", func() { - tc := e2e.NewTestContext() - require := require.New(tc) - - ginkgo.It("creates a Permissionless L1", func() { - env := e2e.GetEnv(tc) - nodeURI := env.GetRandomNodeURI() - infoClient := info.NewClient(nodeURI.URI) - - tc.By("fetching upgrade config") - upgrades, err := infoClient.Upgrades(tc.DefaultContext()) - require.NoError(err) - - tc.By("verifying Etna is activated") - now := time.Now() - if !upgrades.IsEtnaActivated(now) { - ginkgo.Skip("Etna is not activated. Permissionless L1s are enabled post-Etna, skipping test.") - } - - keychain := env.NewKeychain() - baseWallet := e2e.NewWallet(tc, keychain, nodeURI) - - pWallet := baseWallet.P() - pClient := platformvm.NewClient(nodeURI.URI) - - owner := &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - } - - tc.By("issuing a CreateSubnetTx") - subnetTx, err := pWallet.IssueCreateSubnetTx( - owner, - tc.WithDefaultContext(), - ) - require.NoError(err) - - tc.By("verifying a Permissioned Subnet was successfully created") - subnetID := subnetTx.ID() - require.NotEqual(subnetID, constants.PrimaryNetworkID) - - res, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) - require.NoError(err) - - require.Equal(platformvm.GetSubnetClientResponse{ - IsPermissioned: true, - ControlKeys: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - Threshold: 1, - }, res) - - chainID := ids.GenerateTestID() - address := []byte{'a', 'd', 'd', 'r', 'e', 's', 's'} - - tc.By("issuing a ConvertSubnetTx") - _, err = pWallet.IssueConvertSubnetTx( - subnetID, - chainID, - address, - tc.WithDefaultContext(), - ) - require.NoError(err) - - tc.By("verifying the Permissioned Subnet was converted to a Permissionless L1") - res, err = pClient.GetSubnet(tc.DefaultContext(), subnetID) - require.NoError(err) - - require.Equal(platformvm.GetSubnetClientResponse{ - IsPermissioned: false, - ControlKeys: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - Threshold: 1, - ManagerChainID: chainID, - ManagerAddress: address, - }, res) - - _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) - }) -}) From 8a18b148f717f2078e32c659e5085fd81bcb6e1c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 16:02:38 -0500 Subject: [PATCH 108/184] nit --- tests/e2e/p/l1.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 7466f82323b8..544f970f4f8f 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -16,9 +16,8 @@ import ( "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - - platformvmsdk "github.com/ava-labs/avalanchego/vms/platformvm" ) var _ = e2e.DescribePChain("[L1]", func() { @@ -45,7 +44,7 @@ var _ = e2e.DescribePChain("[L1]", func() { keychain = env.NewKeychain() baseWallet = e2e.NewWallet(tc, keychain, nodeURI) pWallet = baseWallet.P() - pClient = platformvmsdk.NewClient(nodeURI.URI) + pClient = platformvm.NewClient(nodeURI.URI) owner = &secp256k1fx.OutputOwners{ Threshold: 1, Addrs: []ids.ShortID{ @@ -86,7 +85,7 @@ var _ = e2e.DescribePChain("[L1]", func() { subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) require.NoError(err) require.Equal( - platformvmsdk.GetSubnetClientResponse{ + platformvm.GetSubnetClientResponse{ IsPermissioned: true, ControlKeys: []ids.ShortID{ keychain.Keys[0].Address(), @@ -128,7 +127,7 @@ var _ = e2e.DescribePChain("[L1]", func() { subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) require.NoError(err) require.Equal( - platformvmsdk.GetSubnetClientResponse{ + platformvm.GetSubnetClientResponse{ IsPermissioned: false, ControlKeys: []ids.ShortID{ keychain.Keys[0].Address(), From f8552baee894c602458654df5446fd88439e97b9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 16:18:19 -0500 Subject: [PATCH 109/184] reduce diff --- tests/e2e/p/l1.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 52c3c9b60d39..82e938e9cc2a 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -48,7 +48,7 @@ var _ = e2e.DescribePChain("[L1]", func() { now := time.Now() if !upgrades.IsEtnaActivated(now) { - ginkgo.Skip("Etna is not activated. Permissionless L1s are enabled post-Etna, skipping test.") + ginkgo.Skip("Etna is not activated. L1s are enabled post-Etna, skipping test.") } }) From cb59182a9a3e9bc6fe864d57ccbcb12e46f4efe4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 17:02:17 -0500 Subject: [PATCH 110/184] ACP-77: Allow legacy validator removal after conversion (#3521) --- .../txs/executor/staker_tx_verification.go | 11 ----- .../txs/executor/standard_tx_executor_test.go | 49 ++++++------------- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/vms/platformvm/txs/executor/staker_tx_verification.go b/vms/platformvm/txs/executor/staker_tx_verification.go index 72ef8561ad43..b84ad9d6e2b1 100644 --- a/vms/platformvm/txs/executor/staker_tx_verification.go +++ b/vms/platformvm/txs/executor/staker_tx_verification.go @@ -41,7 +41,6 @@ var ( ErrDurangoUpgradeNotActive = errors.New("attempting to use a Durango-upgrade feature prior to activation") ErrAddValidatorTxPostDurango = errors.New("AddValidatorTx is not permitted post-Durango") ErrAddDelegatorTxPostDurango = errors.New("AddDelegatorTx is not permitted post-Durango") - ErrRemoveValidatorManagedSubnet = errors.New("RemoveSubnetValidatorTx cannot be used to remove a validator from a Subnet with a manager") ) // verifySubnetValidatorPrimaryNetworkRequirements verifies the primary @@ -307,16 +306,6 @@ func verifyRemoveSubnetValidatorTx( return nil, false, err } - if backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { - _, err := chainState.GetSubnetConversion(tx.Subnet) - if err == nil { - return nil, false, fmt.Errorf("%w: %q", ErrRemoveValidatorManagedSubnet, tx.Subnet) - } - if err != database.ErrNotFound { - return nil, false, err - } - } - isCurrentValidator := true vdr, err := chainState.GetCurrentValidator(tx.Subnet, tx.NodeID) if err == database.ErrNotFound { diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 0301748101bc..43a603ac66e1 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -1755,10 +1755,22 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { env.state.EXPECT().DeleteUTXO(gomock.Any()).Times(len(env.unsignedTx.Ins)) env.state.EXPECT().AddUTXO(gomock.Any()).Times(len(env.unsignedTx.Outs)) + // This isn't actually called, but is added here as a regression + // test to ensure that converted subnets can still remove + // permissioned validators. + env.state.EXPECT().GetSubnetConversion(env.unsignedTx.Subnet).Return( + state.SubnetConversion{ + ConversionID: ids.GenerateTestID(), + ChainID: ids.GenerateTestID(), + Addr: []byte("address"), + }, + nil, + ).AnyTimes() + cfg := &config.Config{ - UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), + UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Etna, env.latestForkTime), } - feeCalculator := state.PickFeeCalculator(cfg, env.state) + feeCalculator := state.NewStaticFeeCalculator(cfg, env.state.GetTimestamp()) e := &StandardTxExecutor{ Backend: &Backend{ Config: cfg, @@ -1997,39 +2009,6 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { }, expectedErr: ErrFlowCheckFailed, }, - { - name: "attempted to remove subnet validator after subnet conversion has occurred", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { - env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) - env.state.EXPECT().GetSubnetConversion(env.unsignedTx.Subnet).Return( - state.SubnetConversion{ - ConversionID: ids.GenerateTestID(), - ChainID: ids.GenerateTestID(), - Addr: []byte("address"), - }, - nil, - ).AnyTimes() - env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() - - cfg := &config.Config{ - UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Etna, env.latestForkTime), - } - e := &StandardTxExecutor{ - Backend: &Backend{ - Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, - Fx: env.fx, - FlowChecker: env.flowChecker, - Ctx: &snow.Context{}, - }, - Tx: env.tx, - State: env.state, - } - e.Bootstrapped.Set(true) - return env.unsignedTx, e - }, - expectedErr: ErrRemoveValidatorManagedSubnet, - }, } for _, tt := range tests { From b51d94b9d13cceaf9a98138249854ddc12504820 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 17:36:16 -0500 Subject: [PATCH 111/184] ACP-77: Refactor e2e test (#3522) Co-authored-by: marun --- tests/e2e/p/l1.go | 146 ++++++++++++++++++++++++ tests/e2e/p/permissionless_layer_one.go | 102 ----------------- 2 files changed, 146 insertions(+), 102 deletions(-) create mode 100644 tests/e2e/p/l1.go delete mode 100644 tests/e2e/p/permissionless_layer_one.go diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go new file mode 100644 index 000000000000..544f970f4f8f --- /dev/null +++ b/tests/e2e/p/l1.go @@ -0,0 +1,146 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package p + +import ( + "math" + "time" + + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" + "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +var _ = e2e.DescribePChain("[L1]", func() { + tc := e2e.NewTestContext() + require := require.New(tc) + + ginkgo.It("creates and updates L1 validators", func() { + env := e2e.GetEnv(tc) + nodeURI := env.GetRandomNodeURI() + + tc.By("verifying Etna is activated", func() { + infoClient := info.NewClient(nodeURI.URI) + upgrades, err := infoClient.Upgrades(tc.DefaultContext()) + require.NoError(err) + + now := time.Now() + if !upgrades.IsEtnaActivated(now) { + ginkgo.Skip("Etna is not activated. L1s are enabled post-Etna, skipping test.") + } + }) + + tc.By("loading the wallet") + var ( + keychain = env.NewKeychain() + baseWallet = e2e.NewWallet(tc, keychain, nodeURI) + pWallet = baseWallet.P() + pClient = platformvm.NewClient(nodeURI.URI) + owner = &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + } + ) + + tc.By("creating the chain genesis") + genesisKey, err := secp256k1.NewPrivateKey() + require.NoError(err) + + genesisBytes, err := genesis.Codec.Marshal(genesis.CodecVersion, &genesis.Genesis{ + Timestamp: time.Now().Unix(), + Allocations: []genesis.Allocation{ + { + Address: genesisKey.Address(), + Balance: math.MaxUint64, + }, + }, + }) + require.NoError(err) + + var subnetID ids.ID + tc.By("issuing a CreateSubnetTx", func() { + subnetTx, err := pWallet.IssueCreateSubnetTx( + owner, + tc.WithDefaultContext(), + ) + require.NoError(err) + + subnetID = subnetTx.ID() + }) + + tc.By("verifying a Permissioned Subnet was successfully created", func() { + require.NotEqual(constants.PrimaryNetworkID, subnetID) + + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvm.GetSubnetClientResponse{ + IsPermissioned: true, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + }, + subnet, + ) + }) + + var chainID ids.ID + tc.By("issuing a CreateChainTx", func() { + chainTx, err := pWallet.IssueCreateChainTx( + subnetID, + genesisBytes, + constants.XSVMID, + nil, + "No Permissions", + tc.WithDefaultContext(), + ) + require.NoError(err) + + chainID = chainTx.ID() + }) + + address := []byte{} + tc.By("issuing a ConvertSubnetTx", func() { + _, err := pWallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + tc.WithDefaultContext(), + ) + require.NoError(err) + }) + + tc.By("verifying the Permissioned Subnet was converted to an L1", func() { + tc.By("verifying the subnet reports as being converted", func() { + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvm.GetSubnetClientResponse{ + IsPermissioned: false, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + ManagerChainID: chainID, + ManagerAddress: address, + }, + subnet, + ) + }) + }) + + _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) + }) +}) diff --git a/tests/e2e/p/permissionless_layer_one.go b/tests/e2e/p/permissionless_layer_one.go deleted file mode 100644 index edd1dbf7f9e4..000000000000 --- a/tests/e2e/p/permissionless_layer_one.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package p - -import ( - "time" - - "github.com/onsi/ginkgo/v2" - "github.com/stretchr/testify/require" - - "github.com/ava-labs/avalanchego/api/info" - "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/tests/fixture/e2e" - "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/vms/platformvm" - "github.com/ava-labs/avalanchego/vms/secp256k1fx" -) - -var _ = e2e.DescribePChain("[Permissionless L1]", func() { - tc := e2e.NewTestContext() - require := require.New(tc) - - ginkgo.It("creates a Permissionless L1", func() { - env := e2e.GetEnv(tc) - nodeURI := env.GetRandomNodeURI() - infoClient := info.NewClient(nodeURI.URI) - - tc.By("fetching upgrade config") - upgrades, err := infoClient.Upgrades(tc.DefaultContext()) - require.NoError(err) - - tc.By("verifying Etna is activated") - now := time.Now() - if !upgrades.IsEtnaActivated(now) { - ginkgo.Skip("Etna is not activated. Permissionless L1s are enabled post-Etna, skipping test.") - } - - keychain := env.NewKeychain() - baseWallet := e2e.NewWallet(tc, keychain, nodeURI) - - pWallet := baseWallet.P() - pClient := platformvm.NewClient(nodeURI.URI) - - owner := &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - } - - tc.By("issuing a CreateSubnetTx") - subnetTx, err := pWallet.IssueCreateSubnetTx( - owner, - tc.WithDefaultContext(), - ) - require.NoError(err) - - tc.By("verifying a Permissioned Subnet was successfully created") - subnetID := subnetTx.ID() - require.NotEqual(subnetID, constants.PrimaryNetworkID) - - res, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) - require.NoError(err) - - require.Equal(platformvm.GetSubnetClientResponse{ - IsPermissioned: true, - ControlKeys: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - Threshold: 1, - }, res) - - chainID := ids.GenerateTestID() - address := []byte{'a', 'd', 'd', 'r', 'e', 's', 's'} - - tc.By("issuing a ConvertSubnetTx") - _, err = pWallet.IssueConvertSubnetTx( - subnetID, - chainID, - address, - tc.WithDefaultContext(), - ) - require.NoError(err) - - tc.By("verifying the Permissioned Subnet was converted to a Permissionless L1") - res, err = pClient.GetSubnet(tc.DefaultContext(), subnetID) - require.NoError(err) - - require.Equal(platformvm.GetSubnetClientResponse{ - IsPermissioned: false, - ControlKeys: []ids.ShortID{ - keychain.Keys[0].Address(), - }, - Threshold: 1, - ManagerChainID: chainID, - ManagerAddress: address, - }, res) - - _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) - }) -}) From 5b08c6ad1afe3616ce3477c2bbec075461ba7c51 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 22:25:54 -0500 Subject: [PATCH 112/184] test subnet state prior to conversion --- tests/e2e/p/l1.go | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 82e938e9cc2a..e9c8ec89a6c3 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -124,6 +124,35 @@ var _ = e2e.DescribePChain("[L1]", func() { chainID = chainTx.ID() }) + verifyValidatorSet := func(expectedValidators map[ids.NodeID]*snowvalidators.GetValidatorOutput) { + height, err := pClient.GetHeight(tc.DefaultContext()) + require.NoError(err) + + subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) + require.NoError(err) + require.Equal(expectedValidators, subnetValidators) + } + tc.By("verifying the Permissioned Subnet is configured as expected", func() { + tc.By("verifying the subnet reports as permissioned", func() { + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvm.GetSubnetClientResponse{ + IsPermissioned: true, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + }, + subnet, + ) + }) + + tc.By("verifying the validator set is empty", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{}) + }) + }) + tc.By("creating the genesis validator") subnetGenesisNode := e2e.AddEphemeralNode(tc, env.GetNetwork(), tmpnet.FlagsMap{ config.TrackSubnetsKey: subnetID.String(), @@ -154,14 +183,6 @@ var _ = e2e.DescribePChain("[L1]", func() { require.NoError(err) }) - verifyValidatorSet := func(expectedValidators map[ids.NodeID]*snowvalidators.GetValidatorOutput) { - height, err := pClient.GetHeight(tc.DefaultContext()) - require.NoError(err) - - subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) - require.NoError(err) - require.Equal(expectedValidators, subnetValidators) - } tc.By("verifying the Permissioned Subnet was converted to an L1", func() { expectedConversionID, err := warpmessage.SubnetConversionID(warpmessage.SubnetConversionData{ SubnetID: subnetID, From 9884a93516ebf75471693175b1a15c29f05c9f3b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 22:36:19 -0500 Subject: [PATCH 113/184] nits --- tests/e2e/p/l1.go | 10 ++++-- .../register-subnet-validator/main.go | 33 +++++++++---------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 5456f4d94e7a..5a8b0fe844e3 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -51,6 +51,11 @@ const ( genesisBalance = units.Avax registerWeight = genesisWeight / 10 registerBalance = 0 + + // Validator registration attempts expire 5 minutes after they are created + expiryDelay = 5 * time.Minute + // P2P message requests timeout after 10 seconds + p2pTimeout = 10 * time.Second ) var _ = e2e.DescribePChain("[L1]", func() { @@ -289,11 +294,12 @@ var _ = e2e.DescribePChain("[L1]", func() { }) tc.By("creating the RegisterSubnetValidatorMessage") + expiry := uint64(time.Now().Add(expiryDelay).Unix()) // This message will expire in 5 minutes registerSubnetValidatorMessage, err := warpmessage.NewRegisterSubnetValidator( subnetID, subnetRegisterNode.NodeID, registerNodePoP.PublicKey, - uint64(time.Now().Add(5*time.Minute).Unix()), + expiry, warpmessage.PChainOwner{}, warpmessage.PChainOwner{}, registerWeight, @@ -379,7 +385,7 @@ func wrapWarpSignatureRequest( logging.NoLog{}, prometheus.NewRegistry(), constants.DefaultNetworkCompressionType, - 10*time.Second, + p2pTimeout, ) if err != nil { return nil, err diff --git a/wallet/subnet/primary/examples/register-subnet-validator/main.go b/wallet/subnet/primary/examples/register-subnet-validator/main.go index 05d51aa1719b..68248a7e18eb 100644 --- a/wallet/subnet/primary/examples/register-subnet-validator/main.go +++ b/wallet/subnet/primary/examples/register-subnet-validator/main.go @@ -8,7 +8,6 @@ import ( "encoding/hex" "encoding/json" "log" - "os" "time" "github.com/ava-labs/avalanchego/api/info" @@ -26,31 +25,27 @@ import ( func main() { key := genesis.EWOQKey - uri := "http://localhost:9710" + uri := primary.LocalAPIURI kc := secp256k1fx.NewKeychain(key) subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") chainID := ids.FromStringOrPanic("2BMFrJ9xeh5JdwZEx6uuFcjfZC2SV2hdbMT8ee5HrvjtfJb5br") - addressHex := "" + address := []byte{} weight := uint64(1) + blsSKHex := "3f783929b295f16cd1172396acb23b20eed057b9afb1caa419e9915f92860b35" - address, err := hex.DecodeString(addressHex) + blsSKBytes, err := hex.DecodeString(blsSKHex) if err != nil { - log.Fatalf("failed to decode address %q: %s\n", addressHex, err) + log.Fatalf("failed to decode secret key: %s\n", err) } - ctx := context.Background() - infoClient := info.NewClient(uri) - - skBytes, err := os.ReadFile("/Users/stephen/.avalanchego/staking/signer.key") - if err != nil { - log.Fatalf("failed to read signer key: %s\n", err) - } - - sk, err := bls.SecretKeyFromBytes(skBytes) + sk, err := bls.SecretKeyFromBytes(blsSKBytes) if err != nil { log.Fatalf("failed to parse secret key: %s\n", err) } + ctx := context.Background() + infoClient := info.NewClient(uri) + nodeInfoStartTime := time.Now() nodeID, nodePoP, err := infoClient.GetNodeID(ctx) if err != nil { @@ -59,7 +54,7 @@ func main() { log.Printf("fetched node ID %s in %s\n", nodeID, time.Since(nodeInfoStartTime)) // MakeWallet fetches the available UTXOs owned by [kc] on the network that - // [uri] is hosting and registers [subnetID]. + // [uri] is hosting. walletSyncStartTime := time.Now() wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{ URI: uri, @@ -75,11 +70,12 @@ func main() { pWallet := wallet.P() context := pWallet.Builder().Context() + expiry := uint64(time.Now().Add(5 * time.Minute).Unix()) // This message will expire in 5 minutes addressedCallPayload, err := message.NewRegisterSubnetValidator( subnetID, nodeID, nodePoP.PublicKey, - uint64(time.Now().Add(5*time.Minute).Unix()), + expiry, message.PChainOwner{}, message.PChainOwner{}, weight, @@ -110,8 +106,9 @@ func main() { log.Fatalf("failed to create unsigned Warp message: %s\n", err) } - signers := set.NewBits() - signers.Add(0) // [signers] has weight from [vdr[0]] + // This example assumes that the hard-coded BLS key is for the first + // validator in the signature bit-set. + signers := set.NewBits(0) unsignedBytes := unsignedWarp.Bytes() sig := bls.Sign(sk, unsignedBytes) From a7cd427491320bf36016e7ac68bdd273c2a04928 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Tue, 5 Nov 2024 22:49:18 -0500 Subject: [PATCH 114/184] nit --- tests/e2e/p/l1.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 5a8b0fe844e3..6ee961450a34 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -333,8 +333,7 @@ var _ = e2e.DescribePChain("[L1]", func() { require.True(ok) tc.By("creating the signed warp message to register the validator") - signers := set.NewBits() - signers.Add(0) // [signers] has weight from the genesis peer + signers := set.NewBits(0) // [signers] has weight from the genesis peer var sigBytes [bls.SignatureLen]byte copy(sigBytes[:], bls.SignatureToBytes(registerSubnetValidatorSignature)) From 4c2605c6210402c2cb96e181d0916313373a2cc3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 01:06:38 -0500 Subject: [PATCH 115/184] wip --- vms/platformvm/block/executor/verifier.go | 31 +- .../block/executor/verifier_test.go | 98 +++++++ vms/platformvm/metrics/tx_metrics.go | 8 +- .../txs/executor/atomic_tx_executor.go | 53 +++- .../txs/executor/standard_tx_executor.go | 270 +++++++++--------- vms/platformvm/txs/fee/complexity.go | 148 +++++----- vms/platformvm/txs/fee/static_calculator.go | 22 +- vms/platformvm/txs/visitor.go | 9 +- wallet/chain/p/signer/visitor.go | 32 +-- wallet/chain/p/wallet/backend_visitor.go | 40 +-- 10 files changed, 419 insertions(+), 292 deletions(-) diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index 1570a1b3c2ac..24d2083d2ae3 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -230,23 +230,22 @@ func (v *verifier) ApricotAtomicBlock(b *block.ApricotAtomicBlock) error { } feeCalculator := state.NewStaticFeeCalculator(v.txExecutorBackend.Config, currentTimestamp) - atomicExecutor := executor.AtomicTxExecutor{ - Backend: v.txExecutorBackend, - FeeCalculator: feeCalculator, - ParentID: parentID, - StateVersions: v, - Tx: b.Tx, - } - - if err := b.Tx.Unsigned.Visit(&atomicExecutor); err != nil { + onAcceptState, atomicInputs, atomicRequests, err := executor.AtomicTx( + v.txExecutorBackend, + feeCalculator, + parentID, + v, + b.Tx, + ) + if err != nil { txID := b.Tx.ID() v.MarkDropped(txID, err) // cache tx as dropped - return fmt.Errorf("tx %s failed semantic verification: %w", txID, err) + return err } - atomicExecutor.OnAccept.AddTx(b.Tx, status.Committed) + onAcceptState.AddTx(b.Tx, status.Committed) - if err := v.verifyUniqueInputs(parentID, atomicExecutor.Inputs); err != nil { + if err := v.verifyUniqueInputs(parentID, atomicInputs); err != nil { return err } @@ -256,11 +255,11 @@ func (v *verifier) ApricotAtomicBlock(b *block.ApricotAtomicBlock) error { v.blkIDToState[blkID] = &blockState{ statelessBlock: b, - onAcceptState: atomicExecutor.OnAccept, + onAcceptState: onAcceptState, - inputs: atomicExecutor.Inputs, - timestamp: atomicExecutor.OnAccept.GetTimestamp(), - atomicRequests: atomicExecutor.AtomicRequests, + inputs: atomicInputs, + timestamp: onAcceptState.GetTimestamp(), + atomicRequests: atomicRequests, verifiedHeights: set.Of(v.pChainHeight), } return nil diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 0a5d9e3c662f..db8e3761f3d1 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -182,6 +182,104 @@ func TestVerifierVisitProposalBlock(t *testing.T) { require.NoError(blk.Verify(context.Background())) } +func TestVerifierVisitAtomicBlock2(t *testing.T) { + s := statetest.New(t, statetest.Config{}) + verifier := newTestVerifier(t, s) + wallet := txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + s, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + nil, // chainIDs + ) + + baseTx0, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) + require.NoError(t, err) + baseTx1, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) + require.NoError(t, err) + + blockComplexity, err := txfee.TxComplexity(baseTx0.Unsigned, baseTx1.Unsigned) + require.NoError(t, err) + blockGas, err := blockComplexity.ToGas(verifier.txExecutorBackend.Config.DynamicFeeConfig.Weights) + require.NoError(t, err) + + const secondsToAdvance = 10 + + initialFeeState := gas.State{} + feeStateAfterTimeAdvanced := initialFeeState.AdvanceTime( + verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxCapacity, + verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxPerSecond, + verifier.txExecutorBackend.Config.DynamicFeeConfig.TargetPerSecond, + secondsToAdvance, + ) + feeStateAfterGasConsumed, err := feeStateAfterTimeAdvanced.ConsumeGas(blockGas) + require.NoError(t, err) + + tests := []struct { + name string + timestamp time.Time + expectedErr error + expectedFeeState gas.State + }{ + { + name: "no capacity", + timestamp: genesistest.DefaultValidatorStartTime, + expectedErr: gas.ErrInsufficientCapacity, + }, + { + name: "updates fee state", + timestamp: genesistest.DefaultValidatorStartTime.Add(secondsToAdvance * time.Second), + expectedFeeState: feeStateAfterGasConsumed, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + // Clear the state to prevent prior tests from impacting this test. + clear(verifier.blkIDToState) + + verifier.txExecutorBackend.Clk.Set(test.timestamp) + timestamp, _, err := state.NextBlockTime( + verifier.txExecutorBackend.Config.ValidatorFeeConfig, + s, + verifier.txExecutorBackend.Clk, + ) + require.NoError(err) + + lastAcceptedID := s.GetLastAccepted() + lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) + require.NoError(err) + + blk, err := block.NewBanffStandardBlock( + timestamp, + lastAcceptedID, + lastAccepted.Height()+1, + []*txs.Tx{ + baseTx0, + baseTx1, + }, + ) + require.NoError(err) + + blkID := blk.ID() + err = blk.Visit(verifier) + require.ErrorIs(err, test.expectedErr) + if err != nil { + require.NotContains(verifier.blkIDToState, blkID) + return + } + + require.Contains(verifier.blkIDToState, blkID) + blockState := verifier.blkIDToState[blkID] + require.Equal(blk, blockState.statelessBlock) + require.Equal(test.expectedFeeState, blockState.onAcceptState.GetFeeState()) + }) + } +} + func TestVerifierVisitAtomicBlock(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index fd6c63494f7c..7957cb6ab2e9 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -132,16 +132,16 @@ func (m *txMetrics) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) er return nil } -func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (m *txMetrics) BaseTx(*txs.BaseTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "convert_subnet", + txLabel: "base", }).Inc() return nil } -func (m *txMetrics) BaseTx(*txs.BaseTx) error { +func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "base", + txLabel: "convert_subnet", }).Inc() return nil } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 190c5ec114c9..c1a4b7b54627 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -4,6 +4,8 @@ package executor import ( + "fmt" + "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" @@ -18,16 +20,37 @@ var _ txs.Visitor = (*AtomicTxExecutor)(nil) // the execution was moved to be performed inside of the standardTxExecutor. type AtomicTxExecutor struct { // inputs, to be filled before visitor methods are called - *Backend - FeeCalculator fee.Calculator - ParentID ids.ID - StateVersions state.Versions - Tx *txs.Tx + backend *Backend + feeCalculator fee.Calculator + parentID ids.ID + stateVersions state.Versions + tx *txs.Tx // outputs of visitor execution OnAccept state.Diff Inputs set.Set[ids.ID] - AtomicRequests map[ids.ID]*atomic.Requests + atomicRequests map[ids.ID]*atomic.Requests +} + +func AtomicTx( + backend *Backend, + feeCalculator fee.Calculator, + parentID ids.ID, + stateVersions state.Versions, + tx *txs.Tx, +) (state.Diff, set.Set[ids.ID], map[ids.ID]*atomic.Requests, error) { + atomicExecutor := AtomicTxExecutor{ + backend: backend, + feeCalculator: feeCalculator, + parentID: parentID, + stateVersions: stateVersions, + tx: tx, + } + if err := tx.Unsigned.Visit(&atomicExecutor); err != nil { + txID := tx.ID() + return nil, nil, nil, fmt.Errorf("atomic tx %s failed execution: %w", txID, err) + } + return atomicExecutor.OnAccept, atomicExecutor.Inputs, atomicExecutor.atomicRequests, nil } func (*AtomicTxExecutor) AddValidatorTx(*txs.AddValidatorTx) error { @@ -66,15 +89,15 @@ func (*AtomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*AtomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*AtomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*AtomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } @@ -96,8 +119,8 @@ func (e *AtomicTxExecutor) ExportTx(tx *txs.ExportTx) error { func (e *AtomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { onAccept, err := state.NewDiff( - e.ParentID, - e.StateVersions, + e.parentID, + e.stateVersions, ) if err != nil { return err @@ -105,13 +128,13 @@ func (e *AtomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { e.OnAccept = onAccept executor := StandardTxExecutor{ - Backend: e.Backend, + Backend: e.backend, State: e.OnAccept, - FeeCalculator: e.FeeCalculator, - Tx: e.Tx, + FeeCalculator: e.feeCalculator, + Tx: e.tx, } err = tx.Visit(&executor) e.Inputs = executor.Inputs - e.AtomicRequests = executor.AtomicRequests + e.atomicRequests = executor.AtomicRequests return err } diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 1c08880a94e0..1136f60e6a1d 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -53,6 +53,82 @@ func (*StandardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } +func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { + if tx.Validator.NodeID == ids.EmptyNodeID { + return errEmptyNodeID + } + + if _, err := verifyAddValidatorTx( + e.Backend, + e.FeeCalculator, + e.State, + e.Tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); err != nil { + return err + } + + txID := e.Tx.ID() + avax.Consume(e.State, tx.Ins) + avax.Produce(e.State, txID, tx.Outs) + + if e.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.Ctx.NodeID { + e.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + zap.String("reason", "primary network is not being fully synced"), + zap.Stringer("txID", txID), + zap.String("txType", "addValidator"), + zap.Stringer("nodeID", tx.Validator.NodeID), + ) + } + return nil +} + +func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { + if err := verifyAddSubnetValidatorTx( + e.Backend, + e.FeeCalculator, + e.State, + e.Tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) AddDelegatorTx(tx *txs.AddDelegatorTx) error { + if _, err := verifyAddDelegatorTx( + e.Backend, + e.FeeCalculator, + e.State, + e.Tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) CreateChainTx(tx *txs.CreateChainTx) error { if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { return err @@ -326,82 +402,6 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { return nil } -func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { - if tx.Validator.NodeID == ids.EmptyNodeID { - return errEmptyNodeID - } - - if _, err := verifyAddValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) - - if e.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.Ctx.NodeID { - e.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", - zap.String("reason", "primary network is not being fully synced"), - zap.Stringer("txID", txID), - zap.String("txType", "addValidator"), - zap.Stringer("nodeID", tx.Validator.NodeID), - ) - } - return nil -} - -func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { - if err := verifyAddSubnetValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); 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) AddDelegatorTx(tx *txs.AddDelegatorTx) error { - if _, err := verifyAddDelegatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) - return nil -} - // Verifies a [*txs.RemoveSubnetValidatorTx] and, if it passes, executes it on // [e.State]. For verification rules, see [verifyRemoveSubnetValidatorTx]. This // transaction will result in [tx.NodeID] being removed as a validator of @@ -495,65 +495,6 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error return nil } -func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { - var ( - currentTimestamp = e.State.GetTimestamp() - upgrades = e.Backend.Config.UpgradeConfig - ) - if !upgrades.IsEtnaActivated(currentTimestamp) { - return errEtnaUpgradeNotActive - } - - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { - return err - } - - if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { - return err - } - - baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.Subnet, tx.SubnetAuth) - if err != nil { - return err - } - - // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) - if err != nil { - return err - } - if err := e.Backend.FlowChecker.VerifySpend( - tx, - e.State, - tx.Ins, - tx.Outs, - baseTxCreds, - map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, - }, - ); err != nil { - return err - } - - txID := e.Tx.ID() - - // Consume the UTXOS - avax.Consume(e.State, tx.Ins) - // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) - // Track the subnet conversion in the database - e.State.SetSubnetConversion( - tx.Subnet, - state.SubnetConversion{ - // TODO: Populate the conversionID - ConversionID: ids.Empty, - ChainID: tx.ChainID, - Addr: tx.Address, - }, - ) - return nil -} - func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if err := verifyAddPermissionlessValidatorTx( e.Backend, @@ -676,6 +617,65 @@ func (e *StandardTxExecutor) BaseTx(tx *txs.BaseTx) error { return nil } +func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + var ( + currentTimestamp = e.State.GetTimestamp() + upgrades = e.Backend.Config.UpgradeConfig + ) + if !upgrades.IsEtnaActivated(currentTimestamp) { + return errEtnaUpgradeNotActive + } + + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + return err + } + + if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { + return err + } + + baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + + // Verify the flowcheck + fee, err := e.FeeCalculator.CalculateFee(tx) + if err != nil { + return err + } + if err := e.Backend.FlowChecker.VerifySpend( + tx, + e.State, + tx.Ins, + tx.Outs, + baseTxCreds, + map[ids.ID]uint64{ + e.Ctx.AVAXAssetID: fee, + }, + ); err != nil { + return err + } + + txID := e.Tx.ID() + + // Consume the UTXOS + avax.Consume(e.State, tx.Ins) + // Produce the UTXOS + avax.Produce(e.State, txID, tx.Outs) + // Track the subnet conversion in the database + e.State.SetSubnetConversion( + tx.Subnet, + state.SubnetConversion{ + // TODO: Populate the conversionID + ConversionID: ids.Empty, + ChainID: tx.ChainID, + Addr: tx.Address, + }, + ) + return nil +} + // Creates the staker as defined in [stakerTx] and adds it to [e.State]. func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 02942e09b76f..59dea8b6a600 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -383,11 +383,11 @@ type complexityVisitor struct { output gas.Dimensions } -func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { return ErrUnsupportedTx } -func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { +func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { return ErrUnsupportedTx } @@ -403,60 +403,6 @@ func (*complexityVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrUnsupportedTx } -func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - signerComplexity, err := SignerComplexity(tx.Signer) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) - if err != nil { - return err - } - delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( - &baseTxComplexity, - &signerComplexity, - &outputsComplexity, - &validatorOwnerComplexity, - &delegatorOwnerComplexity, - ) - return err -} - -func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( - &baseTxComplexity, - &ownerComplexity, - &outputsComplexity, - ) - return err -} - func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { @@ -473,15 +419,6 @@ func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) e return err } -func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { - baseTxComplexity, err := baseTxComplexity(tx) - if err != nil { - return err - } - c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) - return err -} - func (c *complexityVisitor) CreateChainTx(tx *txs.CreateChainTx) error { bandwidth, err := math.Mul(uint64(len(tx.FxIDs)), ids.IDLen) if err != nil { @@ -534,6 +471,23 @@ func (c *complexityVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return err } +func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + // TODO: Should imported inputs be more complex? + inputsComplexity, err := InputComplexity(tx.ImportedInputs...) + if err != nil { + return err + } + c.output, err = IntrinsicImportTxComplexities.Add( + &baseTxComplexity, + &inputsComplexity, + ) + return err +} + func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { @@ -551,35 +505,72 @@ func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { return err } -func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { +func (c *complexityVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - // TODO: Should imported inputs be more complex? - inputsComplexity, err := InputComplexity(tx.ImportedInputs...) + authComplexity, err := AuthComplexity(tx.SubnetAuth) if err != nil { return err } - c.output, err = IntrinsicImportTxComplexities.Add( + c.output, err = IntrinsicRemoveSubnetValidatorTxComplexities.Add( &baseTxComplexity, - &inputsComplexity, + &authComplexity, ) return err } -func (c *complexityVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { +func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + // TODO: Should we include additional complexity for subnets? baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - authComplexity, err := AuthComplexity(tx.SubnetAuth) + signerComplexity, err := SignerComplexity(tx.Signer) if err != nil { return err } - c.output, err = IntrinsicRemoveSubnetValidatorTxComplexities.Add( + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) + if err != nil { + return err + } + delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( &baseTxComplexity, - &authComplexity, + &signerComplexity, + &outputsComplexity, + &validatorOwnerComplexity, + &delegatorOwnerComplexity, + ) + return err +} + +func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + // TODO: Should we include additional complexity for subnets? + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) + if err != nil { + return err + } + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( + &baseTxComplexity, + &ownerComplexity, + &outputsComplexity, ) return err } @@ -605,6 +596,15 @@ func (c *complexityVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwne return err } +func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { + baseTxComplexity, err := baseTxComplexity(tx) + if err != nil { + return err + } + c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) + return err +} + func (c *complexityVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { diff --git a/vms/platformvm/txs/fee/static_calculator.go b/vms/platformvm/txs/fee/static_calculator.go index 888ccba8621c..1b97349ce2cb 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -76,21 +76,26 @@ func (c *staticVisitor) CreateSubnetTx(*txs.CreateSubnetTx) error { return nil } -func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (c *staticVisitor) ImportTx(*txs.ImportTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { - c.fee = c.config.TransformSubnetTxFee +func (c *staticVisitor) ExportTx(*txs.ExportTx) error { + c.fee = c.config.TxFee return nil } -func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { c.fee = c.config.TxFee return nil } +func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { + c.fee = c.config.TransformSubnetTxFee + return nil +} + func (c *staticVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if tx.Subnet != constants.PrimaryNetworkID { c.fee = c.config.AddSubnetValidatorFee @@ -109,17 +114,12 @@ func (c *staticVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDe return nil } -func (c *staticVisitor) BaseTx(*txs.BaseTx) error { - c.fee = c.config.TxFee - return nil -} - -func (c *staticVisitor) ImportTx(*txs.ImportTx) error { +func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) ExportTx(*txs.ExportTx) error { +func (c *staticVisitor) BaseTx(*txs.BaseTx) error { c.fee = c.config.TxFee return nil } diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 142a6115d7af..21c46476fa5f 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -5,6 +5,7 @@ package txs // Allow vm to execute custom logic against the underlying transaction types. type Visitor interface { + // Apricot Transactions: AddValidatorTx(*AddValidatorTx) error AddSubnetValidatorTx(*AddSubnetValidatorTx) error AddDelegatorTx(*AddDelegatorTx) error @@ -14,11 +15,17 @@ type Visitor interface { ExportTx(*ExportTx) error AdvanceTimeTx(*AdvanceTimeTx) error RewardValidatorTx(*RewardValidatorTx) error + + // Banff Transactions: RemoveSubnetValidatorTx(*RemoveSubnetValidatorTx) error TransformSubnetTx(*TransformSubnetTx) error AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error + + // Durango Transactions: TransferSubnetOwnershipTx(*TransferSubnetOwnershipTx) error - ConvertSubnetTx(*ConvertSubnetTx) error BaseTx(*BaseTx) error + + // Etna Transactions: + ConvertSubnetTx(*ConvertSubnetTx) error } diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index 38d501c908b0..b358e1f5d5ea 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -51,14 +51,6 @@ func (*visitor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrUnsupportedTxType } -func (s *visitor) BaseTx(tx *txs.BaseTx) error { - txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) - if err != nil { - return err - } - return sign(s.tx, false, txSigners) -} - func (s *visitor) AddValidatorTx(tx *txs.AddValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { @@ -143,7 +135,7 @@ func (s *visitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error return sign(s.tx, true, txSigners) } -func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { +func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -156,20 +148,23 @@ func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) e return sign(s.tx, true, txSigners) } -func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { +func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + return sign(s.tx, true, txSigners) +} + +func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } -func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { +func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -182,19 +177,24 @@ func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return sign(s.tx, true, txSigners) } -func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { +func (s *visitor) BaseTx(tx *txs.BaseTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, false, txSigners) } -func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { +func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } + subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } diff --git a/wallet/chain/p/wallet/backend_visitor.go b/wallet/chain/p/wallet/backend_visitor.go index 8e44ee3b5e2d..f2f9e646edf8 100644 --- a/wallet/chain/p/wallet/backend_visitor.go +++ b/wallet/chain/p/wallet/backend_visitor.go @@ -58,26 +58,6 @@ func (b *backendVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return b.baseTx(&tx.BaseTx) } -func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - b.b.setSubnetOwner( - tx.Subnet, - tx.Owner, - ) - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { - return b.baseTx(tx) -} - func (b *backendVisitor) ImportTx(tx *txs.ImportTx) error { err := b.b.removeUTXOs( b.ctx, @@ -111,6 +91,10 @@ func (b *backendVisitor) ExportTx(tx *txs.ExportTx) error { return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return b.baseTx(&tx.BaseTx) } @@ -123,6 +107,22 @@ func (b *backendVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessD return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + b.b.setSubnetOwner( + tx.Subnet, + tx.Owner, + ) + return b.baseTx(&tx.BaseTx) +} + +func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { + return b.baseTx(tx) +} + +func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) baseTx(tx *txs.BaseTx) error { return b.b.removeUTXOs( b.ctx, From 436b471c161c69de30136bb5477966fb65dcf566 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 01:23:30 -0500 Subject: [PATCH 116/184] Remove unused proto field --- proto/pb/platformvm/platformvm.pb.go | 31 ++++++++++------------------ proto/platformvm/platformvm.proto | 1 - 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/proto/pb/platformvm/platformvm.pb.go b/proto/pb/platformvm/platformvm.pb.go index 10b40671a881..15c2e96f37f2 100644 --- a/proto/pb/platformvm/platformvm.pb.go +++ b/proto/pb/platformvm/platformvm.pb.go @@ -30,7 +30,6 @@ type SubnetValidatorRegistrationJustification struct { // *SubnetValidatorRegistrationJustification_ConvertSubnetTxData // *SubnetValidatorRegistrationJustification_RegisterSubnetValidatorMessage Preimage isSubnetValidatorRegistrationJustification_Preimage `protobuf_oneof:"preimage"` - Filter []byte `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` } func (x *SubnetValidatorRegistrationJustification) Reset() { @@ -86,13 +85,6 @@ func (x *SubnetValidatorRegistrationJustification) GetRegisterSubnetValidatorMes return nil } -func (x *SubnetValidatorRegistrationJustification) GetFilter() []byte { - if x != nil { - return x.Filter - } - return nil -} - type isSubnetValidatorRegistrationJustification_Preimage interface { isSubnetValidatorRegistrationJustification_Preimage() } @@ -174,7 +166,7 @@ var File_platformvm_platformvm_proto protoreflect.FileDescriptor var file_platformvm_platformvm_proto_rawDesc = []byte{ 0x0a, 0x1b, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x70, - 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x22, 0xed, 0x01, 0x0a, 0x28, 0x53, 0x75, + 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x22, 0xd5, 0x01, 0x0a, 0x28, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x75, 0x73, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x72, @@ -187,17 +179,16 @@ var file_platformvm_platformvm_proto_rawDesc = []byte{ 0x64, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x1e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x0a, 0x0a, - 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x42, 0x0a, 0x0d, 0x53, 0x75, 0x62, - 0x6e, 0x65, 0x74, 0x49, 0x44, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x75, - 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, - 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x35, 0x5a, - 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x76, 0x61, 0x2d, - 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x61, 0x76, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x68, 0x65, 0x67, 0x6f, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, - 0x72, 0x6d, 0x76, 0x6d, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x22, 0x42, 0x0a, 0x0d, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x44, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x76, 0x61, 0x2d, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x61, 0x76, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x68, 0x65, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, + 0x62, 0x2f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x76, 0x6d, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/platformvm/platformvm.proto b/proto/platformvm/platformvm.proto index a1e9adf129d1..571be0d7862b 100644 --- a/proto/platformvm/platformvm.proto +++ b/proto/platformvm/platformvm.proto @@ -12,7 +12,6 @@ message SubnetValidatorRegistrationJustification { // The SubnetValidator is being removed from the Subnet bytes register_subnet_validator_message = 2; } - bytes filter = 3; } message SubnetIDIndex { From c13c57681a0d92c860a7a6e356870265366f44f7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 09:22:17 -0500 Subject: [PATCH 117/184] ACP-77: Update `ConvertSubnetTx` (#3397) Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Co-authored-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- tests/e2e/p/l1.go | 86 ++++++ vms/platformvm/txs/convert_subnet_tx.go | 73 ++++- vms/platformvm/txs/convert_subnet_tx_test.go | 264 +++++++++++++++++- .../txs/convert_subnet_tx_test_complex.json | 23 ++ .../txs/convert_subnet_tx_test_simple.json | 1 + .../txs/executor/standard_tx_executor.go | 78 +++++- .../txs/executor/standard_tx_executor_test.go | 143 ++++++++-- vms/platformvm/txs/fee/calculator_test.go | 8 +- vms/platformvm/txs/fee/complexity.go | 70 ++++- vms/platformvm/txs/fee/complexity_test.go | 80 ++++++ wallet/chain/p/builder/builder.go | 28 +- .../chain/p/builder/builder_with_options.go | 2 + wallet/chain/p/builder_test.go | 46 ++- wallet/chain/p/wallet/wallet.go | 5 +- wallet/chain/p/wallet/with_options.go | 2 + .../primary/examples/convert-subnet/main.go | 106 +++++++ 16 files changed, 970 insertions(+), 45 deletions(-) create mode 100644 wallet/subnet/primary/examples/convert-subnet/main.go diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 544f970f4f8f..e9c8ec89a6c3 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -11,13 +11,26 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/tests/fixture/e2e" + "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" "github.com/ava-labs/avalanchego/vms/platformvm" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + + snowvalidators "github.com/ava-labs/avalanchego/snow/validators" + warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" +) + +const ( + genesisWeight = units.Schmeckle + genesisBalance = units.Avax ) var _ = e2e.DescribePChain("[L1]", func() { @@ -111,18 +124,80 @@ var _ = e2e.DescribePChain("[L1]", func() { chainID = chainTx.ID() }) + verifyValidatorSet := func(expectedValidators map[ids.NodeID]*snowvalidators.GetValidatorOutput) { + height, err := pClient.GetHeight(tc.DefaultContext()) + require.NoError(err) + + subnetValidators, err := pClient.GetValidatorsAt(tc.DefaultContext(), subnetID, height) + require.NoError(err) + require.Equal(expectedValidators, subnetValidators) + } + tc.By("verifying the Permissioned Subnet is configured as expected", func() { + tc.By("verifying the subnet reports as permissioned", func() { + subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) + require.NoError(err) + require.Equal( + platformvm.GetSubnetClientResponse{ + IsPermissioned: true, + ControlKeys: []ids.ShortID{ + keychain.Keys[0].Address(), + }, + Threshold: 1, + }, + subnet, + ) + }) + + tc.By("verifying the validator set is empty", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{}) + }) + }) + + tc.By("creating the genesis validator") + subnetGenesisNode := e2e.AddEphemeralNode(tc, env.GetNetwork(), tmpnet.FlagsMap{ + config.TrackSubnetsKey: subnetID.String(), + }) + + genesisNodePoP, err := subnetGenesisNode.GetProofOfPossession() + require.NoError(err) + + genesisNodePK, err := bls.PublicKeyFromCompressedBytes(genesisNodePoP.PublicKey[:]) + require.NoError(err) + address := []byte{} tc.By("issuing a ConvertSubnetTx", func() { _, err := pWallet.IssueConvertSubnetTx( subnetID, chainID, address, + []*txs.ConvertSubnetValidator{ + { + NodeID: subnetGenesisNode.NodeID.Bytes(), + Weight: genesisWeight, + Balance: genesisBalance, + Signer: *genesisNodePoP, + }, + }, tc.WithDefaultContext(), ) require.NoError(err) }) tc.By("verifying the Permissioned Subnet was converted to an L1", func() { + expectedConversionID, err := warpmessage.SubnetConversionID(warpmessage.SubnetConversionData{ + SubnetID: subnetID, + ManagerChainID: chainID, + ManagerAddress: address, + Validators: []warpmessage.SubnetConversionValidatorData{ + { + NodeID: subnetGenesisNode.NodeID.Bytes(), + BLSPublicKey: genesisNodePoP.PublicKey, + Weight: genesisWeight, + }, + }, + }) + require.NoError(err) + tc.By("verifying the subnet reports as being converted", func() { subnet, err := pClient.GetSubnet(tc.DefaultContext(), subnetID) require.NoError(err) @@ -133,12 +208,23 @@ var _ = e2e.DescribePChain("[L1]", func() { keychain.Keys[0].Address(), }, Threshold: 1, + ConversionID: expectedConversionID, ManagerChainID: chainID, ManagerAddress: address, }, subnet, ) }) + + tc.By("verifying the validator set was updated", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, + }) + }) }) _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) diff --git a/vms/platformvm/txs/convert_subnet_tx.go b/vms/platformvm/txs/convert_subnet_tx.go index 3fa4193faf3c..7f76c962a207 100644 --- a/vms/platformvm/txs/convert_subnet_tx.go +++ b/vms/platformvm/txs/convert_subnet_tx.go @@ -4,22 +4,31 @@ package txs import ( + "bytes" "errors" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/vms/types" ) const MaxSubnetAddressLength = 4096 var ( - _ UnsignedTx = (*TransferSubnetOwnershipTx)(nil) + _ UnsignedTx = (*ConvertSubnetTx)(nil) + _ utils.Sortable[*ConvertSubnetValidator] = (*ConvertSubnetValidator)(nil) - ErrConvertPermissionlessSubnet = errors.New("cannot convert a permissionless subnet") - ErrAddressTooLong = errors.New("address is too long") + ErrConvertPermissionlessSubnet = errors.New("cannot convert a permissionless subnet") + ErrAddressTooLong = errors.New("address is too long") + ErrConvertMustIncludeValidators = errors.New("conversion must include at least one validator") + ErrConvertValidatorsNotSortedAndUnique = errors.New("conversion validators must be sorted and unique") + ErrZeroWeight = errors.New("validator weight must be non-zero") ) type ConvertSubnetTx struct { @@ -31,6 +40,8 @@ type ConvertSubnetTx struct { ChainID ids.ID `serialize:"true" json:"chainID"` // Address of the Subnet manager Address types.JSONByteSlice `serialize:"true" json:"address"` + // Initial pay-as-you-go validators for the Subnet + Validators []*ConvertSubnetValidator `serialize:"true" json:"validators"` // Authorizes this conversion SubnetAuth verify.Verifiable `serialize:"true" json:"subnetAuthorization"` } @@ -46,11 +57,20 @@ func (tx *ConvertSubnetTx) SyntacticVerify(ctx *snow.Context) error { return ErrConvertPermissionlessSubnet case len(tx.Address) > MaxSubnetAddressLength: return ErrAddressTooLong + case len(tx.Validators) == 0: + return ErrConvertMustIncludeValidators + case !utils.IsSortedAndUnique(tx.Validators): + return ErrConvertValidatorsNotSortedAndUnique } if err := tx.BaseTx.SyntacticVerify(ctx); err != nil { return err } + for _, vdr := range tx.Validators { + if err := vdr.Verify(); err != nil { + return err + } + } if err := tx.SubnetAuth.Verify(); err != nil { return err } @@ -62,3 +82,50 @@ func (tx *ConvertSubnetTx) SyntacticVerify(ctx *snow.Context) error { func (tx *ConvertSubnetTx) Visit(visitor Visitor) error { return visitor.ConvertSubnetTx(tx) } + +type ConvertSubnetValidator struct { + // NodeID of this validator + NodeID types.JSONByteSlice `serialize:"true" json:"nodeID"` + // Weight of this validator used when sampling + Weight uint64 `serialize:"true" json:"weight"` + // Initial balance for this validator + Balance uint64 `serialize:"true" json:"balance"` + // [Signer] is the BLS key for this validator. + // Note: We do not enforce that the BLS key is unique across all validators. + // This means that validators can share a key if they so choose. + // However, a NodeID + Subnet does uniquely map to a BLS key + Signer signer.ProofOfPossession `serialize:"true" json:"signer"` + // Leftover $AVAX from the [Balance] will be issued to this owner once it is + // removed from the validator set. + RemainingBalanceOwner message.PChainOwner `serialize:"true" json:"remainingBalanceOwner"` + // This owner has the authority to manually deactivate this validator. + DeactivationOwner message.PChainOwner `serialize:"true" json:"deactivationOwner"` +} + +func (v *ConvertSubnetValidator) Compare(o *ConvertSubnetValidator) int { + return bytes.Compare(v.NodeID, o.NodeID) +} + +func (v *ConvertSubnetValidator) Verify() error { + if v.Weight == 0 { + return ErrZeroWeight + } + nodeID, err := ids.ToNodeID(v.NodeID) + if err != nil { + return err + } + if nodeID == ids.EmptyNodeID { + return errEmptyNodeID + } + return verify.All( + &v.Signer, + &secp256k1fx.OutputOwners{ + Threshold: v.RemainingBalanceOwner.Threshold, + Addrs: v.RemainingBalanceOwner.Addresses, + }, + &secp256k1fx.OutputOwners{ + Threshold: v.DeactivationOwner.Threshold, + Addrs: v.DeactivationOwner.Addresses, + }, + ) +} diff --git a/vms/platformvm/txs/convert_subnet_tx_test.go b/vms/platformvm/txs/convert_subnet_tx_test.go index 42a392f0181e..d2e91acae0af 100644 --- a/vms/platformvm/txs/convert_subnet_tx_test.go +++ b/vms/platformvm/txs/convert_subnet_tx_test.go @@ -4,6 +4,7 @@ package txs import ( + "encoding/hex" "encoding/json" "strings" "testing" @@ -14,10 +15,15 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/vms/types" ) @@ -30,6 +36,11 @@ var ( ) func TestConvertSubnetTxSerialization(t *testing.T) { + skBytes, err := hex.DecodeString("6668fecd4595b81e4d568398c820bbf3f073cb222902279fa55ebb84764ed2e3") + require.NoError(t, err) + sk, err := bls.SecretKeyFromBytes(skBytes) + require.NoError(t, err) + var ( addr = ids.ShortID{ 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, @@ -71,6 +82,11 @@ func TestConvertSubnetTxSerialization(t *testing.T) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, } + nodeID = ids.BuildTestNodeID([]byte{ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x11, 0x22, 0x33, 0x44, + }) ) tests := []struct { @@ -107,9 +123,10 @@ func TestConvertSubnetTxSerialization(t *testing.T) { Memo: types.JSONByteSlice{}, }, }, - Subnet: subnetID, - ChainID: managerChainID, - Address: managerAddress, + Subnet: subnetID, + ChainID: managerChainID, + Address: managerAddress, + Validators: []*ConvertSubnetValidator{}, SubnetAuth: &secp256k1fx.Input{ SigIndices: []uint32{3}, }, @@ -169,6 +186,8 @@ func TestConvertSubnetTxSerialization(t *testing.T) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, + // number of validators + 0x00, 0x00, 0x00, 0x00, // secp256k1fx authorization type ID 0x00, 0x00, 0x00, 0x0a, // number of signatures needed in authorization @@ -277,6 +296,26 @@ func TestConvertSubnetTxSerialization(t *testing.T) { Subnet: subnetID, ChainID: managerChainID, Address: managerAddress, + Validators: []*ConvertSubnetValidator{ + { + NodeID: nodeID[:], + Weight: 0x0102030405060708, + Balance: units.Avax, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + addr, + }, + }, + DeactivationOwner: message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + addr, + }, + }, + }, + }, SubnetAuth: &secp256k1fx.Input{ SigIndices: []uint32{}, }, @@ -430,6 +469,55 @@ func TestConvertSubnetTxSerialization(t *testing.T) { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, + // number of validators + 0x00, 0x00, 0x00, 0x01, + // Validators[0] + // node ID length + 0x00, 0x00, 0x00, 0x14, + // node ID + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x11, 0x22, 0x33, 0x44, + // weight + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + // balance + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // BLS compressed public key + 0xaf, 0xf4, 0xac, 0xb4, 0xc5, 0x43, 0x9b, 0x5d, + 0x42, 0x6c, 0xad, 0xf9, 0xe9, 0x46, 0xd3, 0xa4, + 0x52, 0xf7, 0xde, 0x34, 0x14, 0xd1, 0xad, 0x27, + 0x33, 0x61, 0x33, 0x21, 0x1d, 0x8b, 0x90, 0xcf, + 0x49, 0xfb, 0x97, 0xee, 0xbc, 0xde, 0xee, 0xf7, + 0x14, 0xdc, 0x20, 0xf5, 0x4e, 0xd0, 0xd4, 0xd1, + // BLS compressed signature + 0x8c, 0xfd, 0x79, 0x09, 0xd1, 0x53, 0xb9, 0x60, + 0x4b, 0x62, 0xb1, 0x43, 0xba, 0x36, 0x20, 0x7b, + 0xb7, 0xe6, 0x48, 0x67, 0x42, 0x44, 0x80, 0x20, + 0x2a, 0x67, 0xdc, 0x68, 0x76, 0x83, 0x46, 0xd9, + 0x5c, 0x90, 0x98, 0x3c, 0x2d, 0x27, 0x9c, 0x64, + 0xc4, 0x3c, 0x51, 0x13, 0x6b, 0x2a, 0x05, 0xe0, + 0x16, 0x02, 0xd5, 0x2a, 0xa6, 0x37, 0x6f, 0xda, + 0x17, 0xfa, 0x6e, 0x2a, 0x18, 0xa0, 0x83, 0xe4, + 0x9d, 0x9c, 0x45, 0x0e, 0xab, 0x7b, 0x89, 0xb1, + 0xd5, 0x55, 0x5d, 0xa5, 0xc4, 0x89, 0x87, 0x2e, + 0x02, 0xb7, 0xe5, 0x22, 0x7b, 0x77, 0x55, 0x0a, + 0xf1, 0x33, 0x0e, 0x5a, 0x71, 0xf8, 0xc3, 0x68, + // RemainingBalanceOwner threshold + 0x00, 0x00, 0x00, 0x01, + // RemainingBalanceOwner number of addresses + 0x00, 0x00, 0x00, 0x01, + // RemainingBalanceOwner Addrs[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + // DeactivationOwner threshold + 0x00, 0x00, 0x00, 0x01, + // DeactivationOwner number of addresses + 0x00, 0x00, 0x00, 0x01, + // DeactivationOwner Addrs[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, // secp256k1fx authorization type ID 0x00, 0x00, 0x00, 0x0a, // number of signatures needed in authorization @@ -462,6 +550,9 @@ func TestConvertSubnetTxSerialization(t *testing.T) { } func TestConvertSubnetTxSyntacticVerify(t *testing.T) { + sk, err := bls.NewSecretKey() + require.NoError(t, err) + var ( ctx = snowtest.Context(t, ids.GenerateTestID()) validBaseTx = BaseTx{ @@ -470,8 +561,18 @@ func TestConvertSubnetTxSyntacticVerify(t *testing.T) { BlockchainID: ctx.ChainID, }, } - validSubnetID = ids.GenerateTestID() - invalidAddress = make(types.JSONByteSlice, MaxSubnetAddressLength+1) + validSubnetID = ids.GenerateTestID() + invalidAddress = make(types.JSONByteSlice, MaxSubnetAddressLength+1) + validValidators = []*ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen), + Weight: 1, + Balance: 1, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + } validSubnetAuth = &secp256k1fx.Input{} invalidSubnetAuth = &secp256k1fx.Input{ SigIndices: []uint32{1, 0}, @@ -498,6 +599,7 @@ func TestConvertSubnetTxSyntacticVerify(t *testing.T) { }, Subnet: constants.PrimaryNetworkID, Address: invalidAddress, + Validators: nil, SubnetAuth: invalidSubnetAuth, }, expectedErr: nil, @@ -507,6 +609,7 @@ func TestConvertSubnetTxSyntacticVerify(t *testing.T) { tx: &ConvertSubnetTx{ BaseTx: validBaseTx, Subnet: constants.PrimaryNetworkID, + Validators: validValidators, SubnetAuth: validSubnetAuth, }, expectedErr: ErrConvertPermissionlessSubnet, @@ -517,15 +620,164 @@ func TestConvertSubnetTxSyntacticVerify(t *testing.T) { BaseTx: validBaseTx, Subnet: validSubnetID, Address: invalidAddress, + Validators: validValidators, SubnetAuth: validSubnetAuth, }, expectedErr: ErrAddressTooLong, }, + { + name: "invalid number of validators", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: nil, + SubnetAuth: validSubnetAuth, + }, + expectedErr: ErrConvertMustIncludeValidators, + }, + { + name: "invalid validator order", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: []byte{ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }, + }, + { + NodeID: []byte{ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: ErrConvertValidatorsNotSortedAndUnique, + }, + { + name: "invalid validator weight", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen), + Weight: 0, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: ErrZeroWeight, + }, + { + name: "invalid validator nodeID length", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen + 1), + Weight: 1, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: hashing.ErrInvalidHashLen, + }, + { + name: "invalid validator nodeID", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: ids.EmptyNodeID[:], + Weight: 1, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: errEmptyNodeID, + }, + { + name: "invalid validator pop", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen), + Weight: 1, + Signer: signer.ProofOfPossession{}, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: bls.ErrFailedPublicKeyDecompress, + }, + { + name: "invalid validator remaining balance owner", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen), + Weight: 1, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 1, + }, + DeactivationOwner: message.PChainOwner{}, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: secp256k1fx.ErrOutputUnspendable, + }, + { + name: "invalid validator deactivation owner", + tx: &ConvertSubnetTx{ + BaseTx: validBaseTx, + Subnet: validSubnetID, + Validators: []*ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen), + Weight: 1, + Signer: *signer.NewProofOfPossession(sk), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{ + Threshold: 1, + }, + }, + }, + SubnetAuth: validSubnetAuth, + }, + expectedErr: secp256k1fx.ErrOutputUnspendable, + }, { name: "invalid BaseTx", tx: &ConvertSubnetTx{ BaseTx: BaseTx{}, Subnet: validSubnetID, + Validators: validValidators, SubnetAuth: validSubnetAuth, }, expectedErr: avax.ErrWrongNetworkID, @@ -535,6 +787,7 @@ func TestConvertSubnetTxSyntacticVerify(t *testing.T) { tx: &ConvertSubnetTx{ BaseTx: validBaseTx, Subnet: validSubnetID, + Validators: validValidators, SubnetAuth: invalidSubnetAuth, }, expectedErr: secp256k1fx.ErrInputIndicesNotSortedUnique, @@ -544,6 +797,7 @@ func TestConvertSubnetTxSyntacticVerify(t *testing.T) { tx: &ConvertSubnetTx{ BaseTx: validBaseTx, Subnet: validSubnetID, + Validators: validValidators, SubnetAuth: validSubnetAuth, }, expectedErr: nil, diff --git a/vms/platformvm/txs/convert_subnet_tx_test_complex.json b/vms/platformvm/txs/convert_subnet_tx_test_complex.json index b5178db2f9cb..926c475fec31 100644 --- a/vms/platformvm/txs/convert_subnet_tx_test_complex.json +++ b/vms/platformvm/txs/convert_subnet_tx_test_complex.json @@ -75,6 +75,29 @@ "subnetID": "SkB92YpWm4UpburLz9tEKZw2i67H3FF6YkjaU4BkFUDTG9Xm", "chainID": "NfebWJbJMmUpduqFCF8i1m5pstbVYLP1gGHbacrevXZMhpVMy", "address": "0x000000000000000000000000000000000000dead", + "validators": [ + { + "nodeID": "0x1122334455667788112233445566778811223344", + "weight": 72623859790382856, + "balance": 1000000000, + "signer": { + "publicKey": "0xaff4acb4c5439b5d426cadf9e946d3a452f7de3414d1ad27336133211d8b90cf49fb97eebcdeeef714dc20f54ed0d4d1", + "proofOfPossession": "0x8cfd7909d153b9604b62b143ba36207bb7e64867424480202a67dc68768346d95c90983c2d279c64c43c51136b2a05e01602d52aa6376fda17fa6e2a18a083e49d9c450eab7b89b1d5555da5c489872e02b7e5227b77550af1330e5a71f8c368" + }, + "remainingBalanceOwner": { + "threshold": 1, + "addresses": [ + "7EKFm18KvWqcxMCNgpBSN51pJnEr1cVUb" + ] + }, + "deactivationOwner": { + "threshold": 1, + "addresses": [ + "7EKFm18KvWqcxMCNgpBSN51pJnEr1cVUb" + ] + } + } + ], "subnetAuthorization": { "signatureIndices": [] } diff --git a/vms/platformvm/txs/convert_subnet_tx_test_simple.json b/vms/platformvm/txs/convert_subnet_tx_test_simple.json index 8463a748141f..5d6848a53f23 100644 --- a/vms/platformvm/txs/convert_subnet_tx_test_simple.json +++ b/vms/platformvm/txs/convert_subnet_tx_test_simple.json @@ -20,6 +20,7 @@ "subnetID": "SkB92YpWm4UpburLz9tEKZw2i67H3FF6YkjaU4BkFUDTG9Xm", "chainID": "NfebWJbJMmUpduqFCF8i1m5pstbVYLP1gGHbacrevXZMhpVMy", "address": "0x000000000000000000000000000000000000dead", + "validators": [], "subnetAuthorization": { "signatureIndices": [ 3 diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 1c08880a94e0..d30049cc92c3 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -14,12 +14,16 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "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/math" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" ) var ( @@ -30,6 +34,7 @@ var ( errMissingStartTimePreDurango = errors.New("staker transactions must have a StartTime pre-Durango") errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") + errMaxNumActiveValidators = errors.New("already at the max number of active validators") ) type StandardTxExecutor struct { @@ -522,6 +527,71 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { if err != nil { return err } + + var ( + startTime = uint64(currentTimestamp.Unix()) + currentFees = e.State.GetAccruedFees() + subnetConversionData = message.SubnetConversionData{ + SubnetID: tx.Subnet, + ManagerChainID: tx.ChainID, + ManagerAddress: tx.Address, + Validators: make([]message.SubnetConversionValidatorData, len(tx.Validators)), + } + ) + for i, vdr := range tx.Validators { + nodeID, err := ids.ToNodeID(vdr.NodeID) + if err != nil { + return err + } + + remainingBalanceOwner, err := txs.Codec.Marshal(txs.CodecVersion, &vdr.RemainingBalanceOwner) + if err != nil { + return err + } + deactivationOwner, err := txs.Codec.Marshal(txs.CodecVersion, &vdr.DeactivationOwner) + if err != nil { + return err + } + + sov := state.SubnetOnlyValidator{ + ValidationID: tx.Subnet.Append(uint32(i)), + SubnetID: tx.Subnet, + NodeID: nodeID, + PublicKey: bls.PublicKeyToUncompressedBytes(vdr.Signer.Key()), + RemainingBalanceOwner: remainingBalanceOwner, + DeactivationOwner: deactivationOwner, + StartTime: startTime, + Weight: vdr.Weight, + MinNonce: 0, + EndAccumulatedFee: 0, // If Balance is 0, this is 0 + } + if vdr.Balance != 0 { + // We are attempting to add an active validator + if gas.Gas(e.State.NumActiveSubnetOnlyValidators()) >= e.Backend.Config.ValidatorFeeConfig.Capacity { + return errMaxNumActiveValidators + } + + sov.EndAccumulatedFee, err = math.Add(vdr.Balance, currentFees) + if err != nil { + return err + } + + fee, err = math.Add(fee, vdr.Balance) + if err != nil { + return err + } + } + + if err := e.State.PutSubnetOnlyValidator(sov); err != nil { + return err + } + + subnetConversionData.Validators[i] = message.SubnetConversionValidatorData{ + NodeID: vdr.NodeID, + BLSPublicKey: vdr.Signer.PublicKey, + Weight: vdr.Weight, + } + } if err := e.Backend.FlowChecker.VerifySpend( tx, e.State, @@ -535,6 +605,11 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return err } + conversionID, err := message.SubnetConversionID(subnetConversionData) + if err != nil { + return err + } + txID := e.Tx.ID() // Consume the UTXOS @@ -545,8 +620,7 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { e.State.SetSubnetConversion( tx.Subnet, state.SubnetConversion{ - // TODO: Populate the conversionID - ConversionID: ids.Empty, + ConversionID: conversionID, ChainID: tx.ChainID, Addr: tx.Address, }, diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 43a603ac66e1..1080013ed776 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -37,12 +37,15 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/state/statetest" "github.com/ava-labs/avalanchego/vms/platformvm/status" "github.com/ava-labs/avalanchego/vms/platformvm/txs" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/platformvm/utxo/utxomock" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + + txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) // This tests that the math performed during TransformSubnetTx execution can @@ -2384,8 +2387,9 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { var ( ctx = snowtest.Context(t, constants.PlatformChainID) defaultConfig = &config.Config{ - DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, - UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), } baseState = statetest.New(t, statetest.Config{ Upgrades: defaultConfig.UpgradeConfig, @@ -2430,26 +2434,31 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { require.NoError(t, diff.Apply(baseState)) require.NoError(t, baseState.Commit()) - subnetID := createSubnetTx.ID() + var ( + subnetID = createSubnetTx.ID() + nodeID = ids.GenerateTestNodeID() + ) tests := []struct { name string builderOptions []common.Option - updateExecutor func(executor *StandardTxExecutor) + updateExecutor func(executor *StandardTxExecutor) error expectedErr error }{ { name: "invalid prior to E-Upgrade", - updateExecutor: func(e *StandardTxExecutor) { + updateExecutor: func(e *StandardTxExecutor) error { e.Backend.Config = &config.Config{ UpgradeConfig: upgradetest.GetConfig(upgradetest.Durango), } + return nil }, expectedErr: errEtnaUpgradeNotActive, }, { name: "tx fails syntactic verification", - updateExecutor: func(e *StandardTxExecutor) { + updateExecutor: func(e *StandardTxExecutor) error { e.Backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) + return nil }, expectedErr: avax.ErrWrongChainID, }, @@ -2462,28 +2471,30 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "fail subnet authorization", - updateExecutor: func(e *StandardTxExecutor) { + updateExecutor: func(e *StandardTxExecutor) error { e.State.SetSubnetOwner(subnetID, &secp256k1fx.OutputOwners{ Threshold: 1, Addrs: []ids.ShortID{ ids.GenerateTestShortID(), }, }) + return nil }, expectedErr: errUnauthorizedSubnetModification, }, { name: "invalid if subnet is transformed", - updateExecutor: func(e *StandardTxExecutor) { + updateExecutor: func(e *StandardTxExecutor) error { e.State.AddSubnetTransformation(&txs.Tx{Unsigned: &txs.TransformSubnetTx{ Subnet: subnetID, }}) + return nil }, expectedErr: errIsImmutable, }, { name: "invalid if subnet is converted", - updateExecutor: func(e *StandardTxExecutor) { + updateExecutor: func(e *StandardTxExecutor) error { e.State.SetSubnetConversion( subnetID, state.SubnetConversion{ @@ -2492,23 +2503,55 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { Addr: utils.RandomBytes(32), }, ) + return nil }, expectedErr: errIsImmutable, }, { name: "invalid fee calculation", - updateExecutor: func(e *StandardTxExecutor) { - e.FeeCalculator = fee.NewStaticCalculator(e.Config.StaticFeeConfig) + updateExecutor: func(e *StandardTxExecutor) error { + e.FeeCalculator = txfee.NewStaticCalculator(e.Config.StaticFeeConfig) + return nil + }, + expectedErr: txfee.ErrUnsupportedTx, + }, + { + name: "too many active validators", + updateExecutor: func(e *StandardTxExecutor) error { + e.Backend.Config = &config.Config{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: validatorfee.Config{ + Capacity: 0, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + return nil }, - expectedErr: fee.ErrUnsupportedTx, + expectedErr: errMaxNumActiveValidators, + }, + { + name: "invalid subnet only validator", + updateExecutor: func(e *StandardTxExecutor) error { + return e.State.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: subnetID, + NodeID: nodeID, + Weight: 1, + }) + }, + expectedErr: state.ErrDuplicateSubnetOnlyValidator, }, { name: "insufficient fee", - updateExecutor: func(e *StandardTxExecutor) { - e.FeeCalculator = fee.NewDynamicCalculator( + updateExecutor: func(e *StandardTxExecutor) error { + e.FeeCalculator = txfee.NewDynamicCalculator( e.Config.DynamicFeeConfig.Weights, 100*genesis.LocalParams.DynamicFeeConfig.MinPrice, ) + return nil }, expectedErr: utxo.ErrInsufficientUnlockedFunds, }, @@ -2520,7 +2563,14 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { t.Run(test.name, func(t *testing.T) { require := require.New(t) + sk, err := bls.NewSecretKey() + require.NoError(err) + // Create the ConvertSubnetTx + const ( + weight = 1 + balance = 1 + ) var ( wallet = txstest.NewWallet( t, @@ -2531,13 +2581,25 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { []ids.ID{subnetID}, nil, // chainIDs ) - chainID = ids.GenerateTestID() - address = utils.RandomBytes(32) + chainID = ids.GenerateTestID() + address = utils.RandomBytes(32) + pop = signer.NewProofOfPossession(sk) + validator = &txs.ConvertSubnetValidator{ + NodeID: nodeID.Bytes(), + Weight: weight, + Balance: balance, + Signer: *pop, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + } ) convertSubnetTx, err := wallet.IssueConvertSubnetTx( subnetID, chainID, address, + []*txs.ConvertSubnetValidator{ + validator, + }, test.builderOptions..., ) require.NoError(err) @@ -2558,7 +2620,7 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { State: diff, } if test.updateExecutor != nil { - test.updateExecutor(executor) + require.NoError(test.updateExecutor(executor)) } err = convertSubnetTx.Unsigned.Visit(executor) @@ -2579,17 +2641,58 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { require.Equal(expectedUTXO, utxo) } + expectedConversionID, err := message.SubnetConversionID(message.SubnetConversionData{ + SubnetID: subnetID, + ManagerChainID: chainID, + ManagerAddress: address, + Validators: []message.SubnetConversionValidatorData{ + { + NodeID: nodeID.Bytes(), + BLSPublicKey: pop.PublicKey, + Weight: weight, + }, + }, + }) + require.NoError(err) + stateConversion, err := diff.GetSubnetConversion(subnetID) require.NoError(err) require.Equal( state.SubnetConversion{ - // TODO: Specify the correct conversionID - ConversionID: ids.Empty, + ConversionID: expectedConversionID, ChainID: chainID, Addr: address, }, stateConversion, ) + + var ( + validationID = subnetID.Append(0) + pkBytes = bls.PublicKeyToUncompressedBytes(bls.PublicFromSecretKey(sk)) + ) + remainingBalanceOwner, err := txs.Codec.Marshal(txs.CodecVersion, &validator.RemainingBalanceOwner) + require.NoError(err) + + deactivationOwner, err := txs.Codec.Marshal(txs.CodecVersion, &validator.DeactivationOwner) + require.NoError(err) + + sov, err := diff.GetSubnetOnlyValidator(validationID) + require.NoError(err) + require.Equal( + state.SubnetOnlyValidator{ + ValidationID: validationID, + SubnetID: subnetID, + NodeID: nodeID, + PublicKey: pkBytes, + RemainingBalanceOwner: remainingBalanceOwner, + DeactivationOwner: deactivationOwner, + StartTime: uint64(diff.GetTimestamp().Unix()), + Weight: validator.Weight, + MinNonce: 0, + EndAccumulatedFee: validator.Balance + diff.GetAccruedFees(), + }, + sov, + ) }) } } diff --git a/vms/platformvm/txs/fee/calculator_test.go b/vms/platformvm/txs/fee/calculator_test.go index 172f927e9796..5039dc557345 100644 --- a/vms/platformvm/txs/fee/calculator_test.go +++ b/vms/platformvm/txs/fee/calculator_test.go @@ -221,15 +221,15 @@ var ( }, { name: "ConvertSubnetTx", - tx: "00000000002300015b380000000000000000000000000000000000000000000000000000000000000000000000012a16b813b6a4a64d8e9b3f11460b782fcc319364bc038915af56834b72043ce80000000700470de4d97cdcc00000000000000000000000010000000180fa21568b6a2ef338a773ba18bfc0cb493af926000000018d65db2676f4733a7d263ad14606ddbc2f1996bb2998358f4b6f1e01297d1da5000000002a16b813b6a4a64d8e9b3f11460b782fcc319364bc038915af56834b72043ce80000000500470de4d98c1f000000000100000000000000008d65db2676f4733a7d263ad14606ddbc2f1996bb2998358f4b6f1e01297d1da55fa29ed4356903dac2364713c60f57d8472c7dda4a5e08d88a88ad8ea71aed6000000007616464726573730000000a0000000100000000000000020000000900000001c990ecf3f39646c4c90cb1f5cc2a9a98c33df1a9a41a084e7f3e7b2afe10fd853068a20ad4ddf83c087b6311ab0fdab339ca529f57cda3329ca31b142987c223000000000900000001c990ecf3f39646c4c90cb1f5cc2a9a98c33df1a9a41a084e7f3e7b2afe10fd853068a20ad4ddf83c087b6311ab0fdab339ca529f57cda3329ca31b142987c22300", + tx: "00000000002300003039000000000000000000000000000000000000000000000000000000000000000000000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007002386f234262960000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001705f3d4415f990225d3df5ce437d7af2aa324b1bbce854ee34ab6f39882250d200000000dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000005002386f26fc0f94e000000010000000000000000a0673b4ee5ec44e57c8ab250dd7cd7b68d04421f64bd6559a4284a3ee358ff2b705f3d4415f990225d3df5ce437d7af2aa324b1bbce854ee34ab6f39882250d2000000000000000100000014c582872c37c81efa2c94ea347af49cdc23a830aa000000000000c137000000003b9aca00a3783a891cb41cadbfcf456da149f30e7af972677a162b984bef0779f254baac51ec042df1781d1295df80fb41c801269731fc6c25e1e5940dc3cb8509e30348fa712742cfdc83678acc9f95908eb98b89b28802fb559b4a2a6ff3216707c07f0ceb0b45a95f4f9a9540bbd3331d8ab4f233bffa4abb97fad9d59a1695f31b92a2b89e365facf7ab8c30de7c4a496d1e000000000000000000000000000000000000000a00000001000000000000000200000009000000011430759900fdf516cdeff6a1390dd7438585568a89c06142c44b3bf1178c4cae4bff44e955b19da08f0359d396a7a738b989bb46377e7465cd858ddd1e8dd3790100000009000000011430759900fdf516cdeff6a1390dd7438585568a89c06142c44b3bf1178c4cae4bff44e955b19da08f0359d396a7a738b989bb46377e7465cd858ddd1e8dd37901", expectedStaticFeeErr: ErrUnsupportedTx, expectedComplexity: gas.Dimensions{ - gas.Bandwidth: 459, // The length of the tx in bytes + gas.Bandwidth: 656, // The length of the tx in bytes gas.DBRead: IntrinsicConvertSubnetTxComplexities[gas.DBRead] + intrinsicInputDBRead, - gas.DBWrite: IntrinsicConvertSubnetTxComplexities[gas.DBWrite] + intrinsicInputDBWrite + intrinsicOutputDBWrite, + gas.DBWrite: IntrinsicConvertSubnetTxComplexities[gas.DBWrite] + intrinsicInputDBWrite + intrinsicOutputDBWrite + intrinsicConvertSubnetValidatorDBWrite, gas.Compute: 0, // TODO: implement }, - expectedDynamicFee: 175_900, + expectedDynamicFee: 365_600, }, } ) diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 02942e09b76f..5683ac6b3b6a 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -62,13 +62,22 @@ const ( intrinsicSECP256k1FxSignatureBandwidth = wrappers.IntLen + // signature index secp256k1.SignatureLen // signature length + intrinsicConvertSubnetValidatorBandwidth = wrappers.IntLen + // nodeID length + wrappers.LongLen + // weight + wrappers.LongLen + // balance + wrappers.IntLen + // remaining balance owner threshold + wrappers.IntLen + // remaining balance owner num addresses + wrappers.IntLen + // deactivation owner threshold + wrappers.IntLen // deactivation owner num addresses + intrinsicPoPBandwidth = bls.PublicKeyLen + // public key bls.SignatureLen // signature intrinsicInputDBRead = 1 - intrinsicInputDBWrite = 1 - intrinsicOutputDBWrite = 1 + intrinsicInputDBWrite = 1 + intrinsicOutputDBWrite = 1 + intrinsicConvertSubnetValidatorDBWrite = 4 // weight diff + pub key diff + subnetID/nodeID + validationID ) var ( @@ -180,10 +189,11 @@ var ( ids.IDLen + // subnetID ids.IDLen + // chainID wrappers.IntLen + // address length + wrappers.IntLen + // validators length wrappers.IntLen + // subnetAuth typeID wrappers.IntLen, // subnetAuthCredential typeID - gas.DBRead: 1, - gas.DBWrite: 1, + gas.DBRead: 2, // subnet auth + manager lookup + gas.DBWrite: 2, // manager + weight gas.Compute: 0, } @@ -305,6 +315,53 @@ func inputComplexity(in *avax.TransferableInput) (gas.Dimensions, error) { return complexity, err } +// ConvertSubnetValidatorComplexity returns the complexity the validators add to +// a transaction. +func ConvertSubnetValidatorComplexity(sovs ...*txs.ConvertSubnetValidator) (gas.Dimensions, error) { + var complexity gas.Dimensions + for _, sov := range sovs { + sovComplexity, err := convertSubnetValidatorComplexity(sov) + if err != nil { + return gas.Dimensions{}, err + } + + complexity, err = complexity.Add(&sovComplexity) + if err != nil { + return gas.Dimensions{}, err + } + } + return complexity, nil +} + +func convertSubnetValidatorComplexity(sov *txs.ConvertSubnetValidator) (gas.Dimensions, error) { + complexity := gas.Dimensions{ + gas.Bandwidth: intrinsicConvertSubnetValidatorBandwidth, + gas.DBRead: 0, + gas.DBWrite: intrinsicConvertSubnetValidatorDBWrite, + gas.Compute: 0, // TODO: Add compute complexity + } + + signerComplexity, err := SignerComplexity(&sov.Signer) + if err != nil { + return gas.Dimensions{}, err + } + + numAddresses := uint64(len(sov.RemainingBalanceOwner.Addresses) + len(sov.DeactivationOwner.Addresses)) + addressBandwidth, err := math.Mul(numAddresses, ids.ShortIDLen) + if err != nil { + return gas.Dimensions{}, err + } + return complexity.Add( + &gas.Dimensions{ + gas.Bandwidth: uint64(len(sov.NodeID)), + }, + &signerComplexity, + &gas.Dimensions{ + gas.Bandwidth: addressBandwidth, + }, + ) +} + // OwnerComplexity returns the complexity an owner adds to a transaction. // It does not include the typeID of the owner. func OwnerComplexity(ownerIntf fx.Owner) (gas.Dimensions, error) { @@ -610,12 +667,17 @@ func (c *complexityVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { if err != nil { return err } + validatorComplexity, err := ConvertSubnetValidatorComplexity(tx.Validators...) + if err != nil { + return err + } authComplexity, err := AuthComplexity(tx.SubnetAuth) if err != nil { return err } c.output, err = IntrinsicConvertSubnetTxComplexities.Add( &baseTxComplexity, + &validatorComplexity, &authComplexity, &gas.Dimensions{ gas.Bandwidth: uint64(len(tx.Address)), diff --git a/vms/platformvm/txs/fee/complexity_test.go b/vms/platformvm/txs/fee/complexity_test.go index 0dd9ba90b3f0..d0767317df3b 100644 --- a/vms/platformvm/txs/fee/complexity_test.go +++ b/vms/platformvm/txs/fee/complexity_test.go @@ -20,6 +20,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/secp256k1fx" ) @@ -367,6 +368,85 @@ func TestInputComplexity(t *testing.T) { } } +func TestConvertSubnetValidatorComplexity(t *testing.T) { + tests := []struct { + name string + vdr txs.ConvertSubnetValidator + expected gas.Dimensions + }{ + { + name: "any can spend", + vdr: txs.ConvertSubnetValidator{ + NodeID: make([]byte, ids.NodeIDLen), + Signer: signer.ProofOfPossession{}, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + expected: gas.Dimensions{ + gas.Bandwidth: 200, + gas.DBRead: 0, + gas.DBWrite: 4, + gas.Compute: 0, // TODO: implement + }, + }, + { + name: "single remaining balance owner", + vdr: txs.ConvertSubnetValidator{ + NodeID: make([]byte, ids.NodeIDLen), + Signer: signer.ProofOfPossession{}, + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + DeactivationOwner: message.PChainOwner{}, + }, + expected: gas.Dimensions{ + gas.Bandwidth: 220, + gas.DBRead: 0, + gas.DBWrite: 4, + gas.Compute: 0, // TODO: implement + }, + }, + { + name: "single deactivation owner", + vdr: txs.ConvertSubnetValidator{ + NodeID: make([]byte, ids.NodeIDLen), + Signer: signer.ProofOfPossession{}, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + }, + expected: gas.Dimensions{ + gas.Bandwidth: 220, + gas.DBRead: 0, + gas.DBWrite: 4, + gas.Compute: 0, // TODO: implement + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + actual, err := ConvertSubnetValidatorComplexity(&test.vdr) + require.NoError(err) + require.Equal(test.expected, actual) + + vdrBytes, err := txs.Codec.Marshal(txs.CodecVersion, test.vdr) + require.NoError(err) + + numBytesWithoutCodecVersion := uint64(len(vdrBytes) - codec.VersionSize) + require.Equal(numBytesWithoutCodecVersion, actual[gas.Bandwidth]) + }) + } +} + func TestOwnerComplexity(t *testing.T) { tests := []struct { name string diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index 5a0b063280fd..e8762d7b2a68 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -155,10 +155,12 @@ type Builder interface { // - [subnetID] specifies the subnet to be converted // - [chainID] specifies which chain the manager is deployed on // - [address] specifies the address of the manager + // - [validators] specifies the initial SoVs of the L1 NewConvertSubnetTx( subnetID ids.ID, chainID ids.ID, address []byte, + validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.ConvertSubnetTx, error) @@ -782,12 +784,25 @@ func (b *builder) NewConvertSubnetTx( subnetID ids.ID, chainID ids.ID, address []byte, + validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.ConvertSubnetTx, error) { - toBurn := map[ids.ID]uint64{} - toStake := map[ids.ID]uint64{} + var avaxToBurn uint64 + for _, vdr := range validators { + var err error + avaxToBurn, err = math.Add(avaxToBurn, vdr.Balance) + if err != nil { + return nil, err + } + } - ops := common.NewOptions(options) + var ( + toBurn = map[ids.ID]uint64{ + b.context.AVAXAssetID: avaxToBurn, + } + toStake = map[ids.ID]uint64{} + ops = common.NewOptions(options) + ) subnetAuth, err := b.authorizeSubnet(subnetID, ops) if err != nil { return nil, err @@ -801,12 +816,17 @@ func (b *builder) NewConvertSubnetTx( bytesComplexity := gas.Dimensions{ gas.Bandwidth: additionalBytes, } + validatorComplexity, err := fee.ConvertSubnetValidatorComplexity(validators...) + if err != nil { + return nil, err + } authComplexity, err := fee.AuthComplexity(subnetAuth) if err != nil { return nil, err } complexity, err := fee.IntrinsicConvertSubnetTxComplexities.Add( &bytesComplexity, + &validatorComplexity, &authComplexity, ) if err != nil { @@ -825,6 +845,7 @@ func (b *builder) NewConvertSubnetTx( return nil, err } + utils.Sort(validators) tx := &txs.ConvertSubnetTx{ BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ NetworkID: b.context.NetworkID, @@ -836,6 +857,7 @@ func (b *builder) NewConvertSubnetTx( Subnet: subnetID, ChainID: chainID, Address: address, + Validators: validators, SubnetAuth: subnetAuth, } return tx, b.initCtx(tx) diff --git a/wallet/chain/p/builder/builder_with_options.go b/wallet/chain/p/builder/builder_with_options.go index 4a473cf50bac..8432944e3542 100644 --- a/wallet/chain/p/builder/builder_with_options.go +++ b/wallet/chain/p/builder/builder_with_options.go @@ -159,12 +159,14 @@ func (b *builderWithOptions) NewConvertSubnetTx( subnetID ids.ID, chainID ids.ID, address []byte, + validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.ConvertSubnetTx, error) { return b.builder.NewConvertSubnetTx( subnetID, chainID, address, + validators, common.UnionOptions(b.options, options)..., ) } diff --git a/wallet/chain/p/builder_test.go b/wallet/chain/p/builder_test.go index 26ebfac36238..f56a0eb0896b 100644 --- a/wallet/chain/p/builder_test.go +++ b/wallet/chain/p/builder_test.go @@ -4,6 +4,7 @@ package p import ( + "math/rand" "slices" "testing" "time" @@ -25,6 +26,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/vms/types" "github.com/ava-labs/avalanchego/wallet/chain/p/builder" @@ -668,9 +670,42 @@ func TestAddPermissionlessDelegatorTx(t *testing.T) { } func TestConvertSubnetTx(t *testing.T) { + sk0, err := bls.NewSecretKey() + require.NoError(t, err) + sk1, err := bls.NewSecretKey() + require.NoError(t, err) + var ( - chainID = ids.GenerateTestID() - address = utils.RandomBytes(32) + chainID = ids.GenerateTestID() + address = utils.RandomBytes(32) + validators = []*txs.ConvertSubnetValidator{ + { + NodeID: utils.RandomBytes(ids.NodeIDLen), + Weight: rand.Uint64(), //#nosec G404 + Balance: units.Avax, + Signer: *signer.NewProofOfPossession(sk0), + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + DeactivationOwner: message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + }, + { + NodeID: utils.RandomBytes(ids.NodeIDLen), + Weight: rand.Uint64(), //#nosec G404 + Balance: 2 * units.Avax, + Signer: *signer.NewProofOfPossession(sk1), + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + } ) for _, e := range testEnvironmentPostEtna { t.Run(e.name, func(t *testing.T) { @@ -687,6 +722,7 @@ func TestConvertSubnetTx(t *testing.T) { subnetID, chainID, address, + validators, common.WithMemo(e.memo), ) require.NoError(err) @@ -694,6 +730,8 @@ func TestConvertSubnetTx(t *testing.T) { require.Equal(chainID, utx.ChainID) require.Equal(types.JSONByteSlice(address), utx.Address) require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + require.True(utils.IsSortedAndUnique(utx.Validators)) + require.Equal(validators, utx.Validators) requireFeeIsCorrect( require, e.feeCalculator, @@ -701,7 +739,9 @@ func TestConvertSubnetTx(t *testing.T) { &utx.BaseTx.BaseTx, nil, nil, - nil, + map[ids.ID]uint64{ + e.context.AVAXAssetID: 3 * units.Avax, // Balance of the validators + }, ) }) } diff --git a/wallet/chain/p/wallet/wallet.go b/wallet/chain/p/wallet/wallet.go index 4e2886d41e88..a36522cc6b3a 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -141,10 +141,12 @@ type Wallet interface { // - [subnetID] specifies the subnet to be converted // - [chainID] specifies which chain the manager is deployed on // - [address] specifies the address of the manager + // - [validators] specifies the initial SoVs of the L1 IssueConvertSubnetTx( subnetID ids.ID, chainID ids.ID, address []byte, + validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.Tx, error) @@ -392,9 +394,10 @@ func (w *wallet) IssueConvertSubnetTx( subnetID ids.ID, chainID ids.ID, address []byte, + validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.Tx, error) { - utx, err := w.builder.NewConvertSubnetTx(subnetID, chainID, address, options...) + utx, err := w.builder.NewConvertSubnetTx(subnetID, chainID, address, validators, options...) if err != nil { return nil, err } diff --git a/wallet/chain/p/wallet/with_options.go b/wallet/chain/p/wallet/with_options.go index 7082764aa18d..ae6b4b4d8fa9 100644 --- a/wallet/chain/p/wallet/with_options.go +++ b/wallet/chain/p/wallet/with_options.go @@ -147,12 +147,14 @@ func (w *withOptions) IssueConvertSubnetTx( subnetID ids.ID, chainID ids.ID, address []byte, + validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.Tx, error) { return w.wallet.IssueConvertSubnetTx( subnetID, chainID, address, + validators, common.UnionOptions(w.options, options)..., ) } diff --git a/wallet/subnet/primary/examples/convert-subnet/main.go b/wallet/subnet/primary/examples/convert-subnet/main.go new file mode 100644 index 000000000000..8602c26ffb64 --- /dev/null +++ b/wallet/subnet/primary/examples/convert-subnet/main.go @@ -0,0 +1,106 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/hex" + "log" + "time" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +func main() { + key := genesis.EWOQKey + uri := primary.LocalAPIURI + kc := secp256k1fx.NewKeychain(key) + subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") + chainID := ids.FromStringOrPanic("E8nTR9TtRwfkS7XFjTYUYHENQ91mkPMtDUwwCeu7rNgBBtkqu") + addressHex := "" + weight := units.Schmeckle + + address, err := hex.DecodeString(addressHex) + if err != nil { + log.Fatalf("failed to decode address %q: %s\n", addressHex, err) + } + + ctx := context.Background() + infoClient := info.NewClient(uri) + + nodeInfoStartTime := time.Now() + nodeID, nodePoP, err := infoClient.GetNodeID(ctx) + if err != nil { + log.Fatalf("failed to fetch node IDs: %s\n", err) + } + log.Printf("fetched node ID %s in %s\n", nodeID, time.Since(nodeInfoStartTime)) + + validationID := subnetID.Append(0) + conversionID, err := message.SubnetConversionID(message.SubnetConversionData{ + SubnetID: subnetID, + ManagerChainID: chainID, + ManagerAddress: address, + Validators: []message.SubnetConversionValidatorData{ + { + NodeID: nodeID.Bytes(), + BLSPublicKey: nodePoP.PublicKey, + Weight: weight, + }, + }, + }) + if err != nil { + log.Fatalf("failed to calculate conversionID: %s\n", err) + } + + // MakeWallet fetches the available UTXOs owned by [kc] on the network that + // [uri] is hosting and registers [subnetID]. + walletSyncStartTime := time.Now() + wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{ + URI: uri, + AVAXKeychain: kc, + EthKeychain: kc, + SubnetIDs: []ids.ID{subnetID}, + }) + if err != nil { + log.Fatalf("failed to initialize wallet: %s\n", err) + } + log.Printf("synced wallet in %s\n", time.Since(walletSyncStartTime)) + + // Get the P-chain wallet + pWallet := wallet.P() + + convertSubnetStartTime := time.Now() + convertSubnetTx, err := pWallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + []*txs.ConvertSubnetValidator{ + { + NodeID: nodeID.Bytes(), + Weight: weight, + Balance: units.Avax, + Signer: *nodePoP, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + }, + }, + ) + if err != nil { + log.Fatalf("failed to issue subnet conversion transaction: %s\n", err) + } + log.Printf("converted subnet %s with transactionID %s, validationID %s, and conversionID %s in %s\n", + subnetID, + convertSubnetTx.ID(), + validationID, + conversionID, + time.Since(convertSubnetStartTime), + ) +} From acf026aab812929f65914a4b18ca33f82df33453 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 12:09:43 -0500 Subject: [PATCH 118/184] comments --- vms/platformvm/txs/executor/atomic_tx_executor.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index c1a4b7b54627..317068c97893 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -16,8 +16,6 @@ import ( var _ txs.Visitor = (*AtomicTxExecutor)(nil) -// atomicTxExecutor is used to execute atomic transactions pre-AP5. After AP5 -// the execution was moved to be performed inside of the standardTxExecutor. type AtomicTxExecutor struct { // inputs, to be filled before visitor methods are called backend *Backend @@ -32,6 +30,11 @@ type AtomicTxExecutor struct { atomicRequests map[ids.ID]*atomic.Requests } +// AtomicTx executes the atomic transaction [tx] and returns the resulting state +// modifications. +// +// This is only used to execute atomic transactions pre-AP5. After AP5 the +// execution was moved to be performed during standard transaction execution. func AtomicTx( backend *Backend, feeCalculator fee.Calculator, From 4ec0db95a9c7604d1d1033140e205572b565bd34 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 12:10:23 -0500 Subject: [PATCH 119/184] Remove stutter in P-chain wallet builder (#3524) --- ...uilder_with_options.go => with_options.go} | 111 +++++++++--------- wallet/chain/p/wallet/with_options.go | 2 +- 2 files changed, 56 insertions(+), 57 deletions(-) rename wallet/chain/p/builder/{builder_with_options.go => with_options.go} (60%) diff --git a/wallet/chain/p/builder/builder_with_options.go b/wallet/chain/p/builder/with_options.go similarity index 60% rename from wallet/chain/p/builder/builder_with_options.go rename to wallet/chain/p/builder/with_options.go index 8432944e3542..e83683b89687 100644 --- a/wallet/chain/p/builder/builder_with_options.go +++ b/wallet/chain/p/builder/with_options.go @@ -14,108 +14,107 @@ import ( "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) -var _ Builder = (*builderWithOptions)(nil) +var _ Builder = (*withOptions)(nil) -type builderWithOptions struct { +type withOptions struct { builder Builder options []common.Option } -// NewWithOptions returns a new builder that will use the given options by -// default. +// WithOptions returns a new builder that will use the given options by default. // // - [builder] is the builder that will be called to perform the underlying // operations. // - [options] will be provided to the builder in addition to the options // provided in the method calls. -func NewWithOptions(builder Builder, options ...common.Option) Builder { - return &builderWithOptions{ +func WithOptions(builder Builder, options ...common.Option) Builder { + return &withOptions{ builder: builder, options: options, } } -func (b *builderWithOptions) Context() *Context { - return b.builder.Context() +func (w *withOptions) Context() *Context { + return w.builder.Context() } -func (b *builderWithOptions) GetBalance( +func (w *withOptions) GetBalance( options ...common.Option, ) (map[ids.ID]uint64, error) { - return b.builder.GetBalance( - common.UnionOptions(b.options, options)..., + return w.builder.GetBalance( + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) GetImportableBalance( +func (w *withOptions) GetImportableBalance( chainID ids.ID, options ...common.Option, ) (map[ids.ID]uint64, error) { - return b.builder.GetImportableBalance( + return w.builder.GetImportableBalance( chainID, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewBaseTx( +func (w *withOptions) NewBaseTx( outputs []*avax.TransferableOutput, options ...common.Option, ) (*txs.BaseTx, error) { - return b.builder.NewBaseTx( + return w.builder.NewBaseTx( outputs, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewAddValidatorTx( +func (w *withOptions) NewAddValidatorTx( vdr *txs.Validator, rewardsOwner *secp256k1fx.OutputOwners, shares uint32, options ...common.Option, ) (*txs.AddValidatorTx, error) { - return b.builder.NewAddValidatorTx( + return w.builder.NewAddValidatorTx( vdr, rewardsOwner, shares, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewAddSubnetValidatorTx( +func (w *withOptions) NewAddSubnetValidatorTx( vdr *txs.SubnetValidator, options ...common.Option, ) (*txs.AddSubnetValidatorTx, error) { - return b.builder.NewAddSubnetValidatorTx( + return w.builder.NewAddSubnetValidatorTx( vdr, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewRemoveSubnetValidatorTx( +func (w *withOptions) NewRemoveSubnetValidatorTx( nodeID ids.NodeID, subnetID ids.ID, options ...common.Option, ) (*txs.RemoveSubnetValidatorTx, error) { - return b.builder.NewRemoveSubnetValidatorTx( + return w.builder.NewRemoveSubnetValidatorTx( nodeID, subnetID, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewAddDelegatorTx( +func (w *withOptions) NewAddDelegatorTx( vdr *txs.Validator, rewardsOwner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.AddDelegatorTx, error) { - return b.builder.NewAddDelegatorTx( + return w.builder.NewAddDelegatorTx( vdr, rewardsOwner, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewCreateChainTx( +func (w *withOptions) NewCreateChainTx( subnetID ids.ID, genesis []byte, vmID ids.ID, @@ -123,79 +122,79 @@ func (b *builderWithOptions) NewCreateChainTx( chainName string, options ...common.Option, ) (*txs.CreateChainTx, error) { - return b.builder.NewCreateChainTx( + return w.builder.NewCreateChainTx( subnetID, genesis, vmID, fxIDs, chainName, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewCreateSubnetTx( +func (w *withOptions) NewCreateSubnetTx( owner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.CreateSubnetTx, error) { - return b.builder.NewCreateSubnetTx( + return w.builder.NewCreateSubnetTx( owner, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewTransferSubnetOwnershipTx( +func (w *withOptions) NewTransferSubnetOwnershipTx( subnetID ids.ID, owner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.TransferSubnetOwnershipTx, error) { - return b.builder.NewTransferSubnetOwnershipTx( + return w.builder.NewTransferSubnetOwnershipTx( subnetID, owner, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewConvertSubnetTx( +func (w *withOptions) NewConvertSubnetTx( subnetID ids.ID, chainID ids.ID, address []byte, validators []*txs.ConvertSubnetValidator, options ...common.Option, ) (*txs.ConvertSubnetTx, error) { - return b.builder.NewConvertSubnetTx( + return w.builder.NewConvertSubnetTx( subnetID, chainID, address, validators, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewImportTx( +func (w *withOptions) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.ImportTx, error) { - return b.builder.NewImportTx( + return w.builder.NewImportTx( sourceChainID, to, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewExportTx( +func (w *withOptions) NewExportTx( chainID ids.ID, outputs []*avax.TransferableOutput, options ...common.Option, ) (*txs.ExportTx, error) { - return b.builder.NewExportTx( + return w.builder.NewExportTx( chainID, outputs, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewTransformSubnetTx( +func (w *withOptions) NewTransformSubnetTx( subnetID ids.ID, assetID ids.ID, initialSupply uint64, @@ -212,7 +211,7 @@ func (b *builderWithOptions) NewTransformSubnetTx( uptimeRequirement uint32, options ...common.Option, ) (*txs.TransformSubnetTx, error) { - return b.builder.NewTransformSubnetTx( + return w.builder.NewTransformSubnetTx( subnetID, assetID, initialSupply, @@ -227,11 +226,11 @@ func (b *builderWithOptions) NewTransformSubnetTx( minDelegatorStake, maxValidatorWeightFactor, uptimeRequirement, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewAddPermissionlessValidatorTx( +func (w *withOptions) NewAddPermissionlessValidatorTx( vdr *txs.SubnetValidator, signer signer.Signer, assetID ids.ID, @@ -240,27 +239,27 @@ func (b *builderWithOptions) NewAddPermissionlessValidatorTx( shares uint32, options ...common.Option, ) (*txs.AddPermissionlessValidatorTx, error) { - return b.builder.NewAddPermissionlessValidatorTx( + return w.builder.NewAddPermissionlessValidatorTx( vdr, signer, assetID, validationRewardsOwner, delegationRewardsOwner, shares, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } -func (b *builderWithOptions) NewAddPermissionlessDelegatorTx( +func (w *withOptions) NewAddPermissionlessDelegatorTx( vdr *txs.SubnetValidator, assetID ids.ID, rewardsOwner *secp256k1fx.OutputOwners, options ...common.Option, ) (*txs.AddPermissionlessDelegatorTx, error) { - return b.builder.NewAddPermissionlessDelegatorTx( + return w.builder.NewAddPermissionlessDelegatorTx( vdr, assetID, rewardsOwner, - common.UnionOptions(b.options, options)..., + common.UnionOptions(w.options, options)..., ) } diff --git a/wallet/chain/p/wallet/with_options.go b/wallet/chain/p/wallet/with_options.go index ae6b4b4d8fa9..e73d5a3215ee 100644 --- a/wallet/chain/p/wallet/with_options.go +++ b/wallet/chain/p/wallet/with_options.go @@ -35,7 +35,7 @@ type withOptions struct { } func (w *withOptions) Builder() builder.Builder { - return builder.NewWithOptions( + return builder.WithOptions( w.wallet.Builder(), w.options..., ) From 6217810eb494bfaea94bdd445153daf3f21a7a4a Mon Sep 17 00:00:00 2001 From: Michael Kaplan <55204436+michaelkaplan13@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:17:13 -0500 Subject: [PATCH 120/184] Clarify EndAccumulatedFee comment (#3523) --- vms/platformvm/state/subnet_only_validator.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/state/subnet_only_validator.go b/vms/platformvm/state/subnet_only_validator.go index 352c65e3a27d..d7c20a25ca20 100644 --- a/vms/platformvm/state/subnet_only_validator.go +++ b/vms/platformvm/state/subnet_only_validator.go @@ -115,10 +115,12 @@ type SubnetOnlyValidator struct { // the weight is being set to 0, which removes the validator from the set. MinNonce uint64 `serialize:"true"` - // EndAccumulatedFee is the amount of globally accumulated fees that can - // accrue before this validator must be deactivated. It is equal to the - // amount of fees this validator is willing to pay plus the amount of - // globally accumulated fees when this validator started validating. + // EndAccumulatedFee is the amount of accumulated fees per validator that + // can accrue before this validator must be deactivated. It is equal to the + // amount of fees this validator is willing to pay plus the total amount of + // fees a validator would have needed to pay from the activation of the Etna + // upgrade until this validator was registered. Note that this relies on the + // fact that every validator is charged the same fee for each unit of time. // // If this value is 0, the validator is inactive. EndAccumulatedFee uint64 `serialize:"true"` From 0bf397a3cdb763e50e83013005c5d24e2aeba8dc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 13:23:48 -0500 Subject: [PATCH 121/184] Unexport AtomicTxExecutor --- .../block/executor/verifier_test.go | 285 +++++++----------- .../txs/executor/atomic_tx_executor.go | 74 ++--- 2 files changed, 143 insertions(+), 216 deletions(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index db8e3761f3d1..1a89a0bbda4c 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -18,6 +18,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/upgrade" "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" @@ -49,17 +50,27 @@ import ( validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) -func newTestVerifier(t testing.TB, s state.State) *verifier { +type testVerifierConfig struct { + Upgrades upgrade.Config +} + +func newTestVerifier(t testing.TB, c testVerifierConfig) *verifier { require := require.New(t) + if c.Upgrades == (upgrade.Config{}) { + c.Upgrades = upgradetest.GetConfig(upgradetest.Latest) + } + mempool, err := mempool.New("", prometheus.NewRegistry(), nil) require.NoError(err) var ( - upgrades = upgradetest.GetConfig(upgradetest.Latest) - ctx = snowtest.Context(t, constants.PlatformChainID) - clock = &mockable.Clock{} - fx = &secp256k1fx.Fx{} + state = statetest.New(t, statetest.Config{ + Upgrades: c.Upgrades, + }) + ctx = snowtest.Context(t, constants.PlatformChainID) + clock = &mockable.Clock{} + fx = &secp256k1fx.Fx{} ) require.NoError(fx.InitializeVM(&secp256k1fx.TestVM{ Clk: *clock, @@ -69,9 +80,9 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { return &verifier{ backend: &backend{ Mempool: mempool, - lastAccepted: s.GetLastAccepted(), + lastAccepted: state.GetLastAccepted(), blkIDToState: make(map[ids.ID]*blockState), - state: s, + state: state, ctx: ctx, }, txExecutorBackend: &executor.Backend{ @@ -81,7 +92,7 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, SybilProtectionEnabled: true, - UpgradeConfig: upgrades, + UpgradeConfig: c.Upgrades, }, Ctx: ctx, Clk: clock, @@ -182,185 +193,102 @@ func TestVerifierVisitProposalBlock(t *testing.T) { require.NoError(blk.Verify(context.Background())) } -func TestVerifierVisitAtomicBlock2(t *testing.T) { - s := statetest.New(t, statetest.Config{}) - verifier := newTestVerifier(t, s) - wallet := txstest.NewWallet( - t, - verifier.ctx, - verifier.txExecutorBackend.Config, - s, - secp256k1fx.NewKeychain(genesis.EWOQKey), - nil, // subnetIDs - nil, // chainIDs - ) - - baseTx0, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) - require.NoError(t, err) - baseTx1, err := wallet.IssueBaseTx([]*avax.TransferableOutput{}) - require.NoError(t, err) - - blockComplexity, err := txfee.TxComplexity(baseTx0.Unsigned, baseTx1.Unsigned) - require.NoError(t, err) - blockGas, err := blockComplexity.ToGas(verifier.txExecutorBackend.Config.DynamicFeeConfig.Weights) - require.NoError(t, err) - - const secondsToAdvance = 10 - - initialFeeState := gas.State{} - feeStateAfterTimeAdvanced := initialFeeState.AdvanceTime( - verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxCapacity, - verifier.txExecutorBackend.Config.DynamicFeeConfig.MaxPerSecond, - verifier.txExecutorBackend.Config.DynamicFeeConfig.TargetPerSecond, - secondsToAdvance, +func TestVerifierVisitAtomicBlock(t *testing.T) { + var ( + require = require.New(t) + verifier = newTestVerifier(t, testVerifierConfig{ + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhase4), + }) + wallet = txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + verifier.state, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + nil, // chainIDs + ) + exportedOutput = &avax.TransferableOutput{ + Asset: avax.Asset{ID: verifier.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: units.NanoAvax, + OutputOwners: secp256k1fx.OutputOwners{}, + }, + } + initialTimestamp = verifier.state.GetTimestamp() ) - feeStateAfterGasConsumed, err := feeStateAfterTimeAdvanced.ConsumeGas(blockGas) - require.NoError(t, err) - tests := []struct { - name string - timestamp time.Time - expectedErr error - expectedFeeState gas.State - }{ - { - name: "no capacity", - timestamp: genesistest.DefaultValidatorStartTime, - expectedErr: gas.ErrInsufficientCapacity, - }, - { - name: "updates fee state", - timestamp: genesistest.DefaultValidatorStartTime.Add(secondsToAdvance * time.Second), - expectedFeeState: feeStateAfterGasConsumed, + // Build the transaction that will be executed. + atomicTx, err := wallet.IssueExportTx( + verifier.ctx.XChainID, + []*avax.TransferableOutput{ + exportedOutput, }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - require := require.New(t) - - // Clear the state to prevent prior tests from impacting this test. - clear(verifier.blkIDToState) - - verifier.txExecutorBackend.Clk.Set(test.timestamp) - timestamp, _, err := state.NextBlockTime( - verifier.txExecutorBackend.Config.ValidatorFeeConfig, - s, - verifier.txExecutorBackend.Clk, - ) - require.NoError(err) - - lastAcceptedID := s.GetLastAccepted() - lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) - require.NoError(err) + ) + require.NoError(err) - blk, err := block.NewBanffStandardBlock( - timestamp, - lastAcceptedID, - lastAccepted.Height()+1, - []*txs.Tx{ - baseTx0, - baseTx1, - }, - ) - require.NoError(err) + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) - blkID := blk.ID() - err = blk.Visit(verifier) - require.ErrorIs(err, test.expectedErr) - if err != nil { - require.NotContains(verifier.blkIDToState, blkID) - return - } + atomicBlock, err := block.NewApricotAtomicBlock( + lastAcceptedID, + lastAccepted.Height()+1, + atomicTx, + ) + require.NoError(err) - require.Contains(verifier.blkIDToState, blkID) - blockState := verifier.blkIDToState[blkID] - require.Equal(blk, blockState.statelessBlock) - require.Equal(test.expectedFeeState, blockState.onAcceptState.GetFeeState()) - }) - } -} + // Execute the block. + require.NoError(atomicBlock.Visit(verifier)) -func TestVerifierVisitAtomicBlock(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) + // Verify that the block's execution was recorded as expected. + blkID := atomicBlock.ID() + require.Contains(verifier.blkIDToState, blkID) + atomicBlockState := verifier.blkIDToState[blkID] + onAccept := atomicBlockState.onAcceptState + require.NotNil(onAccept) - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - grandparentID := ids.GenerateTestID() - parentState := state.NewMockDiff(ctrl) + txID := atomicTx.ID() + acceptedTx, acceptedStatus, err := onAccept.GetTx(txID) + require.NoError(err) + require.Equal(atomicTx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), - }, - Clk: &mockable.Clock{}, + exportedUTXO := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(len(atomicTx.UTXOs())), }, - backend: backend, + Asset: exportedOutput.Asset, + Out: exportedOutput.Out, } + exportedUTXOID := exportedUTXO.InputID() + exportedUTXOBytes, err := txs.Codec.Marshal(txs.CodecVersion, exportedUTXO) + require.NoError(err) - onAccept := state.NewMockDiff(ctrl) - blkTx := txsmock.NewUnsignedTx(ctrl) - inputs := set.Of(ids.GenerateTestID()) - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.AtomicTxExecutor{})).DoAndReturn( - func(e *executor.AtomicTxExecutor) error { - e.OnAccept = onAccept - e.Inputs = inputs - return nil - }, - ).Times(1) - - // We can't serialize [blkTx] because it isn't registered with blocks.Codec. - // Serialize this block with a dummy tx and replace it after creation with - // the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotAtomicBlock( - parentID, - 2, - &txs.Tx{ - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, + require.Equal( + &blockState{ + statelessBlock: atomicBlock, + + onAcceptState: onAccept, + + timestamp: initialTimestamp, + atomicRequests: map[ids.ID]*atomic.Requests{ + verifier.ctx.XChainID: { + PutRequests: []*atomic.Element{ + { + Key: exportedUTXOID[:], + Value: exportedUTXOBytes, + Traits: [][]byte{}, + }, + }, + }, + }, + verifiedHeights: set.Of(uint64(0)), }, + atomicBlockState, ) - require.NoError(err) - apricotBlk.Tx.Unsigned = blkTx - - // Set expectations for dependencies. - timestamp := time.Now() - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - parentStatelessBlk.EXPECT().Parent().Return(grandparentID).Times(1) - mempool.EXPECT().Remove([]*txs.Tx{apricotBlk.Tx}).Times(1) - onAccept.EXPECT().AddTx(apricotBlk.Tx, status.Committed).Times(1) - onAccept.EXPECT().GetTimestamp().Return(timestamp).Times(1) - - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) - - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(onAccept, gotBlkState.onAcceptState) - require.Equal(inputs, gotBlkState.inputs) - require.Equal(timestamp, gotBlkState.timestamp) - - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) } func TestVerifierVisitStandardBlock(t *testing.T) { @@ -1222,13 +1150,12 @@ func TestVerifierVisitBanffAbortBlockUnexpectedParentState(t *testing.T) { } func TestBlockExecutionWithComplexity(t *testing.T) { - s := statetest.New(t, statetest.Config{}) - verifier := newTestVerifier(t, s) + verifier := newTestVerifier(t, testVerifierConfig{}) wallet := txstest.NewWallet( t, verifier.ctx, verifier.txExecutorBackend.Config, - s, + verifier.state, secp256k1fx.NewKeychain(genesis.EWOQKey), nil, // subnetIDs nil, // chainIDs @@ -1283,13 +1210,13 @@ func TestBlockExecutionWithComplexity(t *testing.T) { verifier.txExecutorBackend.Clk.Set(test.timestamp) timestamp, _, err := state.NextBlockTime( verifier.txExecutorBackend.Config.ValidatorFeeConfig, - s, + verifier.state, verifier.txExecutorBackend.Clk, ) require.NoError(err) - lastAcceptedID := s.GetLastAccepted() - lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) require.NoError(err) blk, err := block.NewBanffStandardBlock( diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 317068c97893..ed6fc57e0ffc 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -14,21 +14,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) -var _ txs.Visitor = (*AtomicTxExecutor)(nil) - -type AtomicTxExecutor struct { - // inputs, to be filled before visitor methods are called - backend *Backend - feeCalculator fee.Calculator - parentID ids.ID - stateVersions state.Versions - tx *txs.Tx - - // outputs of visitor execution - OnAccept state.Diff - Inputs set.Set[ids.ID] - atomicRequests map[ids.ID]*atomic.Requests -} +var _ txs.Visitor = (*atomicTxVisitor)(nil) // AtomicTx executes the atomic transaction [tx] and returns the resulting state // modifications. @@ -42,7 +28,7 @@ func AtomicTx( stateVersions state.Versions, tx *txs.Tx, ) (state.Diff, set.Set[ids.ID], map[ids.ID]*atomic.Requests, error) { - atomicExecutor := AtomicTxExecutor{ + atomicExecutor := atomicTxVisitor{ backend: backend, feeCalculator: feeCalculator, parentID: parentID, @@ -53,74 +39,88 @@ func AtomicTx( txID := tx.ID() return nil, nil, nil, fmt.Errorf("atomic tx %s failed execution: %w", txID, err) } - return atomicExecutor.OnAccept, atomicExecutor.Inputs, atomicExecutor.atomicRequests, nil + return atomicExecutor.onAccept, atomicExecutor.inputs, atomicExecutor.atomicRequests, nil +} + +type atomicTxVisitor struct { + // inputs, to be filled before visitor methods are called + backend *Backend + feeCalculator fee.Calculator + parentID ids.ID + stateVersions state.Versions + tx *txs.Tx + + // outputs of visitor execution + onAccept state.Diff + inputs set.Set[ids.ID] + atomicRequests map[ids.ID]*atomic.Requests } -func (*AtomicTxExecutor) AddValidatorTx(*txs.AddValidatorTx) error { +func (*atomicTxVisitor) AddValidatorTx(*txs.AddValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { +func (*atomicTxVisitor) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*atomicTxVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) CreateChainTx(*txs.CreateChainTx) error { +func (*atomicTxVisitor) CreateChainTx(*txs.CreateChainTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { +func (*atomicTxVisitor) CreateSubnetTx(*txs.CreateSubnetTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { +func (*atomicTxVisitor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { +func (*atomicTxVisitor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (*atomicTxVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { +func (*atomicTxVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*atomicTxVisitor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*atomicTxVisitor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*atomicTxVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) BaseTx(*txs.BaseTx) error { +func (*atomicTxVisitor) BaseTx(*txs.BaseTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (*atomicTxVisitor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } -func (e *AtomicTxExecutor) ImportTx(tx *txs.ImportTx) error { +func (e *atomicTxVisitor) ImportTx(tx *txs.ImportTx) error { return e.atomicTx(tx) } -func (e *AtomicTxExecutor) ExportTx(tx *txs.ExportTx) error { +func (e *atomicTxVisitor) ExportTx(tx *txs.ExportTx) error { return e.atomicTx(tx) } -func (e *AtomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { +func (e *atomicTxVisitor) atomicTx(tx txs.UnsignedTx) error { onAccept, err := state.NewDiff( e.parentID, e.stateVersions, @@ -128,16 +128,16 @@ func (e *AtomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { if err != nil { return err } - e.OnAccept = onAccept + e.onAccept = onAccept executor := StandardTxExecutor{ Backend: e.backend, - State: e.OnAccept, + State: e.onAccept, FeeCalculator: e.feeCalculator, Tx: e.tx, } err = tx.Visit(&executor) - e.Inputs = executor.Inputs + e.inputs = executor.Inputs e.atomicRequests = executor.AtomicRequests return err } From 5591626513c0de941774078a2c7c7f8f9f05a36f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 13:25:34 -0500 Subject: [PATCH 122/184] nit --- .../txs/executor/atomic_tx_executor.go | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index ed6fc57e0ffc..90eb1725e057 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -14,7 +14,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) -var _ txs.Visitor = (*atomicTxVisitor)(nil) +var _ txs.Visitor = (*atomicTxExecutor)(nil) // AtomicTx executes the atomic transaction [tx] and returns the resulting state // modifications. @@ -28,7 +28,7 @@ func AtomicTx( stateVersions state.Versions, tx *txs.Tx, ) (state.Diff, set.Set[ids.ID], map[ids.ID]*atomic.Requests, error) { - atomicExecutor := atomicTxVisitor{ + atomicExecutor := atomicTxExecutor{ backend: backend, feeCalculator: feeCalculator, parentID: parentID, @@ -42,7 +42,7 @@ func AtomicTx( return atomicExecutor.onAccept, atomicExecutor.inputs, atomicExecutor.atomicRequests, nil } -type atomicTxVisitor struct { +type atomicTxExecutor struct { // inputs, to be filled before visitor methods are called backend *Backend feeCalculator fee.Calculator @@ -56,71 +56,71 @@ type atomicTxVisitor struct { atomicRequests map[ids.ID]*atomic.Requests } -func (*atomicTxVisitor) AddValidatorTx(*txs.AddValidatorTx) error { +func (*atomicTxExecutor) AddValidatorTx(*txs.AddValidatorTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { +func (*atomicTxExecutor) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*atomicTxExecutor) AddDelegatorTx(*txs.AddDelegatorTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) CreateChainTx(*txs.CreateChainTx) error { +func (*atomicTxExecutor) CreateChainTx(*txs.CreateChainTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) CreateSubnetTx(*txs.CreateSubnetTx) error { +func (*atomicTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { +func (*atomicTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) RewardValidatorTx(*txs.RewardValidatorTx) error { +func (*atomicTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (*atomicTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { +func (*atomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) BaseTx(*txs.BaseTx) error { +func (*atomicTxExecutor) BaseTx(*txs.BaseTx) error { return ErrWrongTxType } -func (*atomicTxVisitor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (*atomicTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } -func (e *atomicTxVisitor) ImportTx(tx *txs.ImportTx) error { +func (e *atomicTxExecutor) ImportTx(tx *txs.ImportTx) error { return e.atomicTx(tx) } -func (e *atomicTxVisitor) ExportTx(tx *txs.ExportTx) error { +func (e *atomicTxExecutor) ExportTx(tx *txs.ExportTx) error { return e.atomicTx(tx) } -func (e *atomicTxVisitor) atomicTx(tx txs.UnsignedTx) error { +func (e *atomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { onAccept, err := state.NewDiff( e.parentID, e.stateVersions, From 0093c70cfe79ca93fd1f91bb8a5bbfa8b20ce9e9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 15:35:17 -0500 Subject: [PATCH 123/184] Unexport ProposalTxExecutor --- vms/platformvm/block/executor/verifier.go | 17 +- .../block/executor/verifier_test.go | 123 ++++----- .../txs/executor/advance_time_test.go | 210 ++++++++------- .../txs/executor/proposal_tx_executor.go | 239 +++++++++-------- .../txs/executor/proposal_tx_executor_test.go | 240 ++++++++---------- .../txs/executor/reward_validator_test.go | 196 +++++++------- 6 files changed, 493 insertions(+), 532 deletions(-) diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index 24d2083d2ae3..dbff557ce263 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -394,15 +394,14 @@ func (v *verifier) proposalBlock( atomicRequests map[ids.ID]*atomic.Requests, onAcceptFunc func(), ) error { - txExecutor := executor.ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: v.txExecutorBackend, - FeeCalculator: feeCalculator, - Tx: tx, - } - - if err := tx.Unsigned.Visit(&txExecutor); err != nil { + err := executor.ProposalTx( + v.txExecutorBackend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) + if err != nil { txID := tx.ID() v.MarkDropped(txID, err) // cache tx as dropped return err diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 1a89a0bbda4c..7f7b21671620 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -108,89 +108,70 @@ func newTestVerifier(t testing.TB, c testVerifierConfig) *verifier { } func TestVerifierVisitProposalBlock(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentOnAcceptState := state.NewMockDiff(ctrl) - timestamp := time.Now() - // One call for each of onCommitState and onAbortState. - parentOnAcceptState.EXPECT().GetTimestamp().Return(timestamp).Times(2) - parentOnAcceptState.EXPECT().GetFeeState().Return(gas.State{}).Times(2) - parentOnAcceptState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(2) - parentOnAcceptState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(2) - parentOnAcceptState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(2) - - backend := &backend{ - lastAccepted: parentID, - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentOnAcceptState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + var ( + require = require.New(t) + verifier = newTestVerifier(t, testVerifierConfig{ + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + }) + initialTimestamp = verifier.state.GetTimestamp() + newTimestamp = initialTimestamp.Add(time.Second) + proposalTx = &txs.Tx{ + Unsigned: &txs.AdvanceTimeTx{ + Time: uint64(newTimestamp.Unix()), }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + ) + require.NoError(proposalTx.Initialize(txs.Codec)) - blkTx := txsmock.NewUnsignedTx(ctrl) - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.ProposalTxExecutor{})).Return(nil).Times(1) + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotProposalBlock( - parentID, - 2, - &txs.Tx{ - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, + proposalBlock, err := block.NewApricotProposalBlock( + lastAcceptedID, + lastAccepted.Height()+1, + proposalTx, ) require.NoError(err) - apricotBlk.Tx.Unsigned = blkTx - // Set expectations for dependencies. - tx := apricotBlk.Txs()[0] - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - mempool.EXPECT().Remove([]*txs.Tx{tx}).Times(1) + // Execute the block. + require.NoError(proposalBlock.Visit(verifier)) - // Visit the block - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(timestamp, gotBlkState.timestamp) + // Verify that the block's execution was recorded as expected. + blkID := proposalBlock.ID() + require.Contains(verifier.blkIDToState, blkID) + executedBlockState := verifier.blkIDToState[blkID] - // Assert that the expected tx statuses are set. - _, gotStatus, err := gotBlkState.onCommitState.GetTx(tx.ID()) + txID := proposalTx.ID() + + onCommit := executedBlockState.onCommitState + require.NotNil(onCommit) + acceptedTx, acceptedStatus, err := onCommit.GetTx(txID) require.NoError(err) - require.Equal(status.Committed, gotStatus) + require.Equal(proposalTx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) - _, gotStatus, err = gotBlkState.onAbortState.GetTx(tx.ID()) + onAbort := executedBlockState.onAbortState + require.NotNil(onAbort) + acceptedTx, acceptedStatus, err = onAbort.GetTx(txID) require.NoError(err) - require.Equal(status.Aborted, gotStatus) + require.Equal(proposalTx, acceptedTx) + require.Equal(status.Aborted, acceptedStatus) - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + require.Equal( + &blockState{ + proposalBlockState: proposalBlockState{ + onCommitState: onCommit, + onAbortState: onAbort, + }, + statelessBlock: proposalBlock, + + timestamp: initialTimestamp, + verifiedHeights: set.Of(uint64(0)), + }, + executedBlockState, + ) } func TestVerifierVisitAtomicBlock(t *testing.T) { diff --git a/vms/platformvm/txs/executor/advance_time_test.go b/vms/platformvm/txs/executor/advance_time_test.go index fa7d3583c68f..5acb4f29fca3 100644 --- a/vms/platformvm/txs/executor/advance_time_test.go +++ b/vms/platformvm/txs/executor/advance_time_test.go @@ -66,32 +66,31 @@ func TestAdvanceTimeTxUpdatePrimaryNetworkStakers(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - validatorStaker, err := executor.OnCommitState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + validatorStaker, err := onCommitState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) require.NoError(err) require.Equal(addPendingValidatorTx.ID(), validatorStaker.TxID) require.Equal(uint64(1370), validatorStaker.PotentialReward) // See rewards tests to explain why 1370 - _, err = executor.OnCommitState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) + _, err = onCommitState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) require.ErrorIs(err, database.ErrNotFound) - _, err = executor.OnAbortState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + _, err = onAbortState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) require.ErrorIs(err, database.ErrNotFound) - validatorStaker, err = executor.OnAbortState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) + validatorStaker, err = onAbortState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) require.NoError(err) require.Equal(addPendingValidatorTx.ID(), validatorStaker.TxID) // Test VM validators - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -115,14 +114,13 @@ func TestAdvanceTimeTxTimestampTooEarly(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrChildBlockEarlierThanParent) } @@ -152,14 +150,13 @@ func TestAdvanceTimeTxTimestampTooLate(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrChildBlockAfterStakerChangeTime) } @@ -183,14 +180,13 @@ func TestAdvanceTimeTxTimestampTooLate(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrChildBlockAfterStakerChangeTime) } } @@ -428,16 +424,15 @@ func TestAdvanceTimeTxUpdateStakers(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) - - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) + + require.NoError(onCommitState.Apply(env.state)) } env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -562,20 +557,19 @@ func TestAdvanceTimeTxRemoveSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) - - _, err = executor.OnCommitState.GetCurrentValidator(subnetID, subnetValidatorNodeID) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) + + _, err = onCommitState.GetCurrentValidator(subnetID, subnetValidatorNodeID) require.ErrorIs(err, database.ErrNotFound) // Check VM Validators are removed successfully - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -644,16 +638,15 @@ func TestTrackedSubnet(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -694,16 +687,15 @@ func TestAdvanceTimeTxDelegatorStakerWeight(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -753,16 +745,15 @@ func TestAdvanceTimeTxDelegatorStakerWeight(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - executor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -796,16 +787,15 @@ func TestAdvanceTimeTxDelegatorStakers(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -854,16 +844,15 @@ func TestAdvanceTimeTxDelegatorStakers(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - executor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -895,14 +884,13 @@ func TestAdvanceTimeTxAfterBanff(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrAdvanceTimeTxIssuedAfterBanff) } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 4c4c1f5c2b91..5fe0851e92be 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -30,7 +30,7 @@ const ( ) var ( - _ txs.Visitor = (*ProposalTxExecutor)(nil) + _ txs.Visitor = (*proposalTxExecutor)(nil) ErrRemoveStakerTooEarly = errors.New("attempting to remove staker before their end time") ErrRemoveWrongStaker = errors.New("attempting to remove wrong staker") @@ -42,259 +42,280 @@ var ( ErrAdvanceTimeTxIssuedAfterBanff = errors.New("AdvanceTimeTx issued after Banff") ) -type ProposalTxExecutor struct { +func ProposalTx( + backend *Backend, + feeCalculator fee.Calculator, + tx *txs.Tx, + onCommitState state.Diff, + onAbortState state.Diff, +) error { + proposalExecutor := proposalTxExecutor{ + backend: backend, + feeCalculator: feeCalculator, + tx: tx, + onCommitState: onCommitState, + onAbortState: onAbortState, + } + if err := tx.Unsigned.Visit(&proposalExecutor); err != nil { + txID := tx.ID() + return fmt.Errorf("proposal tx %s failed execution: %w", txID, err) + } + return nil +} + +type proposalTxExecutor struct { // inputs, to be filled before visitor methods are called - *Backend - FeeCalculator fee.Calculator - Tx *txs.Tx - // [OnCommitState] is the state used for validation. - // [OnCommitState] is modified by this struct's methods to + backend *Backend + feeCalculator fee.Calculator + tx *txs.Tx + // [onCommitState] is the state used for validation. + // [onCommitState] is modified by this struct's methods to // reflect changes made to the state if the proposal is committed. // - // Invariant: Both [OnCommitState] and [OnAbortState] represent the same + // Invariant: Both [onCommitState] and [OnAbortState] represent the same // state when provided to this struct. - OnCommitState state.Diff - // [OnAbortState] is modified by this struct's methods to + onCommitState state.Diff + // [onAbortState] is modified by this struct's methods to // reflect changes made to the state if the proposal is aborted. - OnAbortState state.Diff + onAbortState state.Diff } -func (*ProposalTxExecutor) CreateChainTx(*txs.CreateChainTx) error { +func (*proposalTxExecutor) CreateChainTx(*txs.CreateChainTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { +func (*proposalTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) ImportTx(*txs.ImportTx) error { +func (*proposalTxExecutor) ImportTx(*txs.ImportTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) ExportTx(*txs.ExportTx) error { +func (*proposalTxExecutor) ExportTx(*txs.ExportTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (*proposalTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { +func (*proposalTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*proposalTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*proposalTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*proposalTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) BaseTx(*txs.BaseTx) error { +func (*proposalTxExecutor) BaseTx(*txs.BaseTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (*proposalTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } -func (e *ProposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { +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 // StandardBlocks. - currentTimestamp := e.OnCommitState.GetTimestamp() - if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { + currentTimestamp := e.onCommitState.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { return fmt.Errorf( "%w: timestamp (%s) >= Banff fork time (%s)", ErrProposedAddStakerTxAfterBanff, currentTimestamp, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } onAbortOuts, err := verifyAddValidatorTx( - e.Backend, - e.FeeCalculator, - e.OnCommitState, - e.Tx, + e.backend, + e.feeCalculator, + e.onCommitState, + e.tx, tx, ) if err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Set up the state if this tx is committed // Consume the UTXOs - avax.Consume(e.OnCommitState, tx.Ins) + avax.Consume(e.onCommitState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnCommitState, txID, tx.Outs) + avax.Produce(e.onCommitState, txID, tx.Outs) newStaker, err := state.NewPendingStaker(txID, tx) if err != nil { return err } - if err := e.OnCommitState.PutPendingValidator(newStaker); err != nil { + if err := e.onCommitState.PutPendingValidator(newStaker); err != nil { return err } // Set up the state if this tx is aborted // Consume the UTXOs - avax.Consume(e.OnAbortState, tx.Ins) + avax.Consume(e.onAbortState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnAbortState, txID, onAbortOuts) + avax.Produce(e.onAbortState, txID, onAbortOuts) return nil } -func (e *ProposalTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { +func (e *proposalTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { // AddSubnetValidatorTx is a proposal transaction until the Banff fork // activation. Following the activation, AddSubnetValidatorTxs must be // issued into StandardBlocks. - currentTimestamp := e.OnCommitState.GetTimestamp() - if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { + currentTimestamp := e.onCommitState.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { return fmt.Errorf( "%w: timestamp (%s) >= Banff fork time (%s)", ErrProposedAddStakerTxAfterBanff, currentTimestamp, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } if err := verifyAddSubnetValidatorTx( - e.Backend, - e.FeeCalculator, - e.OnCommitState, - e.Tx, + e.backend, + e.feeCalculator, + e.onCommitState, + e.tx, tx, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Set up the state if this tx is committed // Consume the UTXOs - avax.Consume(e.OnCommitState, tx.Ins) + avax.Consume(e.onCommitState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnCommitState, txID, tx.Outs) + avax.Produce(e.onCommitState, txID, tx.Outs) newStaker, err := state.NewPendingStaker(txID, tx) if err != nil { return err } - if err := e.OnCommitState.PutPendingValidator(newStaker); err != nil { + if err := e.onCommitState.PutPendingValidator(newStaker); err != nil { return err } // Set up the state if this tx is aborted // Consume the UTXOs - avax.Consume(e.OnAbortState, tx.Ins) + avax.Consume(e.onAbortState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnAbortState, txID, tx.Outs) + avax.Produce(e.onAbortState, txID, tx.Outs) return nil } -func (e *ProposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { +func (e *proposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { // AddDelegatorTx is a proposal transaction until the Banff fork // activation. Following the activation, AddDelegatorTxs must be issued into // StandardBlocks. - currentTimestamp := e.OnCommitState.GetTimestamp() - if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { + currentTimestamp := e.onCommitState.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { return fmt.Errorf( "%w: timestamp (%s) >= Banff fork time (%s)", ErrProposedAddStakerTxAfterBanff, currentTimestamp, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } onAbortOuts, err := verifyAddDelegatorTx( - e.Backend, - e.FeeCalculator, - e.OnCommitState, - e.Tx, + e.backend, + e.feeCalculator, + e.onCommitState, + e.tx, tx, ) if err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Set up the state if this tx is committed // Consume the UTXOs - avax.Consume(e.OnCommitState, tx.Ins) + avax.Consume(e.onCommitState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnCommitState, txID, tx.Outs) + avax.Produce(e.onCommitState, txID, tx.Outs) newStaker, err := state.NewPendingStaker(txID, tx) if err != nil { return err } - e.OnCommitState.PutPendingDelegator(newStaker) + e.onCommitState.PutPendingDelegator(newStaker) // Set up the state if this tx is aborted // Consume the UTXOs - avax.Consume(e.OnAbortState, tx.Ins) + avax.Consume(e.onAbortState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnAbortState, txID, onAbortOuts) + avax.Produce(e.onAbortState, txID, onAbortOuts) return nil } -func (e *ProposalTxExecutor) AdvanceTimeTx(tx *txs.AdvanceTimeTx) error { +func (e *proposalTxExecutor) AdvanceTimeTx(tx *txs.AdvanceTimeTx) error { switch { case tx == nil: return txs.ErrNilTx - case len(e.Tx.Creds) != 0: + case len(e.tx.Creds) != 0: return errWrongNumberOfCredentials } // Validate [newChainTime] newChainTime := tx.Timestamp() - if e.Config.UpgradeConfig.IsBanffActivated(newChainTime) { + if e.backend.Config.UpgradeConfig.IsBanffActivated(newChainTime) { return fmt.Errorf( "%w: proposed timestamp (%s) >= Banff fork time (%s)", ErrAdvanceTimeTxIssuedAfterBanff, newChainTime, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } - now := e.Clk.Time() + now := e.backend.Clk.Time() if err := VerifyNewChainTime( - e.Config.ValidatorFeeConfig, + e.backend.Config.ValidatorFeeConfig, newChainTime, now, - e.OnCommitState, + e.onCommitState, ); err != nil { return err } // Note that state doesn't change if this proposal is aborted - _, err := AdvanceTimeTo(e.Backend, e.OnCommitState, newChainTime) + _, err := AdvanceTimeTo(e.backend, e.onCommitState, newChainTime) return err } -func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error { +func (e *proposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error { switch { case tx == nil: return txs.ErrNilTx case tx.TxID == ids.Empty: return ErrInvalidID - case len(e.Tx.Creds) != 0: + case len(e.tx.Creds) != 0: return errWrongNumberOfCredentials } - currentStakerIterator, err := e.OnCommitState.GetCurrentStakerIterator() + currentStakerIterator, err := e.onCommitState.GetCurrentStakerIterator() if err != nil { return err } @@ -314,7 +335,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error } // Verify that the chain's timestamp is the validator's end time - currentChainTime := e.OnCommitState.GetTimestamp() + currentChainTime := e.onCommitState.GetTimestamp() if !stakerToReward.EndTime.Equal(currentChainTime) { return fmt.Errorf( "%w: TxID = %s with %s < %s", @@ -325,7 +346,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error ) } - stakerTx, _, err := e.OnCommitState.GetTx(stakerToReward.TxID) + stakerTx, _, err := e.onCommitState.GetTx(stakerToReward.TxID) if err != nil { return fmt.Errorf("failed to get next removed staker tx: %w", err) } @@ -339,16 +360,16 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error } // Handle staker lifecycle. - e.OnCommitState.DeleteCurrentValidator(stakerToReward) - e.OnAbortState.DeleteCurrentValidator(stakerToReward) + e.onCommitState.DeleteCurrentValidator(stakerToReward) + e.onAbortState.DeleteCurrentValidator(stakerToReward) case txs.DelegatorTx: if err := e.rewardDelegatorTx(uStakerTx, stakerToReward); err != nil { return err } // Handle staker lifecycle. - e.OnCommitState.DeleteCurrentDelegator(stakerToReward) - e.OnAbortState.DeleteCurrentDelegator(stakerToReward) + e.onCommitState.DeleteCurrentDelegator(stakerToReward) + e.onAbortState.DeleteCurrentDelegator(stakerToReward) default: // Invariant: Permissioned stakers are removed by the advancement of // time and the current chain timestamp is == this staker's @@ -358,7 +379,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error } // If the reward is aborted, then the current supply should be decreased. - currentSupply, err := e.OnAbortState.GetCurrentSupply(stakerToReward.SubnetID) + currentSupply, err := e.onAbortState.GetCurrentSupply(stakerToReward.SubnetID) if err != nil { return err } @@ -366,11 +387,11 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error if err != nil { return err } - e.OnAbortState.SetCurrentSupply(stakerToReward.SubnetID, newSupply) + e.onAbortState.SetCurrentSupply(stakerToReward.SubnetID, newSupply) return nil } -func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, validator *state.Staker) error { +func (e *proposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, validator *state.Staker) error { var ( txID = validator.TxID stake = uValidatorTx.Stake() @@ -390,8 +411,8 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: out.Asset, Out: out.Output(), } - e.OnCommitState.AddUTXO(utxo) - e.OnAbortState.AddUTXO(utxo) + e.onCommitState.AddUTXO(utxo) + e.onAbortState.AddUTXO(utxo) } utxosOffset := 0 @@ -400,7 +421,7 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val reward := validator.PotentialReward if reward > 0 { validationRewardsOwner := uValidatorTx.ValidationRewardsOwner() - outIntf, err := e.Fx.CreateOutput(reward, validationRewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(reward, validationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -417,14 +438,14 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: stakeAsset, Out: out, } - e.OnCommitState.AddUTXO(utxo) - e.OnCommitState.AddRewardUTXO(txID, utxo) + e.onCommitState.AddUTXO(utxo) + e.onCommitState.AddRewardUTXO(txID, utxo) utxosOffset++ } // Provide the accrued delegatee rewards from successful delegations here. - delegateeReward, err := e.OnCommitState.GetDelegateeReward( + delegateeReward, err := e.onCommitState.GetDelegateeReward( validator.SubnetID, validator.NodeID, ) @@ -437,7 +458,7 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val } delegationRewardsOwner := uValidatorTx.DelegationRewardsOwner() - outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -454,8 +475,8 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: stakeAsset, Out: out, } - e.OnCommitState.AddUTXO(onCommitUtxo) - e.OnCommitState.AddRewardUTXO(txID, onCommitUtxo) + e.onCommitState.AddUTXO(onCommitUtxo) + e.onCommitState.AddRewardUTXO(txID, onCommitUtxo) // Note: There is no [offset] if the RewardValidatorTx is // aborted, because the validator reward is not awarded. @@ -467,12 +488,12 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: stakeAsset, Out: out, } - e.OnAbortState.AddUTXO(onAbortUtxo) - e.OnAbortState.AddRewardUTXO(txID, onAbortUtxo) + e.onAbortState.AddUTXO(onAbortUtxo) + e.onAbortState.AddRewardUTXO(txID, onAbortUtxo) return nil } -func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, delegator *state.Staker) error { +func (e *proposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, delegator *state.Staker) error { var ( txID = delegator.TxID stake = uDelegatorTx.Stake() @@ -492,18 +513,18 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del Asset: out.Asset, Out: out.Output(), } - e.OnCommitState.AddUTXO(utxo) - e.OnAbortState.AddUTXO(utxo) + e.onCommitState.AddUTXO(utxo) + e.onAbortState.AddUTXO(utxo) } // We're (possibly) rewarding a delegator, so we need to fetch // the validator they are delegated to. - validator, err := e.OnCommitState.GetCurrentValidator(delegator.SubnetID, delegator.NodeID) + validator, err := e.onCommitState.GetCurrentValidator(delegator.SubnetID, delegator.NodeID) if err != nil { return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err) } - vdrTxIntf, _, err := e.OnCommitState.GetTx(validator.TxID) + vdrTxIntf, _, err := e.onCommitState.GetTx(validator.TxID) if err != nil { return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err) } @@ -526,7 +547,7 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del reward := delegatorReward if reward > 0 { rewardsOwner := uDelegatorTx.RewardsOwner() - outIntf, err := e.Fx.CreateOutput(reward, rewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(reward, rewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -543,8 +564,8 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del Out: out, } - e.OnCommitState.AddUTXO(utxo) - e.OnCommitState.AddRewardUTXO(txID, utxo) + e.onCommitState.AddUTXO(utxo) + e.onCommitState.AddRewardUTXO(txID, utxo) utxosOffset++ } @@ -554,8 +575,8 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del } // Reward the delegatee here - if e.Config.UpgradeConfig.IsCortinaActivated(validator.StartTime) { - previousDelegateeReward, err := e.OnCommitState.GetDelegateeReward( + if e.backend.Config.UpgradeConfig.IsCortinaActivated(validator.StartTime) { + previousDelegateeReward, err := e.onCommitState.GetDelegateeReward( validator.SubnetID, validator.NodeID, ) @@ -570,7 +591,7 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del // For any validators starting after [CortinaTime], we defer rewarding the // [reward] until their staking period is over. - err = e.OnCommitState.SetDelegateeReward( + err = e.onCommitState.SetDelegateeReward( validator.SubnetID, validator.NodeID, newDelegateeReward, @@ -582,7 +603,7 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del // For any validators who started prior to [CortinaTime], we issue the // [delegateeReward] immediately. delegationRewardsOwner := vdrTx.DelegationRewardsOwner() - outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -599,8 +620,8 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del Out: out, } - e.OnCommitState.AddUTXO(utxo) - e.OnCommitState.AddRewardUTXO(txID, utxo) + e.onCommitState.AddUTXO(utxo) + e.onCommitState.AddRewardUTXO(txID, utxo) } return nil } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor_test.go b/vms/platformvm/txs/executor/proposal_tx_executor_test.go index eed2e6a6ddeb..d6759ee54767 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor_test.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor_test.go @@ -268,14 +268,13 @@ func TestProposalTxExecuteAddDelegator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, tt.expectedErr) }) } @@ -316,14 +315,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -355,14 +353,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) } // Add a validator to pending validator set of primary network @@ -414,14 +411,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrNotValidator) } @@ -468,14 +464,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -505,14 +500,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -542,14 +536,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) } // Case: Proposed validator start validating at/before current timestamp @@ -581,14 +574,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -652,14 +644,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: duplicateSubnetTx, - } - err = duplicateSubnetTx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + duplicateSubnetTx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrDuplicateValidator) } @@ -699,14 +690,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -740,14 +730,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -791,14 +780,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrDuplicateValidator) } } @@ -838,14 +826,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -873,14 +860,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -922,14 +908,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -965,14 +950,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrFlowCheckFailed) } } diff --git a/vms/platformvm/txs/executor/reward_validator_test.go b/vms/platformvm/txs/executor/reward_validator_test.go index 86fb83f994ed..e10da2bfd20e 100644 --- a/vms/platformvm/txs/executor/reward_validator_test.go +++ b/vms/platformvm/txs/executor/reward_validator_test.go @@ -61,14 +61,13 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAbortState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveStakerTooEarly) // Advance chain timestamp to time that next validator leaves @@ -84,14 +83,13 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveWrongStaker) // Case 3: Happy path @@ -104,16 +102,15 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - onCommitStakerIterator, err := txExecutor.OnCommitState.GetCurrentStakerIterator() + onCommitStakerIterator, err := onCommitState.GetCurrentStakerIterator() require.NoError(err) require.True(onCommitStakerIterator.Next()) @@ -128,7 +125,7 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { oldBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -165,14 +162,13 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAbortState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveStakerTooEarly) // Advance chain timestamp to time that next validator leaves @@ -182,14 +178,13 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { tx, err = newRewardValidatorTx(t, ids.GenerateTestID()) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveWrongStaker) // Case 3: Happy path @@ -202,16 +197,15 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - onAbortStakerIterator, err := txExecutor.OnAbortState.GetCurrentStakerIterator() + onAbortStakerIterator, err := onAbortState.GetCurrentStakerIterator() require.NoError(err) require.True(onAbortStakerIterator.Next()) @@ -226,7 +220,7 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { oldBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - require.NoError(txExecutor.OnAbortState.Apply(env.state)) + require.NoError(onAbortState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -322,14 +316,13 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) vdrDestSet := set.Of(vdrRewardAddress) delDestSet := set.Of(delRewardAddress) @@ -341,7 +334,7 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { oldDelBalance, err := avax.GetBalance(env.state, delDestSet) require.NoError(err) - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -464,14 +457,13 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) // 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. @@ -498,7 +490,7 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { require.ErrorIs(err, database.ErrNotFound) // Commit Delegator Diff - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -513,14 +505,13 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) require.NotEqual(vdrStaker.TxID, delStaker.TxID) @@ -569,7 +560,7 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { require.ErrorIs(err, database.ErrNotFound) // Commit Validator Diff - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -687,14 +678,13 @@ func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t * require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, delOnCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: delOnCommitState, - OnAbortState: delOnAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + delOnCommitState, + delOnAbortState, + )) // Create Validator Diffs testID := ids.GenerateTestID() @@ -709,14 +699,13 @@ func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t * tx, err = newRewardValidatorTx(t, vdrTx.ID()) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: vdrOnCommitState, - OnAbortState: vdrOnAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + vdrOnCommitState, + vdrOnAbortState, + )) // aborted validator tx should still distribute accrued delegator rewards numVdrStakeUTXOs := uint32(len(delTx.Unsigned.InputIDs())) @@ -849,14 +838,13 @@ func TestRewardDelegatorTxExecuteOnAbort(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) vdrDestSet := set.Of(vdrRewardAddress) delDestSet := set.Of(delRewardAddress) @@ -868,7 +856,7 @@ func TestRewardDelegatorTxExecuteOnAbort(t *testing.T) { oldDelBalance, err := avax.GetBalance(env.state, delDestSet) require.NoError(err) - require.NoError(txExecutor.OnAbortState.Apply(env.state)) + require.NoError(onAbortState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) From a0e6bcd3115b05fee3cc011052b9aebd5f049ebb Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 16:29:22 -0500 Subject: [PATCH 124/184] comment new function --- .../txs/executor/proposal_tx_executor.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 5fe0851e92be..061f3ee60e95 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -42,6 +42,17 @@ var ( ErrAdvanceTimeTxIssuedAfterBanff = errors.New("AdvanceTimeTx issued after Banff") ) +// ProposalTx executes the proposal transaction [tx] and modifies +// [onCommitState] and [onAbortState] according to the transaction logic. +// +// [onCommitState] will be modified to reflect the changes made to the state if +// the proposal is committed. +// +// [onAbortState] will be modified to reflect the changes made to the state if +// the proposal is aborted. +// +// Invariant: It is assumed that [onCommitState] and [onAbortState] represent +// the same state when passed into this function. func ProposalTx( backend *Backend, feeCalculator fee.Calculator, @@ -71,9 +82,6 @@ type proposalTxExecutor struct { // [onCommitState] is the state used for validation. // [onCommitState] is modified by this struct's methods to // reflect changes made to the state if the proposal is committed. - // - // Invariant: Both [onCommitState] and [OnAbortState] represent the same - // state when provided to this struct. onCommitState state.Diff // [onAbortState] is modified by this struct's methods to // reflect changes made to the state if the proposal is aborted. From 5e46925808518f5c0fe503a66fe954c55fc5c177 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Wed, 6 Nov 2024 16:29:43 -0500 Subject: [PATCH 125/184] nit --- vms/platformvm/txs/executor/proposal_tx_executor.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 061f3ee60e95..03b03c889a23 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -42,8 +42,7 @@ var ( ErrAdvanceTimeTxIssuedAfterBanff = errors.New("AdvanceTimeTx issued after Banff") ) -// ProposalTx executes the proposal transaction [tx] and modifies -// [onCommitState] and [onAbortState] according to the transaction logic. +// ProposalTx executes the proposal transaction [tx]. // // [onCommitState] will be modified to reflect the changes made to the state if // the proposal is committed. From 706c83301f89853e2e269f0fc0318e63af2d5b1b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:03:27 -0500 Subject: [PATCH 126/184] Standardize standard tx executor --- vms/platformvm/block/builder/builder.go | 20 +- vms/platformvm/block/builder/helpers_test.go | 14 +- vms/platformvm/block/executor/helpers_test.go | 14 +- vms/platformvm/block/executor/manager.go | 13 +- vms/platformvm/block/executor/verifier.go | 24 +- .../block/executor/verifier_test.go | 323 +++++----- .../txs/executor/atomic_tx_executor.go | 27 +- .../txs/executor/create_chain_test.go | 79 ++- .../txs/executor/create_subnet_test.go | 13 +- vms/platformvm/txs/executor/export_test.go | 14 +- vms/platformvm/txs/executor/helpers_test.go | 14 +- vms/platformvm/txs/executor/import_test.go | 14 +- .../txs/executor/standard_tx_executor.go | 405 ++++++------ .../txs/executor/standard_tx_executor_test.go | 590 +++++++++--------- 14 files changed, 754 insertions(+), 810 deletions(-) diff --git a/vms/platformvm/block/builder/builder.go b/vms/platformvm/block/builder/builder.go index 3c88e8277929..b57cc9ecb4e5 100644 --- a/vms/platformvm/block/builder/builder.go +++ b/vms/platformvm/block/builder/builder.go @@ -516,32 +516,30 @@ func executeTx( return false, err } - executor := &txexecutor.StandardTxExecutor{ - Backend: backend, - State: txDiff, - FeeCalculator: feeCalculator, - Tx: tx, - } - - err = tx.Unsigned.Visit(executor) + txInputs, _, _, err := txexecutor.StandardTx( + backend, + feeCalculator, + tx, + txDiff, + ) if err != nil { txID := tx.ID() mempool.MarkDropped(txID, err) return false, nil } - if inputs.Overlaps(executor.Inputs) { + if inputs.Overlaps(txInputs) { txID := tx.ID() mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs) return false, nil } - err = manager.VerifyUniqueInputs(parentID, executor.Inputs) + err = manager.VerifyUniqueInputs(parentID, txInputs) if err != nil { txID := tx.ID() mempool.MarkDropped(txID, err) return false, nil } - inputs.Union(executor.Inputs) + inputs.Union(txInputs) txDiff.AddTx(tx, status.Committed) return true, txDiff.Apply(stateDiff) diff --git a/vms/platformvm/block/builder/helpers_test.go b/vms/platformvm/block/builder/helpers_test.go index faba1b1ed695..c779609baf54 100644 --- a/vms/platformvm/block/builder/helpers_test.go +++ b/vms/platformvm/block/builder/helpers_test.go @@ -247,13 +247,13 @@ func addSubnet(t *testing.T, env *environment) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := txexecutor.StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: testSubnet1, - } - require.NoError(testSubnet1.Unsigned.Visit(&executor)) + _, _, _, err = txexecutor.StandardTx( + &env.backend, + feeCalculator, + testSubnet1, + stateDiff, + ) + require.NoError(err) stateDiff.AddTx(testSubnet1, status.Committed) require.NoError(stateDiff.Apply(env.state)) diff --git a/vms/platformvm/block/executor/helpers_test.go b/vms/platformvm/block/executor/helpers_test.go index 4e3da112739f..d0616a8e374c 100644 --- a/vms/platformvm/block/executor/helpers_test.go +++ b/vms/platformvm/block/executor/helpers_test.go @@ -253,13 +253,13 @@ func addSubnet(t testing.TB, env *environment) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := executor.StandardTxExecutor{ - Backend: env.backend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: testSubnet1, - } - require.NoError(testSubnet1.Unsigned.Visit(&executor)) + _, _, _, err = executor.StandardTx( + env.backend, + feeCalculator, + testSubnet1, + stateDiff, + ) + require.NoError(err) stateDiff.AddTx(testSubnet1, status.Committed) require.NoError(stateDiff.Apply(env.state)) diff --git a/vms/platformvm/block/executor/manager.go b/vms/platformvm/block/executor/manager.go index 5c419500e5f7..7f153a83be3f 100644 --- a/vms/platformvm/block/executor/manager.go +++ b/vms/platformvm/block/executor/manager.go @@ -142,12 +142,13 @@ func (m *manager) VerifyTx(tx *txs.Tx) error { } feeCalculator := state.PickFeeCalculator(m.txExecutorBackend.Config, stateDiff) - return tx.Unsigned.Visit(&executor.StandardTxExecutor{ - Backend: m.txExecutorBackend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: tx, - }) + _, _, _, err = executor.StandardTx( + m.txExecutorBackend, + feeCalculator, + tx, + stateDiff, + ) + return err } func (m *manager) VerifyUniqueInputs(blkID ids.ID, inputs set.Set[ids.ID]) error { diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index dbff557ce263..80e925dbc192 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -517,30 +517,30 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calcula atomicRequests = make(map[ids.ID]*atomic.Requests) ) for _, tx := range txs { - txExecutor := executor.StandardTxExecutor{ - Backend: v.txExecutorBackend, - State: diff, - FeeCalculator: feeCalculator, - Tx: tx, - } - if err := tx.Unsigned.Visit(&txExecutor); err != nil { + txInputs, txAtomicRequests, onAccept, err := executor.StandardTx( + v.txExecutorBackend, + feeCalculator, + tx, + diff, + ) + if err != nil { txID := tx.ID() v.MarkDropped(txID, err) // cache tx as dropped return nil, nil, nil, err } // ensure it doesn't overlap with current input batch - if inputs.Overlaps(txExecutor.Inputs) { + if inputs.Overlaps(txInputs) { return nil, nil, nil, ErrConflictingBlockTxs } // Add UTXOs to batch - inputs.Union(txExecutor.Inputs) + inputs.Union(txInputs) diff.AddTx(tx, status.Committed) - if txExecutor.OnAccept != nil { - funcs = append(funcs, txExecutor.OnAccept) + if onAccept != nil { + funcs = append(funcs, onAccept) } - for chainID, txRequests := range txExecutor.AtomicRequests { + for chainID, txRequests := range txAtomicRequests { // Add/merge in the atomic requests represented by [tx] chainRequests, exists := atomicRequests[chainID] if !exists { diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 7f7b21671620..7a9f6b7086ab 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -14,6 +14,8 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" @@ -41,7 +43,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool/mempoolmock" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/txsmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -51,24 +52,33 @@ import ( ) type testVerifierConfig struct { + DB database.Database Upgrades upgrade.Config + Context *snow.Context } func newTestVerifier(t testing.TB, c testVerifierConfig) *verifier { require := require.New(t) + if c.DB == nil { + c.DB = memdb.New() + } if c.Upgrades == (upgrade.Config{}) { c.Upgrades = upgradetest.GetConfig(upgradetest.Latest) } + if c.Context == nil { + c.Context = snowtest.Context(t, constants.PlatformChainID) + } mempool, err := mempool.New("", prometheus.NewRegistry(), nil) require.NoError(err) var ( state = statetest.New(t, statetest.Config{ + DB: c.DB, Upgrades: c.Upgrades, + Context: c.Context, }) - ctx = snowtest.Context(t, constants.PlatformChainID) clock = &mockable.Clock{} fx = &secp256k1fx.Fx{} ) @@ -83,7 +93,7 @@ func newTestVerifier(t testing.TB, c testVerifierConfig) *verifier { lastAccepted: state.GetLastAccepted(), blkIDToState: make(map[ids.ID]*blockState), state: state, - ctx: ctx, + ctx: c.Context, }, txExecutorBackend: &executor.Backend{ Config: &config.Config{ @@ -94,11 +104,11 @@ func newTestVerifier(t testing.TB, c testVerifierConfig) *verifier { SybilProtectionEnabled: true, UpgradeConfig: c.Upgrades, }, - Ctx: ctx, + Ctx: c.Context, Clk: clock, Fx: fx, FlowChecker: utxo.NewVerifier( - ctx, + c.Context, clock, fx, ), @@ -274,101 +284,148 @@ func TestVerifierVisitAtomicBlock(t *testing.T) { func TestVerifierVisitStandardBlock(t *testing.T) { require := require.New(t) - ctrl := gomock.NewController(t) - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentState := state.NewMockDiff(ctrl) + var ( + ctx = snowtest.Context(t, constants.PlatformChainID) - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, + baseDB = memdb.New() + stateDB = prefixdb.New([]byte{0}, baseDB) + amDB = prefixdb.New([]byte{1}, baseDB) + + am = atomic.NewMemory(amDB) + sm = am.NewSharedMemory(ctx.ChainID) + xChainSM = am.NewSharedMemory(ctx.XChainID) + + owner = secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{genesis.EWOQKey.Address()}, + } + utxo = &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: ids.GenerateTestID(), + OutputIndex: 1, }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: units.Avax, + OutputOwners: owner, }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + ) + + inputID := utxo.InputID() + utxoBytes, err := txs.Codec.Marshal(txs.CodecVersion, utxo) + require.NoError(err) - blkTx := txsmock.NewUnsignedTx(ctrl) - atomicRequests := map[ids.ID]*atomic.Requests{ - ids.GenerateTestID(): { - RemoveRequests: [][]byte{{1}, {2}}, + require.NoError(xChainSM.Apply(map[ids.ID]*atomic.Requests{ + ctx.ChainID: { PutRequests: []*atomic.Element{ { - Key: []byte{3}, - Value: []byte{4}, - Traits: [][]byte{{5}, {6}}, + Key: inputID[:], + Value: utxoBytes, + Traits: [][]byte{ + genesis.EWOQKey.Address().Bytes(), + }, }, }, }, - } - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.StandardTxExecutor{})).DoAndReturn( - func(e *executor.StandardTxExecutor) error { - e.OnAccept = func() {} - e.Inputs = set.Set[ids.ID]{} - e.AtomicRequests = atomicRequests - return nil - }, - ).Times(1) - - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotStandardBlock( - parentID, - 2, /*height*/ - []*txs.Tx{ - { - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, - }, + })) + + ctx.SharedMemory = sm + + var ( + verifier = newTestVerifier(t, testVerifierConfig{ + DB: stateDB, + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhase5), + Context: ctx, + }) + wallet = txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + verifier.state, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + []ids.ID{ctx.XChainID}, // Read the UTXO to import + ) + initialTimestamp = verifier.state.GetTimestamp() + ) + + // Build the transaction that will be executed. + tx, err := wallet.IssueImportTx( + verifier.ctx.XChainID, + &owner, ) require.NoError(err) - apricotBlk.Transactions[0].Unsigned = blkTx - // Set expectations for dependencies. - timestamp := time.Now() - parentState.EXPECT().GetTimestamp().Return(timestamp).Times(1) - parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) - parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) - parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) - parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) - parentState.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(&iterator.Empty[state.SubnetOnlyValidator]{}, nil).Times(1) - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - mempool.EXPECT().Remove(apricotBlk.Txs()).Times(1) + // Verify that the transaction is only consuming the imported UTXO. + require.Len(tx.InputIDs(), 1) - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) - // Assert expected state. - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(set.Set[ids.ID]{}, gotBlkState.inputs) - require.Equal(timestamp, gotBlkState.timestamp) + firstBlock, err := block.NewApricotStandardBlock( + lastAcceptedID, + lastAccepted.Height()+1, + []*txs.Tx{tx}, + ) + require.NoError(err) - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + // Execute the block. + require.NoError(firstBlock.Visit(verifier)) + + // Verify that the block's execution was recorded as expected. + firstBlockID := firstBlock.ID() + { + require.Contains(verifier.blkIDToState, firstBlockID) + atomicBlockState := verifier.blkIDToState[firstBlockID] + onAccept := atomicBlockState.onAcceptState + require.NotNil(onAccept) + + txID := tx.ID() + acceptedTx, acceptedStatus, err := onAccept.GetTx(txID) + require.NoError(err) + require.Equal(tx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) + + require.Equal( + &blockState{ + statelessBlock: firstBlock, + + onAcceptState: onAccept, + + inputs: tx.InputIDs(), + timestamp: initialTimestamp, + atomicRequests: map[ids.ID]*atomic.Requests{ + verifier.ctx.XChainID: { + RemoveRequests: [][]byte{ + inputID[:], + }, + }, + }, + verifiedHeights: set.Of(uint64(0)), + }, + atomicBlockState, + ) + } + + // Verify that the import transaction can no be replayed. + { + secondBlock, err := block.NewApricotStandardBlock( + firstBlockID, + firstBlock.Height()+1, + []*txs.Tx{tx}, // Replay the prior transaction + ) + require.NoError(err) + + err = secondBlock.Visit(verifier) + require.ErrorIs(err, errConflictingParentTxs) + + // Verify that the block's execution was not recorded. + require.NotContains(verifier.blkIDToState, secondBlock.ID()) + } } func TestVerifierVisitCommitBlock(t *testing.T) { @@ -744,104 +801,6 @@ func TestBanffCommitBlockTimestampChecks(t *testing.T) { } } -func TestVerifierVisitStandardBlockWithDuplicateInputs(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - - grandParentID := ids.GenerateTestID() - grandParentStatelessBlk := block.NewMockBlock(ctrl) - grandParentState := state.NewMockDiff(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentState := state.NewMockDiff(ctrl) - atomicInputs := set.Of(ids.GenerateTestID()) - - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - grandParentID: { - statelessBlock: grandParentStatelessBlk, - onAcceptState: grandParentState, - inputs: atomicInputs, - }, - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - verifier := &verifier{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), - }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } - - blkTx := txsmock.NewUnsignedTx(ctrl) - atomicRequests := map[ids.ID]*atomic.Requests{ - ids.GenerateTestID(): { - RemoveRequests: [][]byte{{1}, {2}}, - PutRequests: []*atomic.Element{ - { - Key: []byte{3}, - Value: []byte{4}, - Traits: [][]byte{{5}, {6}}, - }, - }, - }, - } - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.StandardTxExecutor{})).DoAndReturn( - func(e *executor.StandardTxExecutor) error { - e.OnAccept = func() {} - e.Inputs = atomicInputs - e.AtomicRequests = atomicRequests - return nil - }, - ).Times(1) - - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - blk, err := block.NewApricotStandardBlock( - parentID, - 2, - []*txs.Tx{ - { - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, - }, - ) - require.NoError(err) - blk.Transactions[0].Unsigned = blkTx - - // Set expectations for dependencies. - timestamp := time.Now() - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - parentState.EXPECT().GetTimestamp().Return(timestamp).Times(1) - parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) - parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) - parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) - parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) - parentStatelessBlk.EXPECT().Parent().Return(grandParentID).Times(1) - - err = verifier.ApricotStandardBlock(blk) - require.ErrorIs(err, errConflictingParentTxs) -} - func TestVerifierVisitApricotStandardBlockWithProposalBlockParent(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 90eb1725e057..cc511109e68e 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -112,15 +112,15 @@ func (*atomicTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } -func (e *atomicTxExecutor) ImportTx(tx *txs.ImportTx) error { - return e.atomicTx(tx) +func (e *atomicTxExecutor) ImportTx(*txs.ImportTx) error { + return e.atomicTx() } -func (e *atomicTxExecutor) ExportTx(tx *txs.ExportTx) error { - return e.atomicTx(tx) +func (e *atomicTxExecutor) ExportTx(*txs.ExportTx) error { + return e.atomicTx() } -func (e *atomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { +func (e *atomicTxExecutor) atomicTx() error { onAccept, err := state.NewDiff( e.parentID, e.stateVersions, @@ -130,14 +130,13 @@ func (e *atomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { } e.onAccept = onAccept - executor := StandardTxExecutor{ - Backend: e.backend, - State: e.onAccept, - FeeCalculator: e.feeCalculator, - Tx: e.tx, - } - err = tx.Visit(&executor) - e.inputs = executor.Inputs - e.atomicRequests = executor.AtomicRequests + inputs, atomicRequests, _, err := StandardTx( + e.backend, + e.feeCalculator, + e.tx, + e.onAccept, + ) + e.inputs = inputs + e.atomicRequests = atomicRequests return err } diff --git a/vms/platformvm/txs/executor/create_chain_test.go b/vms/platformvm/txs/executor/create_chain_test.go index 7f9919e4a8f5..f271b81dbebf 100644 --- a/vms/platformvm/txs/executor/create_chain_test.go +++ b/vms/platformvm/txs/executor/create_chain_test.go @@ -52,13 +52,12 @@ func TestCreateChainTxInsufficientControlSigs(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -96,13 +95,12 @@ func TestCreateChainTxWrongControlSig(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -137,13 +135,12 @@ func TestCreateChainTxNoSuchSubnet(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, builderDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, database.ErrNotFound) } @@ -175,13 +172,13 @@ func TestCreateChainTxValid(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, builderDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) + require.NoError(err) } func TestCreateChainTxAP3FeeChange(t *testing.T) { @@ -248,13 +245,12 @@ func TestCreateChainTxAP3FeeChange(t *testing.T) { stateDiff.SetTimestamp(test.time) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, test.expectedError) }) } @@ -296,12 +292,11 @@ func TestEtnaCreateChainTxInvalidWithManagedSubnet(t *testing.T) { ) feeCalculator := state.PickFeeCalculator(env.config, builderDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errIsImmutable) } diff --git a/vms/platformvm/txs/executor/create_subnet_test.go b/vms/platformvm/txs/executor/create_subnet_test.go index 0290d1811401..482e5175c945 100644 --- a/vms/platformvm/txs/executor/create_subnet_test.go +++ b/vms/platformvm/txs/executor/create_subnet_test.go @@ -79,13 +79,12 @@ func TestCreateSubnetTxAP3FeeChange(t *testing.T) { stateDiff.SetTimestamp(test.time) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, test.expectedErr) }) } diff --git a/vms/platformvm/txs/executor/export_test.go b/vms/platformvm/txs/executor/export_test.go index 34b1e8046454..3ecbc2e590bb 100644 --- a/vms/platformvm/txs/executor/export_test.go +++ b/vms/platformvm/txs/executor/export_test.go @@ -66,13 +66,13 @@ func TestNewExportTx(t *testing.T) { stateDiff.SetTimestamp(tt.timestamp) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - verifier := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&verifier)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) + require.NoError(err) }) } } diff --git a/vms/platformvm/txs/executor/helpers_test.go b/vms/platformvm/txs/executor/helpers_test.go index 129e0d351f38..bf6a6f7214c1 100644 --- a/vms/platformvm/txs/executor/helpers_test.go +++ b/vms/platformvm/txs/executor/helpers_test.go @@ -220,13 +220,13 @@ func addSubnet(t *testing.T, env *environment) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, env.state) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: testSubnet1, - } - require.NoError(testSubnet1.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + testSubnet1, + stateDiff, + ) + require.NoError(err) stateDiff.AddTx(testSubnet1, status.Committed) require.NoError(stateDiff.Apply(env.state)) diff --git a/vms/platformvm/txs/executor/import_test.go b/vms/platformvm/txs/executor/import_test.go index d3c48f17e186..f25a831bb3d4 100644 --- a/vms/platformvm/txs/executor/import_test.go +++ b/vms/platformvm/txs/executor/import_test.go @@ -156,13 +156,13 @@ func TestNewImportTx(t *testing.T) { stateDiff.SetTimestamp(tt.timestamp) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - verifier := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&verifier)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) + require.NoError(err) }) } } diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 19fd07aac6d4..b2b6fdc825da 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -27,7 +27,7 @@ import ( ) var ( - _ txs.Visitor = (*StandardTxExecutor)(nil) + _ txs.Visitor = (*standardTxExecutor)(nil) errEmptyNodeID = errors.New("validator nodeID cannot be empty") errMaxStakeDurationTooLarge = errors.New("max stake duration must be less than or equal to the global max stake duration") @@ -37,37 +37,56 @@ var ( errMaxNumActiveValidators = errors.New("already at the max number of active validators") ) -type StandardTxExecutor struct { +func StandardTx( + backend *Backend, + feeCalculator fee.Calculator, + tx *txs.Tx, + state state.Diff, +) (set.Set[ids.ID], map[ids.ID]*atomic.Requests, func(), error) { + standardExecutor := standardTxExecutor{ + backend: backend, + feeCalculator: feeCalculator, + tx: tx, + state: state, + } + if err := tx.Unsigned.Visit(&standardExecutor); err != nil { + txID := tx.ID() + return nil, nil, nil, fmt.Errorf("standard tx %s failed execution: %w", txID, err) + } + return standardExecutor.inputs, standardExecutor.atomicRequests, standardExecutor.onAccept, nil +} + +type standardTxExecutor struct { // inputs, to be filled before visitor methods are called - *Backend - State state.Diff // state is expected to be modified - FeeCalculator fee.Calculator - Tx *txs.Tx + backend *Backend + state state.Diff // state is expected to be modified + feeCalculator fee.Calculator + tx *txs.Tx // outputs of visitor execution - OnAccept func() // may be nil - Inputs set.Set[ids.ID] - AtomicRequests map[ids.ID]*atomic.Requests // may be nil + onAccept func() // may be nil + inputs set.Set[ids.ID] + atomicRequests map[ids.ID]*atomic.Requests // may be nil } -func (*StandardTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { +func (*standardTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { return ErrWrongTxType } -func (*StandardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { +func (*standardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } -func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { +func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { if tx.Validator.NodeID == ids.EmptyNodeID { return errEmptyNodeID } if _, err := verifyAddValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -77,12 +96,12 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) - if e.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.Ctx.NodeID { - e.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", zap.String("reason", "primary network is not being fully synced"), zap.Stringer("txID", txID), zap.String("txType", "addValidator"), @@ -92,12 +111,12 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { return nil } -func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { +func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { if err := verifyAddSubnetValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -107,18 +126,18 @@ func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { +func (e *standardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { if _, err := verifyAddDelegatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -128,146 +147,146 @@ func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) CreateChainTx(tx *txs.CreateChainTx) error { - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { +func (e *standardTxExecutor) CreateChainTx(tx *txs.CreateChainTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } - baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.SubnetID, tx.SubnetAuth) + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.SubnetID, tx.SubnetAuth) if err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, baseTxCreds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Add the new chain to the database - e.State.AddChain(e.Tx) + e.state.AddChain(e.tx) // If this proposal is committed and this node is a member of the subnet // that validates the blockchain, create the blockchain - e.OnAccept = func() { - e.Config.CreateChain(txID, tx) + e.onAccept = func() { + e.backend.Config.CreateChain(txID, tx) } return nil } -func (e *StandardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { +func (e *standardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { // Make sure this transaction is well formed. - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Add the new subnet to the database - e.State.AddSubnet(txID) - e.State.SetSubnetOwner(txID, tx.Owner) + e.state.AddSubnet(txID) + e.state.SetSubnetOwner(txID, tx.Owner) return nil } -func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { +func (e *standardTxExecutor) ImportTx(tx *txs.ImportTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } - e.Inputs = set.NewSet[ids.ID](len(tx.ImportedInputs)) + e.inputs = set.NewSet[ids.ID](len(tx.ImportedInputs)) utxoIDs := make([][]byte, len(tx.ImportedInputs)) for i, in := range tx.ImportedInputs { utxoID := in.UTXOID.InputID() - e.Inputs.Add(utxoID) + e.inputs.Add(utxoID) utxoIDs[i] = utxoID[:] } // Skip verification of the shared memory inputs if the other primary // network chains are not guaranteed to be up-to-date. - if e.Bootstrapped.Get() && !e.Config.PartialSyncPrimaryNetwork { - if err := verify.SameSubnet(context.TODO(), e.Ctx, tx.SourceChain); err != nil { + if e.backend.Bootstrapped.Get() && !e.backend.Config.PartialSyncPrimaryNetwork { + if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.SourceChain); err != nil { return err } - allUTXOBytes, err := e.Ctx.SharedMemory.Get(tx.SourceChain, utxoIDs) + allUTXOBytes, err := e.backend.Ctx.SharedMemory.Get(tx.SourceChain, utxoIDs) if err != nil { return fmt.Errorf("failed to get shared memory: %w", err) } utxos := make([]*avax.UTXO, len(tx.Ins)+len(tx.ImportedInputs)) for index, input := range tx.Ins { - utxo, err := e.State.GetUTXO(input.InputID()) + utxo, err := e.state.GetUTXO(input.InputID()) if err != nil { return fmt.Errorf("failed to get UTXO %s: %w", &input.UTXOID, err) } @@ -286,35 +305,35 @@ func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { copy(ins[len(tx.Ins):], tx.ImportedInputs) // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpendUTXOs( + if err := e.backend.FlowChecker.VerifySpendUTXOs( tx, utxos, ins, tx.Outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Note: We apply atomic requests even if we are not verifying atomic // requests to ensure the shared state will be correct if we later start // verifying the requests. - e.AtomicRequests = map[ids.ID]*atomic.Requests{ + e.atomicRequests = map[ids.ID]*atomic.Requests{ tx.SourceChain: { RemoveRequests: utxoIDs, }, @@ -322,14 +341,14 @@ func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { return nil } -func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { +func (e *standardTxExecutor) ExportTx(tx *txs.ExportTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err @@ -339,36 +358,36 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { copy(outs, tx.Outs) copy(outs[len(tx.Outs):], tx.ExportedOutputs) - if e.Bootstrapped.Get() { - if err := verify.SameSubnet(context.TODO(), e.Ctx, tx.DestinationChain); err != nil { + if e.backend.Bootstrapped.Get() { + if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.DestinationChain); err != nil { return err } } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return fmt.Errorf("failed verifySpend: %w", err) } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Note: We apply atomic requests even if we are not verifying atomic // requests to ensure the shared state will be correct if we later start @@ -399,7 +418,7 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { elems[i] = elem } - e.AtomicRequests = map[ids.ID]*atomic.Requests{ + e.atomicRequests = map[ids.ID]*atomic.Requests{ tx.DestinationChain: { PutRequests: elems, }, @@ -412,12 +431,12 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { // transaction will result in [tx.NodeID] being removed as a validator of // [tx.SubnetID]. // Note: [tx.NodeID] may be either a current or pending validator. -func (e *StandardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { +func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { staker, isCurrentValidator, err := verifyRemoveSubnetValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ) if err != nil { @@ -425,55 +444,55 @@ func (e *StandardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidat } if isCurrentValidator { - e.State.DeleteCurrentValidator(staker) + e.state.DeleteCurrentValidator(staker) } else { - e.State.DeletePendingValidator(staker) + e.state.DeletePendingValidator(staker) } // Invariant: There are no permissioned subnet delegators to remove. - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { - currentTimestamp := e.State.GetTimestamp() - if e.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { +func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { + currentTimestamp := e.state.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { return errTransformSubnetTxPostEtna } - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } - isDurangoActive := e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + isDurangoActive := e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } // Note: math.MaxInt32 * time.Second < math.MaxInt64 - so this can never // overflow. - if time.Duration(tx.MaxStakeDuration)*time.Second > e.Backend.Config.MaxStakeDuration { + if time.Duration(tx.MaxStakeDuration)*time.Second > e.backend.Config.MaxStakeDuration { return errMaxStakeDurationTooLarge } - baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.Subnet, tx.SubnetAuth) + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) if err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } totalRewardAmount := tx.MaximumSupply - tx.InitialSupply - if err := e.Backend.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, baseTxCreds, @@ -481,31 +500,31 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error // entry in this map literal from being overwritten by the // second entry. map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, - tx.AssetID: totalRewardAmount, + e.backend.Ctx.AVAXAssetID: fee, + tx.AssetID: totalRewardAmount, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Transform the new subnet in the database - e.State.AddSubnetTransformation(e.Tx) - e.State.SetCurrentSupply(tx.Subnet, tx.InitialSupply) + e.state.AddSubnetTransformation(e.tx) + e.state.SetCurrentSupply(tx.Subnet, tx.InitialSupply) return nil } -func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { +func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if err := verifyAddPermissionlessValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -515,14 +534,14 @@ func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionl return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) - if e.Config.PartialSyncPrimaryNetwork && + if e.backend.Config.PartialSyncPrimaryNetwork && tx.Subnet == constants.PrimaryNetworkID && - tx.Validator.NodeID == e.Ctx.NodeID { - e.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", zap.String("reason", "primary network is not being fully synced"), zap.Stringer("txID", txID), zap.String("txType", "addPermissionlessValidator"), @@ -533,12 +552,12 @@ func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionl return nil } -func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { +func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { if err := verifyAddPermissionlessDelegatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -548,9 +567,9 @@ func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionl return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } @@ -558,37 +577,37 @@ func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionl // [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. // This transaction will result in the ownership of [tx.Subnet] being transferred // to [tx.Owner]. -func (e *StandardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { +func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { err := verifyTransferSubnetOwnershipTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ) if err != nil { return err } - e.State.SetSubnetOwner(tx.Subnet, tx.Owner) + e.state.SetSubnetOwner(tx.Subnet, tx.Owner) - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) BaseTx(tx *txs.BaseTx) error { +func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { var ( - currentTimestamp = e.State.GetTimestamp() - upgrades = e.Backend.Config.UpgradeConfig + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig ) if !upgrades.IsDurangoActivated(currentTimestamp) { return ErrDurangoUpgradeNotActive } // Verify the tx is well-formed - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } @@ -597,41 +616,41 @@ func (e *StandardTxExecutor) BaseTx(tx *txs.BaseTx) error { } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { +func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { var ( - currentTimestamp = e.State.GetTimestamp() - upgrades = e.Backend.Config.UpgradeConfig + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig ) if !upgrades.IsEtnaActivated(currentTimestamp) { return errEtnaUpgradeNotActive } - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } @@ -639,20 +658,20 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return err } - baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.Subnet, tx.SubnetAuth) + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) if err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } var ( startTime = uint64(currentTimestamp.Unix()) - currentFees = e.State.GetAccruedFees() + currentFees = e.state.GetAccruedFees() subnetConversionData = message.SubnetConversionData{ SubnetID: tx.Subnet, ManagerChainID: tx.ChainID, @@ -689,7 +708,7 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { } if vdr.Balance != 0 { // We are attempting to add an active validator - if gas.Gas(e.State.NumActiveSubnetOnlyValidators()) >= e.Backend.Config.ValidatorFeeConfig.Capacity { + if gas.Gas(e.state.NumActiveSubnetOnlyValidators()) >= e.backend.Config.ValidatorFeeConfig.Capacity { return errMaxNumActiveValidators } @@ -704,7 +723,7 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { } } - if err := e.State.PutSubnetOnlyValidator(sov); err != nil { + if err := e.state.PutSubnetOnlyValidator(sov); err != nil { return err } @@ -714,14 +733,14 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { Weight: vdr.Weight, } } - if err := e.Backend.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, baseTxCreds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err @@ -732,14 +751,14 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Track the subnet conversion in the database - e.State.SetSubnetConversion( + e.state.SetSubnetConversion( tx.Subnet, state.SubnetConversion{ ConversionID: conversionID, @@ -751,15 +770,15 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { } // Creates the staker as defined in [stakerTx] and adds it to [e.State]. -func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { +func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( - chainTime = e.State.GetTimestamp() - txID = e.Tx.ID() + chainTime = e.state.GetTimestamp() + txID = e.tx.ID() staker *state.Staker err error ) - if !e.Config.UpgradeConfig.IsDurangoActivated(chainTime) { + if !e.backend.Config.UpgradeConfig.IsDurangoActivated(chainTime) { // Pre-Durango, stakers set a future [StartTime] and are added to the // pending staker set. They are promoted to the current staker set once // the chain time reaches [StartTime]. @@ -775,12 +794,12 @@ func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { var potentialReward uint64 if !stakerTx.CurrentPriority().IsPermissionedValidator() { subnetID := stakerTx.SubnetID() - currentSupply, err := e.State.GetCurrentSupply(subnetID) + currentSupply, err := e.state.GetCurrentSupply(subnetID) if err != nil { return err } - rewards, err := GetRewardsCalculator(e.Backend, e.State, subnetID) + rewards, err := GetRewardsCalculator(e.backend, e.state, subnetID) if err != nil { return err } @@ -794,7 +813,7 @@ func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { currentSupply, ) - e.State.SetCurrentSupply(subnetID, currentSupply+potentialReward) + e.state.SetCurrentSupply(subnetID, currentSupply+potentialReward) } staker, err = state.NewCurrentStaker(txID, stakerTx, chainTime, potentialReward) @@ -805,17 +824,17 @@ func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { switch priority := staker.Priority; { case priority.IsCurrentValidator(): - if err := e.State.PutCurrentValidator(staker); err != nil { + if err := e.state.PutCurrentValidator(staker); err != nil { return err } case priority.IsCurrentDelegator(): - e.State.PutCurrentDelegator(staker) + e.state.PutCurrentDelegator(staker) case priority.IsPendingValidator(): - if err := e.State.PutPendingValidator(staker); err != nil { + if err := e.state.PutPendingValidator(staker); err != nil { return err } case priority.IsPendingDelegator(): - e.State.PutPendingDelegator(staker) + e.state.PutPendingDelegator(staker) default: return fmt.Errorf("staker %s, unexpected priority %d", staker.TxID, priority) } diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 1080013ed776..8a2e574ccfc5 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -101,13 +101,12 @@ func TestStandardTxExecutorAddValidatorTxEmptyID(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errEmptyNodeID) } } @@ -353,13 +352,12 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { env.config.UpgradeConfig.BanffTime = onAcceptState.GetTimestamp() feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, tt.expectedExecutionErr) }) } @@ -400,13 +398,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -435,13 +432,13 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) + require.NoError(err) } // Add a validator to pending validator set of primary network @@ -488,13 +485,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrNotValidator) } @@ -538,13 +534,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -571,13 +566,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -604,13 +598,13 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) + require.NoError(err) } // Case: Proposed validator start validating at/before current timestamp @@ -639,13 +633,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -707,13 +700,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrDuplicateValidator) } @@ -751,13 +743,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, secp256k1fx.ErrInputIndicesNotSortedUnique) } @@ -791,13 +782,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -829,13 +819,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -877,13 +866,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrDuplicateValidator) } } @@ -925,12 +913,13 @@ func TestEtnaStandardTxExecutorAddSubnetValidator(t *testing.T) { }, ) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errIsImmutable) } @@ -965,13 +954,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -1008,13 +996,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { onAcceptState.AddTx(tx, status.Committed) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -1048,13 +1035,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { onAcceptState.AddTx(tx, status.Committed) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -1089,13 +1075,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { } feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: onAcceptState, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrFlowCheckFailed) } } @@ -1188,14 +1173,13 @@ func TestDurangoDisabledTransactions(t *testing.T) { require.NoError(err) tx := tt.buildTx(t, env) - feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - err = tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - }) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, tt.expectedErr) }) } @@ -1401,12 +1385,13 @@ func TestDurangoMemoField(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - require.NoError(subnetValTx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: subnetValTx, - })) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + subnetValTx, + onAcceptState, + ) + require.NoError(err) tx, err := wallet.IssueRemoveSubnetValidatorTx( primaryValidator.NodeID, @@ -1592,22 +1577,23 @@ func TestDurangoMemoField(t *testing.T) { // Populated memo field should error tx, onAcceptState := tt.setupTest(t, env, []byte{'m', 'e', 'm', 'o'}) - err := tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - }) + _, _, _, err := StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, avax.ErrMemoTooLarge) // Empty memo field should not error tx, onAcceptState = tt.setupTest(t, env, []byte{}) - require.NoError(tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - })) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) + require.NoError(err) }) } } @@ -1623,15 +1609,16 @@ func TestEtnaDisabledTransactions(t *testing.T) { onAcceptState, err := state.NewDiff(env.state.GetLastAccepted(), env) require.NoError(err) + feeCalculator := state.PickFeeCalculator(env.config, env.state) tx := &txs.Tx{ Unsigned: &txs.TransformSubnetTx{}, } - - err = tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, - }) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errTransformSubnetTxPostEtna) } @@ -1735,14 +1722,14 @@ func newValidRemoveSubnetValidatorTxVerifyEnv(t *testing.T, ctrl *gomock.Control func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { type test struct { name string - newExecutor func(*gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) + newExecutor func(*gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) expectedErr error } tests := []test{ { name: "valid tx", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) // Set dependency expectations. @@ -1774,26 +1761,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Etna, env.latestForkTime), } feeCalculator := state.NewStaticFeeCalculator(cfg, env.state.GetTimestamp()) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: nil, }, { name: "tx fails syntactic verification", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) // Setting the subnet ID to the Primary Network ID makes the tx fail syntactic verification env.tx.Unsigned.(*txs.RemoveSubnetValidatorTx).Subnet = constants.PrimaryNetworkID @@ -1804,26 +1790,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: txs.ErrRemovePrimaryNetworkValidator, }, { name: "node isn't a validator of the subnet", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1834,26 +1819,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: ErrNotValidator, }, { name: "validator is permissionless", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) staker := *env.staker @@ -1867,26 +1851,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: ErrRemovePermissionlessValidator, }, { name: "tx has no credentials", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) // Remove credentials env.tx.Creds = nil @@ -1898,26 +1881,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: errWrongNumberOfCredentials, }, { name: "can't find subnet", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1928,26 +1910,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: database.ErrNotFound, }, { name: "no permission to remove validator", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1960,26 +1941,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: errUnauthorizedSubnetModification, }, { name: "flow checker failed", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1995,19 +1975,18 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: ErrFlowCheckFailed, @@ -2137,14 +2116,14 @@ func newValidTransformSubnetTxVerifyEnv(t *testing.T, ctrl *gomock.Controller) t func TestStandardExecutorTransformSubnetTx(t *testing.T) { type test struct { name string - newExecutor func(*gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) + newExecutor func(*gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) err error } tests := []test{ { name: "tx fails syntactic verification", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Setting the tx to nil makes the tx fail syntactic verification env.tx.Unsigned = (*txs.TransformSubnetTx)(nil) @@ -2155,26 +2134,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: txs.ErrNilTx, }, { name: "max stake duration too large", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) env.unsignedTx.MaxStakeDuration = math.MaxUint32 env.state = state.NewMockDiff(ctrl) @@ -2184,26 +2162,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: errMaxStakeDurationTooLarge, }, { name: "fail subnet authorization", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Remove credentials env.tx.Creds = nil @@ -2216,26 +2193,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: errWrongNumberOfCredentials, }, { name: "flow checker failed", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) subnetOwner := fxmock.NewOwner(ctrl) @@ -2257,26 +2233,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: ErrFlowCheckFailed, }, { name: "invalid after subnet conversion", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Set dependency expectations. @@ -2299,26 +2274,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { MaxStakeDuration: math.MaxInt64, } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: errIsImmutable, }, { name: "valid tx", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Set dependency expectations. @@ -2345,19 +2319,18 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: nil, @@ -2419,18 +2392,19 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { diff, err := state.NewDiffOn(baseState) require.NoError(t, err) - require.NoError(t, createSubnetTx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &Backend{ + _, _, _, err = StandardTx( + &Backend{ Config: defaultConfig, Bootstrapped: utils.NewAtomic(true), Fx: fx, FlowChecker: flowChecker, Ctx: ctx, }, - FeeCalculator: state.PickFeeCalculator(defaultConfig, baseState), - Tx: createSubnetTx, - State: diff, - })) + state.PickFeeCalculator(defaultConfig, baseState), + createSubnetTx, + diff, + ) + require.NoError(t, err) require.NoError(t, diff.Apply(baseState)) require.NoError(t, baseState.Commit()) @@ -2441,13 +2415,13 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { tests := []struct { name string builderOptions []common.Option - updateExecutor func(executor *StandardTxExecutor) error + updateExecutor func(executor *standardTxExecutor) error expectedErr error }{ { name: "invalid prior to E-Upgrade", - updateExecutor: func(e *StandardTxExecutor) error { - e.Backend.Config = &config.Config{ + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ UpgradeConfig: upgradetest.GetConfig(upgradetest.Durango), } return nil @@ -2456,8 +2430,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "tx fails syntactic verification", - updateExecutor: func(e *StandardTxExecutor) error { - e.Backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) return nil }, expectedErr: avax.ErrWrongChainID, @@ -2471,8 +2445,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "fail subnet authorization", - updateExecutor: func(e *StandardTxExecutor) error { - e.State.SetSubnetOwner(subnetID, &secp256k1fx.OutputOwners{ + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetOwner(subnetID, &secp256k1fx.OutputOwners{ Threshold: 1, Addrs: []ids.ShortID{ ids.GenerateTestShortID(), @@ -2484,8 +2458,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid if subnet is transformed", - updateExecutor: func(e *StandardTxExecutor) error { - e.State.AddSubnetTransformation(&txs.Tx{Unsigned: &txs.TransformSubnetTx{ + updateExecutor: func(e *standardTxExecutor) error { + e.state.AddSubnetTransformation(&txs.Tx{Unsigned: &txs.TransformSubnetTx{ Subnet: subnetID, }}) return nil @@ -2494,8 +2468,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid if subnet is converted", - updateExecutor: func(e *StandardTxExecutor) error { - e.State.SetSubnetConversion( + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion( subnetID, state.SubnetConversion{ ConversionID: ids.GenerateTestID(), @@ -2509,16 +2483,16 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid fee calculation", - updateExecutor: func(e *StandardTxExecutor) error { - e.FeeCalculator = txfee.NewStaticCalculator(e.Config.StaticFeeConfig) + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewStaticCalculator(e.backend.Config.StaticFeeConfig) return nil }, expectedErr: txfee.ErrUnsupportedTx, }, { name: "too many active validators", - updateExecutor: func(e *StandardTxExecutor) error { - e.Backend.Config = &config.Config{ + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, ValidatorFeeConfig: validatorfee.Config{ Capacity: 0, @@ -2534,8 +2508,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid subnet only validator", - updateExecutor: func(e *StandardTxExecutor) error { - return e.State.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ + updateExecutor: func(e *standardTxExecutor) error { + return e.state.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ ValidationID: ids.GenerateTestID(), SubnetID: subnetID, NodeID: nodeID, @@ -2546,9 +2520,9 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "insufficient fee", - updateExecutor: func(e *StandardTxExecutor) error { - e.FeeCalculator = txfee.NewDynamicCalculator( - e.Config.DynamicFeeConfig.Weights, + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewDynamicCalculator( + e.backend.Config.DynamicFeeConfig.Weights, 100*genesis.LocalParams.DynamicFeeConfig.MinPrice, ) return nil @@ -2607,17 +2581,17 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { diff, err := state.NewDiffOn(baseState) require.NoError(err) - executor := &StandardTxExecutor{ - Backend: &Backend{ + executor := &standardTxExecutor{ + backend: &Backend{ Config: defaultConfig, Bootstrapped: utils.NewAtomic(true), Fx: fx, FlowChecker: flowChecker, Ctx: ctx, }, - FeeCalculator: state.PickFeeCalculator(defaultConfig, baseState), - Tx: convertSubnetTx, - State: diff, + feeCalculator: state.PickFeeCalculator(defaultConfig, baseState), + tx: convertSubnetTx, + state: diff, } if test.updateExecutor != nil { require.NoError(test.updateExecutor(executor)) From 3a01924c0d92c8f7eb599ce38e195d8fea67a331 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:06:08 -0500 Subject: [PATCH 127/184] nit --- vms/platformvm/block/executor/verifier_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 7a9f6b7086ab..9dfc631a4915 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -411,7 +411,7 @@ func TestVerifierVisitStandardBlock(t *testing.T) { ) } - // Verify that the import transaction can no be replayed. + // Verify that the import transaction can not be replayed. { secondBlock, err := block.NewApricotStandardBlock( firstBlockID, From d7e00ddf76346253c824dfa3511acd75ddf3033c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:08:53 -0500 Subject: [PATCH 128/184] nit --- vms/platformvm/txs/executor/atomic_tx_executor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index cc511109e68e..0545bc476d8b 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -128,14 +128,14 @@ func (e *atomicTxExecutor) atomicTx() error { if err != nil { return err } - e.onAccept = onAccept inputs, atomicRequests, _, err := StandardTx( e.backend, e.feeCalculator, e.tx, - e.onAccept, + onAccept, ) + e.onAccept = onAccept e.inputs = inputs e.atomicRequests = atomicRequests return err From 56b00498c38345bc8cc6876e03835a5dc301df7d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:09:29 -0500 Subject: [PATCH 129/184] nit --- vms/platformvm/txs/executor/atomic_tx_executor.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 0545bc476d8b..a4535a1d1494 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -129,14 +129,12 @@ func (e *atomicTxExecutor) atomicTx() error { return err } - inputs, atomicRequests, _, err := StandardTx( + e.onAccept = onAccept + e.inputs, e.atomicRequests, _, err = StandardTx( e.backend, e.feeCalculator, e.tx, onAccept, ) - e.onAccept = onAccept - e.inputs = inputs - e.atomicRequests = atomicRequests return err } From 3540203f62e2fe7a8e643b4469fdb0590a213be8 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:10:28 -0500 Subject: [PATCH 130/184] nit --- vms/platformvm/block/builder/builder.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vms/platformvm/block/builder/builder.go b/vms/platformvm/block/builder/builder.go index b57cc9ecb4e5..40ce8a3633c7 100644 --- a/vms/platformvm/block/builder/builder.go +++ b/vms/platformvm/block/builder/builder.go @@ -533,8 +533,7 @@ func executeTx( mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs) return false, nil } - err = manager.VerifyUniqueInputs(parentID, txInputs) - if err != nil { + if err := manager.VerifyUniqueInputs(parentID, txInputs); err != nil { txID := tx.ID() mempool.MarkDropped(txID, err) return false, nil From 1bf8709bca076e1e2c93aea199b5699bed42736b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:14:38 -0500 Subject: [PATCH 131/184] nit --- vms/platformvm/txs/executor/atomic_tx_executor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index a4535a1d1494..1977608d09c1 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -20,7 +20,7 @@ var _ txs.Visitor = (*atomicTxExecutor)(nil) // modifications. // // This is only used to execute atomic transactions pre-AP5. After AP5 the -// execution was moved to be performed during standard transaction execution. +// execution was moved to [StandardTx]. func AtomicTx( backend *Backend, feeCalculator fee.Calculator, From 018b0aabaa83b7497952bcb9547fe67e5322dbf5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:22:46 -0500 Subject: [PATCH 132/184] Comment --- vms/platformvm/txs/executor/standard_tx_executor.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index b2b6fdc825da..3ccc059bfa8e 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -37,6 +37,17 @@ var ( errMaxNumActiveValidators = errors.New("already at the max number of active validators") ) +// StandardTx executes the standard transaction [tx]. +// +// [state] is modified to represent the state of the chain after the execution +// of [tx]. +// +// Returns: +// - The IDs of any import UTXOs consumed. +// - The, potentially nil, atomic requests that should be performed against +// shared memory when this transaction is accepted. +// - A, potentially nil, function that should be called when this transaction +// is accepted. func StandardTx( backend *Backend, feeCalculator fee.Calculator, From bd6e10b5f2a668c5235f6543f75f62e6758835e9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:31:25 -0500 Subject: [PATCH 133/184] Remove P-chain txsmock package --- scripts/mocks.mockgen.source.txt | 1 - .../block/executor/verifier_test.go | 637 +++++++++--------- vms/platformvm/txs/txsmock/unsigned_tx.go | 138 ---- 3 files changed, 301 insertions(+), 475 deletions(-) delete mode 100644 vms/platformvm/txs/txsmock/unsigned_tx.go diff --git a/scripts/mocks.mockgen.source.txt b/scripts/mocks.mockgen.source.txt index 93f4c2fb5127..10cc5a678ebf 100644 --- a/scripts/mocks.mockgen.source.txt +++ b/scripts/mocks.mockgen.source.txt @@ -10,5 +10,4 @@ vms/platformvm/signer/signer.go==Signer=vms/platformvm/signer/signermock/signer. vms/platformvm/state/diff.go==MockDiff=vms/platformvm/state/mock_diff.go vms/platformvm/state/state.go=Chain=MockState=vms/platformvm/state/mock_state.go vms/platformvm/state/state.go=State=MockChain=vms/platformvm/state/mock_chain.go -vms/platformvm/txs/unsigned_tx.go==UnsignedTx=vms/platformvm/txs/txsmock/unsigned_tx.go vms/proposervm/block.go=Block=MockPostForkBlock=vms/proposervm/mock_post_fork_block.go diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 0a5d9e3c662f..9dfc631a4915 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -14,10 +14,13 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/upgrade" "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" @@ -40,7 +43,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool/mempoolmock" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/txsmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -49,17 +51,36 @@ import ( validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) -func newTestVerifier(t testing.TB, s state.State) *verifier { +type testVerifierConfig struct { + DB database.Database + Upgrades upgrade.Config + Context *snow.Context +} + +func newTestVerifier(t testing.TB, c testVerifierConfig) *verifier { require := require.New(t) + if c.DB == nil { + c.DB = memdb.New() + } + if c.Upgrades == (upgrade.Config{}) { + c.Upgrades = upgradetest.GetConfig(upgradetest.Latest) + } + if c.Context == nil { + c.Context = snowtest.Context(t, constants.PlatformChainID) + } + mempool, err := mempool.New("", prometheus.NewRegistry(), nil) require.NoError(err) var ( - upgrades = upgradetest.GetConfig(upgradetest.Latest) - ctx = snowtest.Context(t, constants.PlatformChainID) - clock = &mockable.Clock{} - fx = &secp256k1fx.Fx{} + state = statetest.New(t, statetest.Config{ + DB: c.DB, + Upgrades: c.Upgrades, + Context: c.Context, + }) + clock = &mockable.Clock{} + fx = &secp256k1fx.Fx{} ) require.NoError(fx.InitializeVM(&secp256k1fx.TestVM{ Clk: *clock, @@ -69,10 +90,10 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { return &verifier{ backend: &backend{ Mempool: mempool, - lastAccepted: s.GetLastAccepted(), + lastAccepted: state.GetLastAccepted(), blkIDToState: make(map[ids.ID]*blockState), - state: s, - ctx: ctx, + state: state, + ctx: c.Context, }, txExecutorBackend: &executor.Backend{ Config: &config.Config{ @@ -81,13 +102,13 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, SybilProtectionEnabled: true, - UpgradeConfig: upgrades, + UpgradeConfig: c.Upgrades, }, - Ctx: ctx, + Ctx: c.Context, Clk: clock, Fx: fx, FlowChecker: utxo.NewVerifier( - ctx, + c.Context, clock, fx, ), @@ -97,271 +118,314 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { } func TestVerifierVisitProposalBlock(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentOnAcceptState := state.NewMockDiff(ctrl) - timestamp := time.Now() - // One call for each of onCommitState and onAbortState. - parentOnAcceptState.EXPECT().GetTimestamp().Return(timestamp).Times(2) - parentOnAcceptState.EXPECT().GetFeeState().Return(gas.State{}).Times(2) - parentOnAcceptState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(2) - parentOnAcceptState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(2) - parentOnAcceptState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(2) - - backend := &backend{ - lastAccepted: parentID, - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentOnAcceptState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + var ( + require = require.New(t) + verifier = newTestVerifier(t, testVerifierConfig{ + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + }) + initialTimestamp = verifier.state.GetTimestamp() + newTimestamp = initialTimestamp.Add(time.Second) + proposalTx = &txs.Tx{ + Unsigned: &txs.AdvanceTimeTx{ + Time: uint64(newTimestamp.Unix()), }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + ) + require.NoError(proposalTx.Initialize(txs.Codec)) - blkTx := txsmock.NewUnsignedTx(ctrl) - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.ProposalTxExecutor{})).Return(nil).Times(1) + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotProposalBlock( - parentID, - 2, - &txs.Tx{ - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, + proposalBlock, err := block.NewApricotProposalBlock( + lastAcceptedID, + lastAccepted.Height()+1, + proposalTx, ) require.NoError(err) - apricotBlk.Tx.Unsigned = blkTx - // Set expectations for dependencies. - tx := apricotBlk.Txs()[0] - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - mempool.EXPECT().Remove([]*txs.Tx{tx}).Times(1) + // Execute the block. + require.NoError(proposalBlock.Visit(verifier)) - // Visit the block - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(timestamp, gotBlkState.timestamp) + // Verify that the block's execution was recorded as expected. + blkID := proposalBlock.ID() + require.Contains(verifier.blkIDToState, blkID) + executedBlockState := verifier.blkIDToState[blkID] + + txID := proposalTx.ID() - // Assert that the expected tx statuses are set. - _, gotStatus, err := gotBlkState.onCommitState.GetTx(tx.ID()) + onCommit := executedBlockState.onCommitState + require.NotNil(onCommit) + acceptedTx, acceptedStatus, err := onCommit.GetTx(txID) require.NoError(err) - require.Equal(status.Committed, gotStatus) + require.Equal(proposalTx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) - _, gotStatus, err = gotBlkState.onAbortState.GetTx(tx.ID()) + onAbort := executedBlockState.onAbortState + require.NotNil(onAbort) + acceptedTx, acceptedStatus, err = onAbort.GetTx(txID) require.NoError(err) - require.Equal(status.Aborted, gotStatus) + require.Equal(proposalTx, acceptedTx) + require.Equal(status.Aborted, acceptedStatus) + + require.Equal( + &blockState{ + proposalBlockState: proposalBlockState{ + onCommitState: onCommit, + onAbortState: onAbort, + }, + statelessBlock: proposalBlock, - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + timestamp: initialTimestamp, + verifiedHeights: set.Of(uint64(0)), + }, + executedBlockState, + ) } func TestVerifierVisitAtomicBlock(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - grandparentID := ids.GenerateTestID() - parentState := state.NewMockDiff(ctrl) - - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + var ( + require = require.New(t) + verifier = newTestVerifier(t, testVerifierConfig{ + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhase4), + }) + wallet = txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + verifier.state, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + nil, // chainIDs + ) + exportedOutput = &avax.TransferableOutput{ + Asset: avax.Asset{ID: verifier.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: units.NanoAvax, + OutputOwners: secp256k1fx.OutputOwners{}, }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + initialTimestamp = verifier.state.GetTimestamp() + ) - onAccept := state.NewMockDiff(ctrl) - blkTx := txsmock.NewUnsignedTx(ctrl) - inputs := set.Of(ids.GenerateTestID()) - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.AtomicTxExecutor{})).DoAndReturn( - func(e *executor.AtomicTxExecutor) error { - e.OnAccept = onAccept - e.Inputs = inputs - return nil + // Build the transaction that will be executed. + atomicTx, err := wallet.IssueExportTx( + verifier.ctx.XChainID, + []*avax.TransferableOutput{ + exportedOutput, }, - ).Times(1) + ) + require.NoError(err) - // We can't serialize [blkTx] because it isn't registered with blocks.Codec. - // Serialize this block with a dummy tx and replace it after creation with - // the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotAtomicBlock( - parentID, - 2, - &txs.Tx{ - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) + + atomicBlock, err := block.NewApricotAtomicBlock( + lastAcceptedID, + lastAccepted.Height()+1, + atomicTx, ) require.NoError(err) - apricotBlk.Tx.Unsigned = blkTx - // Set expectations for dependencies. - timestamp := time.Now() - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - parentStatelessBlk.EXPECT().Parent().Return(grandparentID).Times(1) - mempool.EXPECT().Remove([]*txs.Tx{apricotBlk.Tx}).Times(1) - onAccept.EXPECT().AddTx(apricotBlk.Tx, status.Committed).Times(1) - onAccept.EXPECT().GetTimestamp().Return(timestamp).Times(1) + // Execute the block. + require.NoError(atomicBlock.Visit(verifier)) - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) + // Verify that the block's execution was recorded as expected. + blkID := atomicBlock.ID() + require.Contains(verifier.blkIDToState, blkID) + atomicBlockState := verifier.blkIDToState[blkID] + onAccept := atomicBlockState.onAcceptState + require.NotNil(onAccept) - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(onAccept, gotBlkState.onAcceptState) - require.Equal(inputs, gotBlkState.inputs) - require.Equal(timestamp, gotBlkState.timestamp) + txID := atomicTx.ID() + acceptedTx, acceptedStatus, err := onAccept.GetTx(txID) + require.NoError(err) + require.Equal(atomicTx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + exportedUTXO := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(len(atomicTx.UTXOs())), + }, + Asset: exportedOutput.Asset, + Out: exportedOutput.Out, + } + exportedUTXOID := exportedUTXO.InputID() + exportedUTXOBytes, err := txs.Codec.Marshal(txs.CodecVersion, exportedUTXO) + require.NoError(err) + + require.Equal( + &blockState{ + statelessBlock: atomicBlock, + + onAcceptState: onAccept, + + timestamp: initialTimestamp, + atomicRequests: map[ids.ID]*atomic.Requests{ + verifier.ctx.XChainID: { + PutRequests: []*atomic.Element{ + { + Key: exportedUTXOID[:], + Value: exportedUTXOBytes, + Traits: [][]byte{}, + }, + }, + }, + }, + verifiedHeights: set.Of(uint64(0)), + }, + atomicBlockState, + ) } func TestVerifierVisitStandardBlock(t *testing.T) { require := require.New(t) - ctrl := gomock.NewController(t) - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentState := state.NewMockDiff(ctrl) + var ( + ctx = snowtest.Context(t, constants.PlatformChainID) - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, + baseDB = memdb.New() + stateDB = prefixdb.New([]byte{0}, baseDB) + amDB = prefixdb.New([]byte{1}, baseDB) + + am = atomic.NewMemory(amDB) + sm = am.NewSharedMemory(ctx.ChainID) + xChainSM = am.NewSharedMemory(ctx.XChainID) + + owner = secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{genesis.EWOQKey.Address()}, + } + utxo = &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: ids.GenerateTestID(), + OutputIndex: 1, }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: units.Avax, + OutputOwners: owner, }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + ) - blkTx := txsmock.NewUnsignedTx(ctrl) - atomicRequests := map[ids.ID]*atomic.Requests{ - ids.GenerateTestID(): { - RemoveRequests: [][]byte{{1}, {2}}, + inputID := utxo.InputID() + utxoBytes, err := txs.Codec.Marshal(txs.CodecVersion, utxo) + require.NoError(err) + + require.NoError(xChainSM.Apply(map[ids.ID]*atomic.Requests{ + ctx.ChainID: { PutRequests: []*atomic.Element{ { - Key: []byte{3}, - Value: []byte{4}, - Traits: [][]byte{{5}, {6}}, + Key: inputID[:], + Value: utxoBytes, + Traits: [][]byte{ + genesis.EWOQKey.Address().Bytes(), + }, }, }, }, - } - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.StandardTxExecutor{})).DoAndReturn( - func(e *executor.StandardTxExecutor) error { - e.OnAccept = func() {} - e.Inputs = set.Set[ids.ID]{} - e.AtomicRequests = atomicRequests - return nil - }, - ).Times(1) - - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotStandardBlock( - parentID, - 2, /*height*/ - []*txs.Tx{ - { - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, - }, + })) + + ctx.SharedMemory = sm + + var ( + verifier = newTestVerifier(t, testVerifierConfig{ + DB: stateDB, + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhase5), + Context: ctx, + }) + wallet = txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + verifier.state, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + []ids.ID{ctx.XChainID}, // Read the UTXO to import + ) + initialTimestamp = verifier.state.GetTimestamp() + ) + + // Build the transaction that will be executed. + tx, err := wallet.IssueImportTx( + verifier.ctx.XChainID, + &owner, ) require.NoError(err) - apricotBlk.Transactions[0].Unsigned = blkTx - // Set expectations for dependencies. - timestamp := time.Now() - parentState.EXPECT().GetTimestamp().Return(timestamp).Times(1) - parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) - parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) - parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) - parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) - parentState.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(&iterator.Empty[state.SubnetOnlyValidator]{}, nil).Times(1) - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - mempool.EXPECT().Remove(apricotBlk.Txs()).Times(1) + // Verify that the transaction is only consuming the imported UTXO. + require.Len(tx.InputIDs(), 1) - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) - // Assert expected state. - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(set.Set[ids.ID]{}, gotBlkState.inputs) - require.Equal(timestamp, gotBlkState.timestamp) + firstBlock, err := block.NewApricotStandardBlock( + lastAcceptedID, + lastAccepted.Height()+1, + []*txs.Tx{tx}, + ) + require.NoError(err) - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + // Execute the block. + require.NoError(firstBlock.Visit(verifier)) + + // Verify that the block's execution was recorded as expected. + firstBlockID := firstBlock.ID() + { + require.Contains(verifier.blkIDToState, firstBlockID) + atomicBlockState := verifier.blkIDToState[firstBlockID] + onAccept := atomicBlockState.onAcceptState + require.NotNil(onAccept) + + txID := tx.ID() + acceptedTx, acceptedStatus, err := onAccept.GetTx(txID) + require.NoError(err) + require.Equal(tx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) + + require.Equal( + &blockState{ + statelessBlock: firstBlock, + + onAcceptState: onAccept, + + inputs: tx.InputIDs(), + timestamp: initialTimestamp, + atomicRequests: map[ids.ID]*atomic.Requests{ + verifier.ctx.XChainID: { + RemoveRequests: [][]byte{ + inputID[:], + }, + }, + }, + verifiedHeights: set.Of(uint64(0)), + }, + atomicBlockState, + ) + } + + // Verify that the import transaction can not be replayed. + { + secondBlock, err := block.NewApricotStandardBlock( + firstBlockID, + firstBlock.Height()+1, + []*txs.Tx{tx}, // Replay the prior transaction + ) + require.NoError(err) + + err = secondBlock.Visit(verifier) + require.ErrorIs(err, errConflictingParentTxs) + + // Verify that the block's execution was not recorded. + require.NotContains(verifier.blkIDToState, secondBlock.ID()) + } } func TestVerifierVisitCommitBlock(t *testing.T) { @@ -737,104 +801,6 @@ func TestBanffCommitBlockTimestampChecks(t *testing.T) { } } -func TestVerifierVisitStandardBlockWithDuplicateInputs(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - - grandParentID := ids.GenerateTestID() - grandParentStatelessBlk := block.NewMockBlock(ctrl) - grandParentState := state.NewMockDiff(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentState := state.NewMockDiff(ctrl) - atomicInputs := set.Of(ids.GenerateTestID()) - - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - grandParentID: { - statelessBlock: grandParentStatelessBlk, - onAcceptState: grandParentState, - inputs: atomicInputs, - }, - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - verifier := &verifier{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), - }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } - - blkTx := txsmock.NewUnsignedTx(ctrl) - atomicRequests := map[ids.ID]*atomic.Requests{ - ids.GenerateTestID(): { - RemoveRequests: [][]byte{{1}, {2}}, - PutRequests: []*atomic.Element{ - { - Key: []byte{3}, - Value: []byte{4}, - Traits: [][]byte{{5}, {6}}, - }, - }, - }, - } - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.StandardTxExecutor{})).DoAndReturn( - func(e *executor.StandardTxExecutor) error { - e.OnAccept = func() {} - e.Inputs = atomicInputs - e.AtomicRequests = atomicRequests - return nil - }, - ).Times(1) - - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - blk, err := block.NewApricotStandardBlock( - parentID, - 2, - []*txs.Tx{ - { - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, - }, - ) - require.NoError(err) - blk.Transactions[0].Unsigned = blkTx - - // Set expectations for dependencies. - timestamp := time.Now() - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - parentState.EXPECT().GetTimestamp().Return(timestamp).Times(1) - parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) - parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) - parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) - parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) - parentStatelessBlk.EXPECT().Parent().Return(grandParentID).Times(1) - - err = verifier.ApricotStandardBlock(blk) - require.ErrorIs(err, errConflictingParentTxs) -} - func TestVerifierVisitApricotStandardBlockWithProposalBlockParent(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) @@ -1124,13 +1090,12 @@ func TestVerifierVisitBanffAbortBlockUnexpectedParentState(t *testing.T) { } func TestBlockExecutionWithComplexity(t *testing.T) { - s := statetest.New(t, statetest.Config{}) - verifier := newTestVerifier(t, s) + verifier := newTestVerifier(t, testVerifierConfig{}) wallet := txstest.NewWallet( t, verifier.ctx, verifier.txExecutorBackend.Config, - s, + verifier.state, secp256k1fx.NewKeychain(genesis.EWOQKey), nil, // subnetIDs nil, // chainIDs @@ -1185,13 +1150,13 @@ func TestBlockExecutionWithComplexity(t *testing.T) { verifier.txExecutorBackend.Clk.Set(test.timestamp) timestamp, _, err := state.NextBlockTime( verifier.txExecutorBackend.Config.ValidatorFeeConfig, - s, + verifier.state, verifier.txExecutorBackend.Clk, ) require.NoError(err) - lastAcceptedID := s.GetLastAccepted() - lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) require.NoError(err) blk, err := block.NewBanffStandardBlock( diff --git a/vms/platformvm/txs/txsmock/unsigned_tx.go b/vms/platformvm/txs/txsmock/unsigned_tx.go deleted file mode 100644 index a7ba0daac896..000000000000 --- a/vms/platformvm/txs/txsmock/unsigned_tx.go +++ /dev/null @@ -1,138 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: vms/platformvm/txs/unsigned_tx.go -// -// Generated by this command: -// -// mockgen -source=vms/platformvm/txs/unsigned_tx.go -destination=vms/platformvm/txs/txsmock/unsigned_tx.go -package=txsmock -exclude_interfaces= -mock_names=UnsignedTx=UnsignedTx -// - -// Package txsmock is a generated GoMock package. -package txsmock - -import ( - reflect "reflect" - - ids "github.com/ava-labs/avalanchego/ids" - snow "github.com/ava-labs/avalanchego/snow" - set "github.com/ava-labs/avalanchego/utils/set" - avax "github.com/ava-labs/avalanchego/vms/components/avax" - txs "github.com/ava-labs/avalanchego/vms/platformvm/txs" - gomock "go.uber.org/mock/gomock" -) - -// UnsignedTx is a mock of UnsignedTx interface. -type UnsignedTx struct { - ctrl *gomock.Controller - recorder *UnsignedTxMockRecorder -} - -// UnsignedTxMockRecorder is the mock recorder for UnsignedTx. -type UnsignedTxMockRecorder struct { - mock *UnsignedTx -} - -// NewUnsignedTx creates a new mock instance. -func NewUnsignedTx(ctrl *gomock.Controller) *UnsignedTx { - mock := &UnsignedTx{ctrl: ctrl} - mock.recorder = &UnsignedTxMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *UnsignedTx) EXPECT() *UnsignedTxMockRecorder { - return m.recorder -} - -// Bytes mocks base method. -func (m *UnsignedTx) Bytes() []byte { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Bytes") - ret0, _ := ret[0].([]byte) - return ret0 -} - -// Bytes indicates an expected call of Bytes. -func (mr *UnsignedTxMockRecorder) Bytes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bytes", reflect.TypeOf((*UnsignedTx)(nil).Bytes)) -} - -// InitCtx mocks base method. -func (m *UnsignedTx) InitCtx(ctx *snow.Context) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "InitCtx", ctx) -} - -// InitCtx indicates an expected call of InitCtx. -func (mr *UnsignedTxMockRecorder) InitCtx(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitCtx", reflect.TypeOf((*UnsignedTx)(nil).InitCtx), ctx) -} - -// InputIDs mocks base method. -func (m *UnsignedTx) InputIDs() set.Set[ids.ID] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InputIDs") - ret0, _ := ret[0].(set.Set[ids.ID]) - return ret0 -} - -// InputIDs indicates an expected call of InputIDs. -func (mr *UnsignedTxMockRecorder) InputIDs() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InputIDs", reflect.TypeOf((*UnsignedTx)(nil).InputIDs)) -} - -// Outputs mocks base method. -func (m *UnsignedTx) Outputs() []*avax.TransferableOutput { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Outputs") - ret0, _ := ret[0].([]*avax.TransferableOutput) - return ret0 -} - -// Outputs indicates an expected call of Outputs. -func (mr *UnsignedTxMockRecorder) Outputs() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Outputs", reflect.TypeOf((*UnsignedTx)(nil).Outputs)) -} - -// SetBytes mocks base method. -func (m *UnsignedTx) SetBytes(unsignedBytes []byte) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetBytes", unsignedBytes) -} - -// SetBytes indicates an expected call of SetBytes. -func (mr *UnsignedTxMockRecorder) SetBytes(unsignedBytes any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBytes", reflect.TypeOf((*UnsignedTx)(nil).SetBytes), unsignedBytes) -} - -// SyntacticVerify mocks base method. -func (m *UnsignedTx) SyntacticVerify(ctx *snow.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SyntacticVerify", ctx) - ret0, _ := ret[0].(error) - return ret0 -} - -// SyntacticVerify indicates an expected call of SyntacticVerify. -func (mr *UnsignedTxMockRecorder) SyntacticVerify(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyntacticVerify", reflect.TypeOf((*UnsignedTx)(nil).SyntacticVerify), ctx) -} - -// Visit mocks base method. -func (m *UnsignedTx) Visit(visitor txs.Visitor) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Visit", visitor) - ret0, _ := ret[0].(error) - return ret0 -} - -// Visit indicates an expected call of Visit. -func (mr *UnsignedTxMockRecorder) Visit(visitor any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Visit", reflect.TypeOf((*UnsignedTx)(nil).Visit), visitor) -} From 24c7d4baa8881d75fdc4b1e9d2c9177e7a7effbb Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:40:13 -0500 Subject: [PATCH 134/184] nit --- vms/platformvm/block/executor/verifier_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 9dfc631a4915..4de3a6c8b2dd 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -178,7 +178,7 @@ func TestVerifierVisitProposalBlock(t *testing.T) { statelessBlock: proposalBlock, timestamp: initialTimestamp, - verifiedHeights: set.Of(uint64(0)), + verifiedHeights: set.Of[uint64](0), }, executedBlockState, ) @@ -276,7 +276,7 @@ func TestVerifierVisitAtomicBlock(t *testing.T) { }, }, }, - verifiedHeights: set.Of(uint64(0)), + verifiedHeights: set.Of[uint64](0), }, atomicBlockState, ) @@ -405,7 +405,7 @@ func TestVerifierVisitStandardBlock(t *testing.T) { }, }, }, - verifiedHeights: set.Of(uint64(0)), + verifiedHeights: set.Of[uint64](0), }, atomicBlockState, ) From 70a53b3768b547976a98578b54290c206018b738 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:58:14 -0500 Subject: [PATCH 135/184] reduce diff --- vms/platformvm/metrics/tx_metrics.go | 8 +- .../txs/executor/atomic_tx_executor.go | 6 +- .../txs/executor/standard_tx_executor.go | 322 +++++++++--------- 3 files changed, 168 insertions(+), 168 deletions(-) diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index 7957cb6ab2e9..fd6c63494f7c 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -132,16 +132,16 @@ func (m *txMetrics) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) er return nil } -func (m *txMetrics) BaseTx(*txs.BaseTx) error { +func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "base", + txLabel: "convert_subnet", }).Inc() return nil } -func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (m *txMetrics) BaseTx(*txs.BaseTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "convert_subnet", + txLabel: "base", }).Inc() return nil } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 1977608d09c1..ea5294c2559c 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -92,15 +92,15 @@ func (*atomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 3ccc059bfa8e..3ac1e3f48550 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -88,82 +88,6 @@ func (*standardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } -func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { - if tx.Validator.NodeID == ids.EmptyNodeID { - return errEmptyNodeID - } - - if _, err := verifyAddValidatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - - if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { - e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", - zap.String("reason", "primary network is not being fully synced"), - zap.Stringer("txID", txID), - zap.String("txType", "addValidator"), - zap.Stringer("nodeID", tx.Validator.NodeID), - ) - } - return nil -} - -func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { - if err := verifyAddSubnetValidatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); 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) AddDelegatorTx(tx *txs.AddDelegatorTx) error { - if _, err := verifyAddDelegatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); 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) CreateChainTx(tx *txs.CreateChainTx) error { if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err @@ -261,6 +185,167 @@ func (e *standardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return nil } +func (e *standardTxExecutor) ExportTx(tx *txs.ExportTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { + return err + } + + var ( + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + ) + if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { + return err + } + + outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.ExportedOutputs)) + copy(outs, tx.Outs) + copy(outs[len(tx.Outs):], tx.ExportedOutputs) + + if e.backend.Bootstrapped.Get() { + if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.DestinationChain); err != nil { + return err + } + } + + // Verify the flowcheck + fee, err := e.feeCalculator.CalculateFee(tx) + if err != nil { + return err + } + if err := e.backend.FlowChecker.VerifySpend( + tx, + e.state, + tx.Ins, + outs, + e.tx.Creds, + map[ids.ID]uint64{ + e.backend.Ctx.AVAXAssetID: fee, + }, + ); err != nil { + return fmt.Errorf("failed verifySpend: %w", err) + } + + txID := e.tx.ID() + + // Consume the UTXOS + avax.Consume(e.state, tx.Ins) + // Produce the UTXOS + avax.Produce(e.state, txID, tx.Outs) + + // Note: We apply atomic requests even if we are not verifying atomic + // requests to ensure the shared state will be correct if we later start + // verifying the requests. + elems := make([]*atomic.Element, len(tx.ExportedOutputs)) + for i, out := range tx.ExportedOutputs { + utxo := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(len(tx.Outs) + i), + }, + Asset: avax.Asset{ID: out.AssetID()}, + Out: out.Out, + } + + utxoBytes, err := txs.Codec.Marshal(txs.CodecVersion, utxo) + if err != nil { + return fmt.Errorf("failed to marshal UTXO: %w", err) + } + utxoID := utxo.InputID() + elem := &atomic.Element{ + Key: utxoID[:], + Value: utxoBytes, + } + if out, ok := utxo.Out.(avax.Addressable); ok { + elem.Traits = out.Addresses() + } + + elems[i] = elem + } + e.atomicRequests = map[ids.ID]*atomic.Requests{ + tx.DestinationChain: { + PutRequests: elems, + }, + } + return nil +} + +func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { + if tx.Validator.NodeID == ids.EmptyNodeID { + return errEmptyNodeID + } + + if _, err := verifyAddValidatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); err != nil { + return err + } + + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) + + if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + zap.String("reason", "primary network is not being fully synced"), + zap.Stringer("txID", txID), + zap.String("txType", "addValidator"), + zap.Stringer("nodeID", tx.Validator.NodeID), + ) + } + return nil +} + +func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { + if err := verifyAddSubnetValidatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) AddDelegatorTx(tx *txs.AddDelegatorTx) error { + if _, err := verifyAddDelegatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) ImportTx(tx *txs.ImportTx) error { if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err @@ -352,91 +437,6 @@ func (e *standardTxExecutor) ImportTx(tx *txs.ImportTx) error { return nil } -func (e *standardTxExecutor) ExportTx(tx *txs.ExportTx) error { - if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { - return err - } - - var ( - currentTimestamp = e.state.GetTimestamp() - isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) - ) - if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { - return err - } - - outs := make([]*avax.TransferableOutput, len(tx.Outs)+len(tx.ExportedOutputs)) - copy(outs, tx.Outs) - copy(outs[len(tx.Outs):], tx.ExportedOutputs) - - if e.backend.Bootstrapped.Get() { - if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.DestinationChain); err != nil { - return err - } - } - - // Verify the flowcheck - fee, err := e.feeCalculator.CalculateFee(tx) - if err != nil { - return err - } - if err := e.backend.FlowChecker.VerifySpend( - tx, - e.state, - tx.Ins, - outs, - e.tx.Creds, - map[ids.ID]uint64{ - e.backend.Ctx.AVAXAssetID: fee, - }, - ); err != nil { - return fmt.Errorf("failed verifySpend: %w", err) - } - - txID := e.tx.ID() - - // Consume the UTXOS - avax.Consume(e.state, tx.Ins) - // Produce the UTXOS - avax.Produce(e.state, txID, tx.Outs) - - // Note: We apply atomic requests even if we are not verifying atomic - // requests to ensure the shared state will be correct if we later start - // verifying the requests. - elems := make([]*atomic.Element, len(tx.ExportedOutputs)) - for i, out := range tx.ExportedOutputs { - utxo := &avax.UTXO{ - UTXOID: avax.UTXOID{ - TxID: txID, - OutputIndex: uint32(len(tx.Outs) + i), - }, - Asset: avax.Asset{ID: out.AssetID()}, - Out: out.Out, - } - - utxoBytes, err := txs.Codec.Marshal(txs.CodecVersion, utxo) - if err != nil { - return fmt.Errorf("failed to marshal UTXO: %w", err) - } - utxoID := utxo.InputID() - elem := &atomic.Element{ - Key: utxoID[:], - Value: utxoBytes, - } - if out, ok := utxo.Out.(avax.Addressable); ok { - elem.Traits = out.Addresses() - } - - elems[i] = elem - } - e.atomicRequests = map[ids.ID]*atomic.Requests{ - tx.DestinationChain: { - PutRequests: elems, - }, - } - return nil -} - // Verifies a [*txs.RemoveSubnetValidatorTx] and, if it passes, executes it on // [e.State]. For verification rules, see [verifyRemoveSubnetValidatorTx]. This // transaction will result in [tx.NodeID] being removed as a validator of From 26c3549e2aea25f07fac350105fa054971a18750 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 11:59:31 -0500 Subject: [PATCH 136/184] reduce diff --- .../txs/executor/standard_tx_executor.go | 182 +++++++++--------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 3ac1e3f48550..04ba94453756 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -185,6 +185,97 @@ func (e *standardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return nil } +func (e *standardTxExecutor) ImportTx(tx *txs.ImportTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { + return err + } + + var ( + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + ) + if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { + return err + } + + e.inputs = set.NewSet[ids.ID](len(tx.ImportedInputs)) + utxoIDs := make([][]byte, len(tx.ImportedInputs)) + for i, in := range tx.ImportedInputs { + utxoID := in.UTXOID.InputID() + + e.inputs.Add(utxoID) + utxoIDs[i] = utxoID[:] + } + + // Skip verification of the shared memory inputs if the other primary + // network chains are not guaranteed to be up-to-date. + if e.backend.Bootstrapped.Get() && !e.backend.Config.PartialSyncPrimaryNetwork { + if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.SourceChain); err != nil { + return err + } + + allUTXOBytes, err := e.backend.Ctx.SharedMemory.Get(tx.SourceChain, utxoIDs) + if err != nil { + return fmt.Errorf("failed to get shared memory: %w", err) + } + + utxos := make([]*avax.UTXO, len(tx.Ins)+len(tx.ImportedInputs)) + for index, input := range tx.Ins { + utxo, err := e.state.GetUTXO(input.InputID()) + if err != nil { + return fmt.Errorf("failed to get UTXO %s: %w", &input.UTXOID, err) + } + utxos[index] = utxo + } + for i, utxoBytes := range allUTXOBytes { + utxo := &avax.UTXO{} + if _, err := txs.Codec.Unmarshal(utxoBytes, utxo); err != nil { + return fmt.Errorf("failed to unmarshal UTXO: %w", err) + } + utxos[i+len(tx.Ins)] = utxo + } + + ins := make([]*avax.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) + copy(ins, tx.Ins) + copy(ins[len(tx.Ins):], tx.ImportedInputs) + + // Verify the flowcheck + fee, err := e.feeCalculator.CalculateFee(tx) + if err != nil { + return err + } + if err := e.backend.FlowChecker.VerifySpendUTXOs( + tx, + utxos, + ins, + tx.Outs, + e.tx.Creds, + map[ids.ID]uint64{ + e.backend.Ctx.AVAXAssetID: fee, + }, + ); err != nil { + return err + } + } + + txID := e.tx.ID() + + // Consume the UTXOS + avax.Consume(e.state, tx.Ins) + // Produce the UTXOS + avax.Produce(e.state, txID, tx.Outs) + + // Note: We apply atomic requests even if we are not verifying atomic + // requests to ensure the shared state will be correct if we later start + // verifying the requests. + e.atomicRequests = map[ids.ID]*atomic.Requests{ + tx.SourceChain: { + RemoveRequests: utxoIDs, + }, + } + return nil +} + func (e *standardTxExecutor) ExportTx(tx *txs.ExportTx) error { if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err @@ -346,97 +437,6 @@ func (e *standardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { return nil } -func (e *standardTxExecutor) ImportTx(tx *txs.ImportTx) error { - if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { - return err - } - - var ( - currentTimestamp = e.state.GetTimestamp() - isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) - ) - if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { - return err - } - - e.inputs = set.NewSet[ids.ID](len(tx.ImportedInputs)) - utxoIDs := make([][]byte, len(tx.ImportedInputs)) - for i, in := range tx.ImportedInputs { - utxoID := in.UTXOID.InputID() - - e.inputs.Add(utxoID) - utxoIDs[i] = utxoID[:] - } - - // Skip verification of the shared memory inputs if the other primary - // network chains are not guaranteed to be up-to-date. - if e.backend.Bootstrapped.Get() && !e.backend.Config.PartialSyncPrimaryNetwork { - if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.SourceChain); err != nil { - return err - } - - allUTXOBytes, err := e.backend.Ctx.SharedMemory.Get(tx.SourceChain, utxoIDs) - if err != nil { - return fmt.Errorf("failed to get shared memory: %w", err) - } - - utxos := make([]*avax.UTXO, len(tx.Ins)+len(tx.ImportedInputs)) - for index, input := range tx.Ins { - utxo, err := e.state.GetUTXO(input.InputID()) - if err != nil { - return fmt.Errorf("failed to get UTXO %s: %w", &input.UTXOID, err) - } - utxos[index] = utxo - } - for i, utxoBytes := range allUTXOBytes { - utxo := &avax.UTXO{} - if _, err := txs.Codec.Unmarshal(utxoBytes, utxo); err != nil { - return fmt.Errorf("failed to unmarshal UTXO: %w", err) - } - utxos[i+len(tx.Ins)] = utxo - } - - ins := make([]*avax.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) - copy(ins, tx.Ins) - copy(ins[len(tx.Ins):], tx.ImportedInputs) - - // Verify the flowcheck - fee, err := e.feeCalculator.CalculateFee(tx) - if err != nil { - return err - } - if err := e.backend.FlowChecker.VerifySpendUTXOs( - tx, - utxos, - ins, - tx.Outs, - e.tx.Creds, - map[ids.ID]uint64{ - e.backend.Ctx.AVAXAssetID: fee, - }, - ); err != nil { - return err - } - } - - txID := e.tx.ID() - - // Consume the UTXOS - avax.Consume(e.state, tx.Ins) - // Produce the UTXOS - avax.Produce(e.state, txID, tx.Outs) - - // Note: We apply atomic requests even if we are not verifying atomic - // requests to ensure the shared state will be correct if we later start - // verifying the requests. - e.atomicRequests = map[ids.ID]*atomic.Requests{ - tx.SourceChain: { - RemoveRequests: utxoIDs, - }, - } - return nil -} - // Verifies a [*txs.RemoveSubnetValidatorTx] and, if it passes, executes it on // [e.State]. For verification rules, see [verifyRemoveSubnetValidatorTx]. This // transaction will result in [tx.NodeID] being removed as a validator of From 877242fff2cdc13a4c626364943bdc9e48fa34f3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:00:45 -0500 Subject: [PATCH 137/184] reduce diff --- .../txs/executor/standard_tx_executor.go | 244 +++++++++--------- 1 file changed, 122 insertions(+), 122 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 04ba94453756..7c561473a7bd 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -530,128 +530,6 @@ func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error return nil } -func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - if err := verifyAddPermissionlessValidatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - - if e.backend.Config.PartialSyncPrimaryNetwork && - tx.Subnet == constants.PrimaryNetworkID && - tx.Validator.NodeID == e.backend.Ctx.NodeID { - e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", - zap.String("reason", "primary network is not being fully synced"), - zap.Stringer("txID", txID), - zap.String("txType", "addPermissionlessValidator"), - zap.Stringer("nodeID", tx.Validator.NodeID), - ) - } - - return nil -} - -func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - if err := verifyAddPermissionlessDelegatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - return nil -} - -// Verifies a [*txs.TransferSubnetOwnershipTx] and, if it passes, executes it on -// [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. -// This transaction will result in the ownership of [tx.Subnet] being transferred -// to [tx.Owner]. -func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - err := verifyTransferSubnetOwnershipTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ) - if err != nil { - return err - } - - e.state.SetSubnetOwner(tx.Subnet, tx.Owner) - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - return nil -} - -func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { - var ( - currentTimestamp = e.state.GetTimestamp() - upgrades = e.backend.Config.UpgradeConfig - ) - if !upgrades.IsDurangoActivated(currentTimestamp) { - return ErrDurangoUpgradeNotActive - } - - // Verify the tx is well-formed - if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { - return err - } - - if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { - return err - } - - // Verify the flowcheck - fee, err := e.feeCalculator.CalculateFee(tx) - if err != nil { - return err - } - if err := e.backend.FlowChecker.VerifySpend( - tx, - e.state, - tx.Ins, - tx.Outs, - e.tx.Creds, - map[ids.ID]uint64{ - e.backend.Ctx.AVAXAssetID: fee, - }, - ); err != nil { - return err - } - - txID := e.tx.ID() - // Consume the UTXOS - avax.Consume(e.state, tx.Ins) - // Produce the UTXOS - avax.Produce(e.state, txID, tx.Outs) - return nil -} - func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { var ( currentTimestamp = e.state.GetTimestamp() @@ -780,6 +658,128 @@ func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return nil } +func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + if err := verifyAddPermissionlessValidatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); err != nil { + return err + } + + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) + + if e.backend.Config.PartialSyncPrimaryNetwork && + tx.Subnet == constants.PrimaryNetworkID && + tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + zap.String("reason", "primary network is not being fully synced"), + zap.Stringer("txID", txID), + zap.String("txType", "addPermissionlessValidator"), + zap.Stringer("nodeID", tx.Validator.NodeID), + ) + } + + return nil +} + +func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + if err := verifyAddPermissionlessDelegatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); err != nil { + return err + } + + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) + return nil +} + +// Verifies a [*txs.TransferSubnetOwnershipTx] and, if it passes, executes it on +// [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. +// This transaction will result in the ownership of [tx.Subnet] being transferred +// to [tx.Owner]. +func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + err := verifyTransferSubnetOwnershipTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ) + if err != nil { + return err + } + + e.state.SetSubnetOwner(tx.Subnet, tx.Owner) + + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) + return nil +} + +func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { + var ( + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig + ) + if !upgrades.IsDurangoActivated(currentTimestamp) { + return ErrDurangoUpgradeNotActive + } + + // Verify the tx is well-formed + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { + return err + } + + if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { + return err + } + + // Verify the flowcheck + fee, err := e.feeCalculator.CalculateFee(tx) + if err != nil { + return err + } + if err := e.backend.FlowChecker.VerifySpend( + tx, + e.state, + tx.Ins, + tx.Outs, + e.tx.Creds, + map[ids.ID]uint64{ + e.backend.Ctx.AVAXAssetID: fee, + }, + ); err != nil { + return err + } + + txID := e.tx.ID() + // Consume the UTXOS + avax.Consume(e.state, tx.Ins) + // Produce the UTXOS + avax.Produce(e.state, txID, tx.Outs) + return nil +} + // Creates the staker as defined in [stakerTx] and adds it to [e.State]. func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( From 5110cfa6a24e04172548682458b7ce03eb759f8f Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:02:38 -0500 Subject: [PATCH 138/184] reduce diff --- vms/platformvm/txs/fee/complexity.go | 112 +++++++++++++-------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index d7e687e90bd5..c9a33b60ce70 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -440,11 +440,11 @@ type complexityVisitor struct { output gas.Dimensions } -func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { +func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { return ErrUnsupportedTx } -func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { return ErrUnsupportedTx } @@ -460,6 +460,60 @@ func (*complexityVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrUnsupportedTx } +func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + // TODO: Should we include additional complexity for subnets? + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + signerComplexity, err := SignerComplexity(tx.Signer) + if err != nil { + return err + } + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) + if err != nil { + return err + } + delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( + &baseTxComplexity, + &signerComplexity, + &outputsComplexity, + &validatorOwnerComplexity, + &delegatorOwnerComplexity, + ) + return err +} + +func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + // TODO: Should we include additional complexity for subnets? + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) + if err != nil { + return err + } + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( + &baseTxComplexity, + &ownerComplexity, + &outputsComplexity, + ) + return err +} + func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { @@ -578,60 +632,6 @@ func (c *complexityVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidato return err } -func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - signerComplexity, err := SignerComplexity(tx.Signer) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) - if err != nil { - return err - } - delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( - &baseTxComplexity, - &signerComplexity, - &outputsComplexity, - &validatorOwnerComplexity, - &delegatorOwnerComplexity, - ) - return err -} - -func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( - &baseTxComplexity, - &ownerComplexity, - &outputsComplexity, - ) - return err -} - func (c *complexityVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { From e435ed7904187f7455e30dbcbd5116c79858db9c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:03:07 -0500 Subject: [PATCH 139/184] reduce diff --- vms/platformvm/txs/fee/complexity.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index c9a33b60ce70..104a900e18bc 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -530,6 +530,15 @@ func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) e return err } +func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { + baseTxComplexity, err := baseTxComplexity(tx) + if err != nil { + return err + } + c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) + return err +} + func (c *complexityVisitor) CreateChainTx(tx *txs.CreateChainTx) error { bandwidth, err := math.Mul(uint64(len(tx.FxIDs)), ids.IDLen) if err != nil { @@ -653,15 +662,6 @@ func (c *complexityVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwne return err } -func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { - baseTxComplexity, err := baseTxComplexity(tx) - if err != nil { - return err - } - c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) - return err -} - func (c *complexityVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { From 3750b1c4e3d8941f1a359fe7eabd84a267e487b6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:03:22 -0500 Subject: [PATCH 140/184] reduce diff --- vms/platformvm/txs/fee/complexity.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 104a900e18bc..5683ac6b3b6a 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -591,36 +591,36 @@ func (c *complexityVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return err } -func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { +func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - // TODO: Should imported inputs be more complex? - inputsComplexity, err := InputComplexity(tx.ImportedInputs...) + // TODO: Should exported outputs be more complex? + outputsComplexity, err := OutputComplexity(tx.ExportedOutputs...) if err != nil { return err } - c.output, err = IntrinsicImportTxComplexities.Add( + c.output, err = IntrinsicExportTxComplexities.Add( &baseTxComplexity, - &inputsComplexity, + &outputsComplexity, ) return err } -func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { +func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - // TODO: Should exported outputs be more complex? - outputsComplexity, err := OutputComplexity(tx.ExportedOutputs...) + // TODO: Should imported inputs be more complex? + inputsComplexity, err := InputComplexity(tx.ImportedInputs...) if err != nil { return err } - c.output, err = IntrinsicExportTxComplexities.Add( + c.output, err = IntrinsicImportTxComplexities.Add( &baseTxComplexity, - &outputsComplexity, + &inputsComplexity, ) return err } From 687398e38ab941f5aa5e07d9c74b2b0c97313ff7 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:05:16 -0500 Subject: [PATCH 141/184] reduce diff --- vms/platformvm/txs/fee/static_calculator.go | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/vms/platformvm/txs/fee/static_calculator.go b/vms/platformvm/txs/fee/static_calculator.go index 1b97349ce2cb..888ccba8621c 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -76,26 +76,21 @@ func (c *staticVisitor) CreateSubnetTx(*txs.CreateSubnetTx) error { return nil } -func (c *staticVisitor) ImportTx(*txs.ImportTx) error { +func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) ExportTx(*txs.ExportTx) error { - c.fee = c.config.TxFee +func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { + c.fee = c.config.TransformSubnetTxFee return nil } -func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { - c.fee = c.config.TransformSubnetTxFee - return nil -} - func (c *staticVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if tx.Subnet != constants.PrimaryNetworkID { c.fee = c.config.AddSubnetValidatorFee @@ -114,12 +109,17 @@ func (c *staticVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDe return nil } -func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (c *staticVisitor) BaseTx(*txs.BaseTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) BaseTx(*txs.BaseTx) error { +func (c *staticVisitor) ImportTx(*txs.ImportTx) error { + c.fee = c.config.TxFee + return nil +} + +func (c *staticVisitor) ExportTx(*txs.ExportTx) error { c.fee = c.config.TxFee return nil } From 5a0850551a22718c2883bb0f9efd84bf6fb45a91 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:05:51 -0500 Subject: [PATCH 142/184] reduce diff --- vms/platformvm/txs/visitor.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 21c46476fa5f..142a6115d7af 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -5,7 +5,6 @@ package txs // Allow vm to execute custom logic against the underlying transaction types. type Visitor interface { - // Apricot Transactions: AddValidatorTx(*AddValidatorTx) error AddSubnetValidatorTx(*AddSubnetValidatorTx) error AddDelegatorTx(*AddDelegatorTx) error @@ -15,17 +14,11 @@ type Visitor interface { ExportTx(*ExportTx) error AdvanceTimeTx(*AdvanceTimeTx) error RewardValidatorTx(*RewardValidatorTx) error - - // Banff Transactions: RemoveSubnetValidatorTx(*RemoveSubnetValidatorTx) error TransformSubnetTx(*TransformSubnetTx) error AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error - - // Durango Transactions: TransferSubnetOwnershipTx(*TransferSubnetOwnershipTx) error - BaseTx(*BaseTx) error - - // Etna Transactions: ConvertSubnetTx(*ConvertSubnetTx) error + BaseTx(*BaseTx) error } From 05d24ca03bc45f6b6aebf208a1e35238687f7b5a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:06:49 -0500 Subject: [PATCH 143/184] reduce diff --- wallet/chain/p/signer/visitor.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index b358e1f5d5ea..47bc4542835a 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -51,6 +51,14 @@ func (*visitor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrUnsupportedTxType } +func (s *visitor) BaseTx(tx *txs.BaseTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + return sign(s.tx, false, txSigners) +} + func (s *visitor) AddValidatorTx(tx *txs.AddValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { @@ -135,7 +143,7 @@ func (s *visitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error return sign(s.tx, true, txSigners) } -func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { +func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -148,41 +156,33 @@ func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return sign(s.tx, true, txSigners) } -func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { +func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - return sign(s.tx, true, txSigners) -} - -func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) if err != nil { return err } + txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } -func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { +func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) - if err != nil { - return err - } - txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } -func (s *visitor) BaseTx(tx *txs.BaseTx) error { +func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - return sign(s.tx, false, txSigners) + return sign(s.tx, true, txSigners) } func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { From 37bede469a17eb16afd8886ac886d79405a0e1cd Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:07:20 -0500 Subject: [PATCH 144/184] reduce diff --- wallet/chain/p/signer/visitor.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index 47bc4542835a..38d501c908b0 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -156,7 +156,7 @@ func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) e return sign(s.tx, true, txSigners) } -func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { +func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -169,15 +169,20 @@ func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return sign(s.tx, true, txSigners) } -func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { +func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } + subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } -func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { +func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -185,16 +190,11 @@ func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegato return sign(s.tx, true, txSigners) } -func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { +func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) - if err != nil { - return err - } - txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } From 3098c0bf7b02d177015c0957cc8997fe582fea6a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:08:19 -0500 Subject: [PATCH 145/184] reduce diff --- wallet/chain/p/wallet/backend_visitor.go | 40 ++++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/wallet/chain/p/wallet/backend_visitor.go b/wallet/chain/p/wallet/backend_visitor.go index f2f9e646edf8..8e44ee3b5e2d 100644 --- a/wallet/chain/p/wallet/backend_visitor.go +++ b/wallet/chain/p/wallet/backend_visitor.go @@ -58,6 +58,26 @@ func (b *backendVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + return b.baseTx(&tx.BaseTx) +} + +func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + b.b.setSubnetOwner( + tx.Subnet, + tx.Owner, + ) + return b.baseTx(&tx.BaseTx) +} + +func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + return b.baseTx(&tx.BaseTx) +} + +func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { + return b.baseTx(tx) +} + func (b *backendVisitor) ImportTx(tx *txs.ImportTx) error { err := b.b.removeUTXOs( b.ctx, @@ -91,10 +111,6 @@ func (b *backendVisitor) ExportTx(tx *txs.ExportTx) error { return b.baseTx(&tx.BaseTx) } -func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { - return b.baseTx(&tx.BaseTx) -} - func (b *backendVisitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return b.baseTx(&tx.BaseTx) } @@ -107,22 +123,6 @@ func (b *backendVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessD return b.baseTx(&tx.BaseTx) } -func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - b.b.setSubnetOwner( - tx.Subnet, - tx.Owner, - ) - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { - return b.baseTx(tx) -} - -func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { - return b.baseTx(&tx.BaseTx) -} - func (b *backendVisitor) baseTx(tx *txs.BaseTx) error { return b.b.removeUTXOs( b.ctx, From 240f1e8410d207c2bb79ebe9c54f1180ef927f55 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 12:59:13 -0500 Subject: [PATCH 146/184] Standardize P-Chain tx visitor order --- vms/platformvm/metrics/tx_metrics.go | 8 +- .../txs/executor/atomic_tx_executor.go | 6 +- .../txs/executor/standard_tx_executor.go | 368 +++++++++--------- vms/platformvm/txs/fee/complexity.go | 232 +++++------ vms/platformvm/txs/fee/static_calculator.go | 22 +- vms/platformvm/txs/visitor.go | 9 +- wallet/chain/p/signer/visitor.go | 32 +- wallet/chain/p/wallet/backend_visitor.go | 40 +- 8 files changed, 362 insertions(+), 355 deletions(-) diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index fd6c63494f7c..7957cb6ab2e9 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -132,16 +132,16 @@ func (m *txMetrics) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) er return nil } -func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (m *txMetrics) BaseTx(*txs.BaseTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "convert_subnet", + txLabel: "base", }).Inc() return nil } -func (m *txMetrics) BaseTx(*txs.BaseTx) error { +func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "base", + txLabel: "convert_subnet", }).Inc() return nil } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index ea5294c2559c..1977608d09c1 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -92,15 +92,15 @@ func (*atomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 7c561473a7bd..3ccc059bfa8e 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -88,6 +88,82 @@ func (*standardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } +func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { + if tx.Validator.NodeID == ids.EmptyNodeID { + return errEmptyNodeID + } + + if _, err := verifyAddValidatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); err != nil { + return err + } + + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) + + if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + zap.String("reason", "primary network is not being fully synced"), + zap.Stringer("txID", txID), + zap.String("txType", "addValidator"), + zap.Stringer("nodeID", tx.Validator.NodeID), + ) + } + return nil +} + +func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { + if err := verifyAddSubnetValidatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) AddDelegatorTx(tx *txs.AddDelegatorTx) error { + if _, err := verifyAddDelegatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) CreateChainTx(tx *txs.CreateChainTx) error { if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err @@ -361,42 +437,101 @@ func (e *standardTxExecutor) ExportTx(tx *txs.ExportTx) error { return nil } -func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { - if tx.Validator.NodeID == ids.EmptyNodeID { - return errEmptyNodeID - } - - if _, err := verifyAddValidatorTx( +// Verifies a [*txs.RemoveSubnetValidatorTx] and, if it passes, executes it on +// [e.State]. For verification rules, see [verifyRemoveSubnetValidatorTx]. This +// transaction will result in [tx.NodeID] being removed as a validator of +// [tx.SubnetID]. +// Note: [tx.NodeID] may be either a current or pending validator. +func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + staker, isCurrentValidator, err := verifyRemoveSubnetValidatorTx( e.backend, e.feeCalculator, e.state, e.tx, tx, - ); err != nil { + ) + if err != nil { return err } - if err := e.putStaker(tx); err != nil { - return err + if isCurrentValidator { + e.state.DeleteCurrentValidator(staker) + } else { + e.state.DeletePendingValidator(staker) } + // Invariant: There are no permissioned subnet delegators to remove. + txID := e.tx.ID() avax.Consume(e.state, tx.Ins) avax.Produce(e.state, txID, tx.Outs) - if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { - e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", - zap.String("reason", "primary network is not being fully synced"), - zap.Stringer("txID", txID), - zap.String("txType", "addValidator"), - zap.Stringer("nodeID", tx.Validator.NodeID), - ) + return nil +} + +func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { + currentTimestamp := e.state.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { + return errTransformSubnetTxPostEtna + } + + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { + return err + } + + isDurangoActive := e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { + return err + } + + // Note: math.MaxInt32 * time.Second < math.MaxInt64 - so this can never + // overflow. + if time.Duration(tx.MaxStakeDuration)*time.Second > e.backend.Config.MaxStakeDuration { + return errMaxStakeDurationTooLarge + } + + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + + // Verify the flowcheck + fee, err := e.feeCalculator.CalculateFee(tx) + if err != nil { + return err } + totalRewardAmount := tx.MaximumSupply - tx.InitialSupply + if err := e.backend.FlowChecker.VerifySpend( + tx, + e.state, + tx.Ins, + tx.Outs, + baseTxCreds, + // Invariant: [tx.AssetID != e.Ctx.AVAXAssetID]. This prevents the first + // entry in this map literal from being overwritten by the + // second entry. + map[ids.ID]uint64{ + e.backend.Ctx.AVAXAssetID: fee, + tx.AssetID: totalRewardAmount, + }, + ); err != nil { + return err + } + + txID := e.tx.ID() + + // Consume the UTXOS + avax.Consume(e.state, tx.Ins) + // Produce the UTXOS + avax.Produce(e.state, txID, tx.Outs) + // Transform the new subnet in the database + e.state.AddSubnetTransformation(e.tx) + e.state.SetCurrentSupply(tx.Subnet, tx.InitialSupply) return nil } -func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { - if err := verifyAddSubnetValidatorTx( +func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + if err := verifyAddPermissionlessValidatorTx( e.backend, e.feeCalculator, e.state, @@ -413,11 +548,23 @@ func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) txID := e.tx.ID() avax.Consume(e.state, tx.Ins) avax.Produce(e.state, txID, tx.Outs) + + if e.backend.Config.PartialSyncPrimaryNetwork && + tx.Subnet == constants.PrimaryNetworkID && + tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + zap.String("reason", "primary network is not being fully synced"), + zap.Stringer("txID", txID), + zap.String("txType", "addPermissionlessValidator"), + zap.Stringer("nodeID", tx.Validator.NodeID), + ) + } + return nil } -func (e *standardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { - if _, err := verifyAddDelegatorTx( +func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + if err := verifyAddPermissionlessDelegatorTx( e.backend, e.feeCalculator, e.state, @@ -437,13 +584,12 @@ func (e *standardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { return nil } -// Verifies a [*txs.RemoveSubnetValidatorTx] and, if it passes, executes it on -// [e.State]. For verification rules, see [verifyRemoveSubnetValidatorTx]. This -// transaction will result in [tx.NodeID] being removed as a validator of -// [tx.SubnetID]. -// Note: [tx.NodeID] may be either a current or pending validator. -func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { - staker, isCurrentValidator, err := verifyRemoveSubnetValidatorTx( +// Verifies a [*txs.TransferSubnetOwnershipTx] and, if it passes, executes it on +// [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. +// This transaction will result in the ownership of [tx.Subnet] being transferred +// to [tx.Owner]. +func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + err := verifyTransferSubnetOwnershipTx( e.backend, e.feeCalculator, e.state, @@ -454,44 +600,29 @@ func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidat return err } - if isCurrentValidator { - e.state.DeleteCurrentValidator(staker) - } else { - e.state.DeletePendingValidator(staker) - } - - // Invariant: There are no permissioned subnet delegators to remove. + e.state.SetSubnetOwner(tx.Subnet, tx.Owner) txID := e.tx.ID() avax.Consume(e.state, tx.Ins) avax.Produce(e.state, txID, tx.Outs) - return nil } -func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { - currentTimestamp := e.state.GetTimestamp() - if e.backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { - return errTransformSubnetTxPostEtna +func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { + var ( + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig + ) + if !upgrades.IsDurangoActivated(currentTimestamp) { + return ErrDurangoUpgradeNotActive } + // Verify the tx is well-formed if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } - isDurangoActive := e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) - if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { - return err - } - - // Note: math.MaxInt32 * time.Second < math.MaxInt64 - so this can never - // overflow. - if time.Duration(tx.MaxStakeDuration)*time.Second > e.backend.Config.MaxStakeDuration { - return errMaxStakeDurationTooLarge - } - - baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) - if err != nil { + if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { return err } @@ -500,33 +631,24 @@ func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error if err != nil { return err } - totalRewardAmount := tx.MaximumSupply - tx.InitialSupply if err := e.backend.FlowChecker.VerifySpend( tx, e.state, tx.Ins, tx.Outs, - baseTxCreds, - // Invariant: [tx.AssetID != e.Ctx.AVAXAssetID]. This prevents the first - // entry in this map literal from being overwritten by the - // second entry. + e.tx.Creds, map[ids.ID]uint64{ e.backend.Ctx.AVAXAssetID: fee, - tx.AssetID: totalRewardAmount, }, ); err != nil { return err } txID := e.tx.ID() - // Consume the UTXOS avax.Consume(e.state, tx.Ins) // Produce the UTXOS avax.Produce(e.state, txID, tx.Outs) - // Transform the new subnet in the database - e.state.AddSubnetTransformation(e.tx) - e.state.SetCurrentSupply(tx.Subnet, tx.InitialSupply) return nil } @@ -658,128 +780,6 @@ func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return nil } -func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - if err := verifyAddPermissionlessValidatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - - if e.backend.Config.PartialSyncPrimaryNetwork && - tx.Subnet == constants.PrimaryNetworkID && - tx.Validator.NodeID == e.backend.Ctx.NodeID { - e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", - zap.String("reason", "primary network is not being fully synced"), - zap.Stringer("txID", txID), - zap.String("txType", "addPermissionlessValidator"), - zap.Stringer("nodeID", tx.Validator.NodeID), - ) - } - - return nil -} - -func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - if err := verifyAddPermissionlessDelegatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - return nil -} - -// Verifies a [*txs.TransferSubnetOwnershipTx] and, if it passes, executes it on -// [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. -// This transaction will result in the ownership of [tx.Subnet] being transferred -// to [tx.Owner]. -func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - err := verifyTransferSubnetOwnershipTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ) - if err != nil { - return err - } - - e.state.SetSubnetOwner(tx.Subnet, tx.Owner) - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - return nil -} - -func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { - var ( - currentTimestamp = e.state.GetTimestamp() - upgrades = e.backend.Config.UpgradeConfig - ) - if !upgrades.IsDurangoActivated(currentTimestamp) { - return ErrDurangoUpgradeNotActive - } - - // Verify the tx is well-formed - if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { - return err - } - - if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { - return err - } - - // Verify the flowcheck - fee, err := e.feeCalculator.CalculateFee(tx) - if err != nil { - return err - } - if err := e.backend.FlowChecker.VerifySpend( - tx, - e.state, - tx.Ins, - tx.Outs, - e.tx.Creds, - map[ids.ID]uint64{ - e.backend.Ctx.AVAXAssetID: fee, - }, - ); err != nil { - return err - } - - txID := e.tx.ID() - // Consume the UTXOS - avax.Consume(e.state, tx.Ins) - // Produce the UTXOS - avax.Produce(e.state, txID, tx.Outs) - return nil -} - // Creates the staker as defined in [stakerTx] and adds it to [e.State]. func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 5683ac6b3b6a..e1891a3d51a1 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -83,29 +83,6 @@ const ( var ( _ txs.Visitor = (*complexityVisitor)(nil) - IntrinsicAddPermissionlessValidatorTxComplexities = gas.Dimensions{ - gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - intrinsicValidatorBandwidth + // validator - ids.IDLen + // subnetID - wrappers.IntLen + // signer typeID - wrappers.IntLen + // num stake outs - wrappers.IntLen + // validator rewards typeID - wrappers.IntLen + // delegator rewards typeID - wrappers.IntLen, // delegation shares - gas.DBRead: 1, - gas.DBWrite: 1, - gas.Compute: 0, - } - IntrinsicAddPermissionlessDelegatorTxComplexities = gas.Dimensions{ - gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - intrinsicValidatorBandwidth + // validator - ids.IDLen + // subnetID - wrappers.IntLen + // num stake outs - wrappers.IntLen, // delegator rewards typeID - gas.DBRead: 1, - gas.DBWrite: 1, - gas.Compute: 0, - } IntrinsicAddSubnetValidatorTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + intrinsicSubnetValidatorBandwidth + // subnetValidator @@ -115,19 +92,6 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } - IntrinsicBaseTxComplexities = gas.Dimensions{ - gas.Bandwidth: codec.VersionSize + // codecVersion - wrappers.IntLen + // typeID - wrappers.IntLen + // networkID - ids.IDLen + // blockchainID - wrappers.IntLen + // number of outputs - wrappers.IntLen + // number of inputs - wrappers.IntLen + // length of memo - wrappers.IntLen, // number of credentials - gas.DBRead: 0, - gas.DBWrite: 0, - gas.Compute: 0, - } IntrinsicCreateChainTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + ids.IDLen + // subnetID @@ -148,18 +112,18 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } - IntrinsicExportTxComplexities = gas.Dimensions{ + IntrinsicImportTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - ids.IDLen + // destination chainID - wrappers.IntLen, // num exported outputs + ids.IDLen + // source chainID + wrappers.IntLen, // num importing inputs gas.DBRead: 0, gas.DBWrite: 0, gas.Compute: 0, } - IntrinsicImportTxComplexities = gas.Dimensions{ + IntrinsicExportTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - ids.IDLen + // source chainID - wrappers.IntLen, // num importing inputs + ids.IDLen + // destination chainID + wrappers.IntLen, // num exported outputs gas.DBRead: 0, gas.DBWrite: 0, gas.Compute: 0, @@ -174,6 +138,29 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } + IntrinsicAddPermissionlessValidatorTxComplexities = gas.Dimensions{ + gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + + intrinsicValidatorBandwidth + // validator + ids.IDLen + // subnetID + wrappers.IntLen + // signer typeID + wrappers.IntLen + // num stake outs + wrappers.IntLen + // validator rewards typeID + wrappers.IntLen + // delegator rewards typeID + wrappers.IntLen, // delegation shares + gas.DBRead: 1, + gas.DBWrite: 1, + gas.Compute: 0, + } + IntrinsicAddPermissionlessDelegatorTxComplexities = gas.Dimensions{ + gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + + intrinsicValidatorBandwidth + // validator + ids.IDLen + // subnetID + wrappers.IntLen + // num stake outs + wrappers.IntLen, // delegator rewards typeID + gas.DBRead: 1, + gas.DBWrite: 1, + gas.Compute: 0, + } IntrinsicTransferSubnetOwnershipTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + ids.IDLen + // subnetID @@ -184,6 +171,19 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } + IntrinsicBaseTxComplexities = gas.Dimensions{ + gas.Bandwidth: codec.VersionSize + // codecVersion + wrappers.IntLen + // typeID + wrappers.IntLen + // networkID + ids.IDLen + // blockchainID + wrappers.IntLen + // number of outputs + wrappers.IntLen + // number of inputs + wrappers.IntLen + // length of memo + wrappers.IntLen, // number of credentials + gas.DBRead: 0, + gas.DBWrite: 0, + gas.Compute: 0, + } IntrinsicConvertSubnetTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + ids.IDLen + // subnetID @@ -440,11 +440,11 @@ type complexityVisitor struct { output gas.Dimensions } -func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { return ErrUnsupportedTx } -func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { +func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { return ErrUnsupportedTx } @@ -460,60 +460,6 @@ func (*complexityVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrUnsupportedTx } -func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - signerComplexity, err := SignerComplexity(tx.Signer) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) - if err != nil { - return err - } - delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( - &baseTxComplexity, - &signerComplexity, - &outputsComplexity, - &validatorOwnerComplexity, - &delegatorOwnerComplexity, - ) - return err -} - -func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( - &baseTxComplexity, - &ownerComplexity, - &outputsComplexity, - ) - return err -} - func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { @@ -530,15 +476,6 @@ func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) e return err } -func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { - baseTxComplexity, err := baseTxComplexity(tx) - if err != nil { - return err - } - c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) - return err -} - func (c *complexityVisitor) CreateChainTx(tx *txs.CreateChainTx) error { bandwidth, err := math.Mul(uint64(len(tx.FxIDs)), ids.IDLen) if err != nil { @@ -591,6 +528,23 @@ func (c *complexityVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return err } +func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + // TODO: Should imported inputs be more complex? + inputsComplexity, err := InputComplexity(tx.ImportedInputs...) + if err != nil { + return err + } + c.output, err = IntrinsicImportTxComplexities.Add( + &baseTxComplexity, + &inputsComplexity, + ) + return err +} + func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { @@ -608,35 +562,72 @@ func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { return err } -func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { +func (c *complexityVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - // TODO: Should imported inputs be more complex? - inputsComplexity, err := InputComplexity(tx.ImportedInputs...) + authComplexity, err := AuthComplexity(tx.SubnetAuth) if err != nil { return err } - c.output, err = IntrinsicImportTxComplexities.Add( + c.output, err = IntrinsicRemoveSubnetValidatorTxComplexities.Add( &baseTxComplexity, - &inputsComplexity, + &authComplexity, ) return err } -func (c *complexityVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { +func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + // TODO: Should we include additional complexity for subnets? baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - authComplexity, err := AuthComplexity(tx.SubnetAuth) + signerComplexity, err := SignerComplexity(tx.Signer) if err != nil { return err } - c.output, err = IntrinsicRemoveSubnetValidatorTxComplexities.Add( + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) + if err != nil { + return err + } + delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( &baseTxComplexity, - &authComplexity, + &signerComplexity, + &outputsComplexity, + &validatorOwnerComplexity, + &delegatorOwnerComplexity, + ) + return err +} + +func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + // TODO: Should we include additional complexity for subnets? + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) + if err != nil { + return err + } + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( + &baseTxComplexity, + &ownerComplexity, + &outputsComplexity, ) return err } @@ -662,6 +653,15 @@ func (c *complexityVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwne return err } +func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { + baseTxComplexity, err := baseTxComplexity(tx) + if err != nil { + return err + } + c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) + return err +} + func (c *complexityVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { diff --git a/vms/platformvm/txs/fee/static_calculator.go b/vms/platformvm/txs/fee/static_calculator.go index 888ccba8621c..1b97349ce2cb 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -76,21 +76,26 @@ func (c *staticVisitor) CreateSubnetTx(*txs.CreateSubnetTx) error { return nil } -func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (c *staticVisitor) ImportTx(*txs.ImportTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { - c.fee = c.config.TransformSubnetTxFee +func (c *staticVisitor) ExportTx(*txs.ExportTx) error { + c.fee = c.config.TxFee return nil } -func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { c.fee = c.config.TxFee return nil } +func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { + c.fee = c.config.TransformSubnetTxFee + return nil +} + func (c *staticVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if tx.Subnet != constants.PrimaryNetworkID { c.fee = c.config.AddSubnetValidatorFee @@ -109,17 +114,12 @@ func (c *staticVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDe return nil } -func (c *staticVisitor) BaseTx(*txs.BaseTx) error { - c.fee = c.config.TxFee - return nil -} - -func (c *staticVisitor) ImportTx(*txs.ImportTx) error { +func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) ExportTx(*txs.ExportTx) error { +func (c *staticVisitor) BaseTx(*txs.BaseTx) error { c.fee = c.config.TxFee return nil } diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 142a6115d7af..21c46476fa5f 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -5,6 +5,7 @@ package txs // Allow vm to execute custom logic against the underlying transaction types. type Visitor interface { + // Apricot Transactions: AddValidatorTx(*AddValidatorTx) error AddSubnetValidatorTx(*AddSubnetValidatorTx) error AddDelegatorTx(*AddDelegatorTx) error @@ -14,11 +15,17 @@ type Visitor interface { ExportTx(*ExportTx) error AdvanceTimeTx(*AdvanceTimeTx) error RewardValidatorTx(*RewardValidatorTx) error + + // Banff Transactions: RemoveSubnetValidatorTx(*RemoveSubnetValidatorTx) error TransformSubnetTx(*TransformSubnetTx) error AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error + + // Durango Transactions: TransferSubnetOwnershipTx(*TransferSubnetOwnershipTx) error - ConvertSubnetTx(*ConvertSubnetTx) error BaseTx(*BaseTx) error + + // Etna Transactions: + ConvertSubnetTx(*ConvertSubnetTx) error } diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index 38d501c908b0..b358e1f5d5ea 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -51,14 +51,6 @@ func (*visitor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrUnsupportedTxType } -func (s *visitor) BaseTx(tx *txs.BaseTx) error { - txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) - if err != nil { - return err - } - return sign(s.tx, false, txSigners) -} - func (s *visitor) AddValidatorTx(tx *txs.AddValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { @@ -143,7 +135,7 @@ func (s *visitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error return sign(s.tx, true, txSigners) } -func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { +func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -156,20 +148,23 @@ func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) e return sign(s.tx, true, txSigners) } -func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { +func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + return sign(s.tx, true, txSigners) +} + +func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } -func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { +func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -182,19 +177,24 @@ func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return sign(s.tx, true, txSigners) } -func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { +func (s *visitor) BaseTx(tx *txs.BaseTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, false, txSigners) } -func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { +func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } + subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } diff --git a/wallet/chain/p/wallet/backend_visitor.go b/wallet/chain/p/wallet/backend_visitor.go index 8e44ee3b5e2d..f2f9e646edf8 100644 --- a/wallet/chain/p/wallet/backend_visitor.go +++ b/wallet/chain/p/wallet/backend_visitor.go @@ -58,26 +58,6 @@ func (b *backendVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return b.baseTx(&tx.BaseTx) } -func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - b.b.setSubnetOwner( - tx.Subnet, - tx.Owner, - ) - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { - return b.baseTx(tx) -} - func (b *backendVisitor) ImportTx(tx *txs.ImportTx) error { err := b.b.removeUTXOs( b.ctx, @@ -111,6 +91,10 @@ func (b *backendVisitor) ExportTx(tx *txs.ExportTx) error { return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return b.baseTx(&tx.BaseTx) } @@ -123,6 +107,22 @@ func (b *backendVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessD return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + b.b.setSubnetOwner( + tx.Subnet, + tx.Owner, + ) + return b.baseTx(&tx.BaseTx) +} + +func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { + return b.baseTx(tx) +} + +func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) baseTx(tx *txs.BaseTx) error { return b.b.removeUTXOs( b.ctx, From efa35a4275eb04220a625d9b522f6d96c7b41a10 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 13:28:31 -0500 Subject: [PATCH 147/184] nit --- wallet/chain/p/builder/builder.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index 80936c30fed6..e2b95c185b13 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -889,13 +889,13 @@ func (b *builder) NewRegisterSubnetValidatorTx( b.context.AVAXAssetID: balance, } toStake = map[ids.ID]uint64{} - ) - ops := common.NewOptions(options) - memo := ops.Memo() - memoComplexity := gas.Dimensions{ - gas.Bandwidth: uint64(len(memo)), - } + ops = common.NewOptions(options) + memo = ops.Memo() + memoComplexity = gas.Dimensions{ + gas.Bandwidth: uint64(len(memo)), + } + ) warpComplexity, err := fee.WarpComplexity(message) if err != nil { return nil, err From 031b989b8e7b68280d8bd3904037064289a328f1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 13:31:39 -0500 Subject: [PATCH 148/184] nit --- wallet/chain/p/builder/builder.go | 2 +- wallet/chain/p/wallet/wallet.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index e2b95c185b13..8a429fbe24c3 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -165,7 +165,7 @@ type Builder interface { options ...common.Option, ) (*txs.ConvertSubnetTx, error) - // RegisterSubnetValidatorTx adds a validator to a Permissionless L1. + // RegisterSubnetValidatorTx adds a validator to an L1. // // - [balance] that the validator should allocate to continuous fees // - [proofOfPossession] is the BLS PoP for the key included in the Warp diff --git a/wallet/chain/p/wallet/wallet.go b/wallet/chain/p/wallet/wallet.go index 21b60abd5d4f..fd6fd9a782e0 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -152,7 +152,7 @@ type Wallet interface { ) (*txs.Tx, error) // IssueRegisterSubnetValidatorTx creates, signs, and issues a transaction - // that adds a validator to a Permissionless L1. + // that adds a validator to an L1. // // - [balance] that the validator should allocate to continuous fees // - [proofOfPossession] is the BLS PoP for the key included in the Warp From b28affedf634342ebcfa157f5be798947fa8ee3a Mon Sep 17 00:00:00 2001 From: marun Date: Thu, 7 Nov 2024 19:49:50 +0100 Subject: [PATCH 149/184] [tmpnet] Misc cleanup for monitoring tooling (#3527) --- scripts/configure-local-metrics-collection.sh | 7 +++- scripts/run_prometheus.sh | 34 ++++++++++--------- scripts/run_promtail.sh | 26 +++++++------- tests/fixture/tmpnet/README.md | 6 ++++ tests/fixture/tmpnet/network.go | 11 ++++-- 5 files changed, 53 insertions(+), 31 deletions(-) diff --git a/scripts/configure-local-metrics-collection.sh b/scripts/configure-local-metrics-collection.sh index 1b86f98b4050..4ba3027be509 100755 --- a/scripts/configure-local-metrics-collection.sh +++ b/scripts/configure-local-metrics-collection.sh @@ -51,4 +51,9 @@ echo "Wrote promtail configuration to ${PROMTAIL_CONFIG_FILE}" echo "Metrics collection by prometheus can be started with ./scripts/run_prometheus.sh" echo "Log collection by promtail can be started with ./scripts/run_promtail.sh" -echo "Grafana link: https://grafana-poc.avax-dev.network/d/kBQpRdWnk/avalanche-main-dashboard?var-filter=network_uuid%7C%3D%7C${METRICS_UUID}" + +GRAFANA_LINK="https://grafana-poc.avax-dev.network/d/kBQpRdWnk/avalanche-main-dashboard?var-filter=network_uuid%7C%3D%7C${METRICS_UUID}" +METRICS_PATH="${HOME}/.avalanchego/metrics.txt" +echo "${GRAFANA_LINK}" > "${METRICS_PATH}" +echo "Metrics and logs can be viewed at: ${GRAFANA_LINK}" +echo "Link also saved to ${METRICS_PATH}" diff --git a/scripts/run_prometheus.sh b/scripts/run_prometheus.sh index dce9ef1e30cb..f5d917c23a01 100755 --- a/scripts/run_prometheus.sh +++ b/scripts/run_prometheus.sh @@ -2,17 +2,16 @@ set -euo pipefail -# Starts a prometheus instance in agent-mode, forwarding to a central -# instance. Intended to enable metrics collection from temporary networks running -# locally and in CI. +# - Starts a prometheus instance in agent-mode to collect metrics from nodes running +# locally and in CI. # -# The prometheus instance will remain running in the background and will forward -# metrics to the central instance for all tmpnet networks. +# - promtail will remain running in the background and will forward metrics to the +# specified prometheus endpoint. # -# To stop it: -# -# $ kill -9 `cat ~/.tmpnet/prometheus/run.pid` && rm ~/.tmpnet/prometheus/run.pid +# - Each node is configured with a file written to ~/.tmpnet/prometheus/file_sd_configs # +# - To stop the running instance: +# $ kill -9 `cat ~/.tmpnet/promtheus/run.pid` && rm ~/.tmpnet/promtail/run.pid # e.g., # PROMETHEUS_ID= PROMETHEUS_PASSWORD= ./scripts/run_prometheus.sh @@ -45,7 +44,7 @@ fi PROMETHEUS_PASSWORD="${PROMETHEUS_PASSWORD:-}" if [[ -z "${PROMETHEUS_PASSWORD}" ]]; then - echo "Plase provide a value for PROMETHEUS_PASSWORD" + echo "Please provide a value for PROMETHEUS_PASSWORD" exit 1 fi @@ -64,13 +63,13 @@ if ! command -v "${CMD}" &> /dev/null; then # Determine the arch if which sw_vers &> /dev/null; then - echo "on macos, only amd64 binaries are available so rosetta is required on apple silicon machines." - echo "to avoid using rosetta, install via homebrew: brew install prometheus" + echo "On macos, only amd64 binaries are available so rosetta is required on apple silicon machines." + echo "To avoid using rosetta, install via homebrew: brew install prometheus" DIST=darwin else ARCH="$(uname -i)" if [[ "${ARCH}" != "x86_64" ]]; then - echo "on linux, only amd64 binaries are available. manual installation of prometheus is required." + echo "On linux, only amd64 binaries are available. manual installation of prometheus is required." exit 1 else DIST="linux" @@ -90,8 +89,8 @@ fi FILE_SD_PATH="${PROMETHEUS_WORKING_DIR}/file_sd_configs" mkdir -p "${FILE_SD_PATH}" -echo "writing configuration..." -cat >"${PROMETHEUS_WORKING_DIR}"/prometheus.yaml < "${CONFIG_PATH}" < prometheus.log 2>&1 & echo $! > "${PIDFILE}" -echo "running with pid $(cat "${PIDFILE}")" +echo "prometheus started with pid $(cat "${PIDFILE}")" +# shellcheck disable=SC2016 +echo 'To stop prometheus: "kill -SIGTERM `cat ~/.tmpnet/prometheus/run.pid` && rm ~/.tmpnet/prometheus/run.pid"' diff --git a/scripts/run_promtail.sh b/scripts/run_promtail.sh index 5811dcf06f2f..465a183b7452 100755 --- a/scripts/run_promtail.sh +++ b/scripts/run_promtail.sh @@ -2,16 +2,15 @@ set -euo pipefail -# Starts a promtail instance to collect logs from temporary networks -# running locally and in CI. +# - Starts a promtail instance to collect logs from nodes running locally and in CI. # -# The promtail instance will remain running in the background and will forward -# logs to the central instance for all tmpnet networks. +# - promtail will remain running in the background and will forward logs to the +# specified Loki endpoint. # -# To stop it: -# -# $ kill -9 `cat ~/.tmpnet/promtail/run.pid` && rm ~/.tmpnet/promtail/run.pid +# - Each node is configured with a file written to ~/.tmpnet/promtail/file_sd_configs/ # +# - To stop the running instance: +# $ kill -9 `cat ~/.tmpnet/promtail/run.pid` && rm ~/.tmpnet/promtail/run.pid # e.g., # LOKI_ID= LOKI_PASSWORD= ./scripts/run_promtail.sh @@ -44,7 +43,7 @@ fi LOKI_PASSWORD="${LOKI_PASSWORD:-}" if [[ -z "${LOKI_PASSWORD}" ]]; then - echo "Plase provide a value for LOKI_PASSWORD" + echo "Please provide a value for LOKI_PASSWORD" exit 1 fi @@ -86,8 +85,8 @@ fi FILE_SD_PATH="${PROMTAIL_WORKING_DIR}/file_sd_configs" mkdir -p "${FILE_SD_PATH}" -echo "writing configuration..." -cat >"${PROMTAIL_WORKING_DIR}"/promtail.yaml < "${CONFIG_PATH}" < promtail.log 2>&1 & echo $! > "${PIDFILE}" -echo "running with pid $(cat "${PIDFILE}")" +echo "promtail started with pid $(cat "${PIDFILE}")" +# shellcheck disable=SC2016 +echo 'To stop promtail: "kill -SIGTERM `cat ~/.tmpnet/promtail/run.pid` && rm ~/.tmpnet/promtail/run.pid"' diff --git a/tests/fixture/tmpnet/README.md b/tests/fixture/tmpnet/README.md index e48deef3b34b..718ae065a812 100644 --- a/tests/fixture/tmpnet/README.md +++ b/tests/fixture/tmpnet/README.md @@ -167,6 +167,7 @@ HOME │ └── config.json // Custom chain configuration for all nodes ├── config.json // Common configuration (including defaults and pre-funded keys) ├── genesis.json // Genesis for all nodes + ├── metrics.txt // Link for metrics and logs collected from the network (see: Monitoring) ├── network.env // Sets network dir env var to simplify network usage └── subnets // Directory containing subnet config for both avalanchego and tmpnet ├── subnet-a.json // tmpnet configuration for subnet-a and its chain(s) @@ -269,6 +270,11 @@ LOKI_ID= LOKI_PASSWORD= ./scripts/run_promtail.sh # Network start emits link to grafana displaying collected logs and metrics ./build/tmpnetctl start-network + +# Configure metrics collection from a local node binding to the default API +# port of 9650 and storing its logs in ~/.avalanchego/logs. The script will +# also emit a link to grafana. +./scripts/configure-local-metrics-collection.sh ``` ### Metrics collection diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go index 690386c87534..2160c8ad072e 100644 --- a/tests/fixture/tmpnet/network.go +++ b/tests/fixture/tmpnet/network.go @@ -373,10 +373,17 @@ func (n *Network) StartNodes(ctx context.Context, w io.Writer, nodesToStart ...* if _, err := fmt.Fprintf(w, "\nStarted network %s (UUID: %s)\n", n.Dir, n.UUID); err != nil { return err } - // Provide a link to the main dashboard filtered by the uuid and showing results from now till whenever the link is viewed + + // Generate a link to the main dashboard filtered by the uuid and showing results from now till whenever the link is viewed startTimeStr := strconv.FormatInt(startTime.UnixMilli(), 10) metricsURL := MetricsLinkForNetwork(n.UUID, startTimeStr, "") - if _, err := fmt.Fprintf(w, "\nMetrics: %s\n", metricsURL); err != nil { + + // Write link to the network path and to stdout + metricsPath := filepath.Join(n.Dir, "metrics.txt") + if err := os.WriteFile(metricsPath, []byte(metricsURL+"\n"), perms.ReadWrite); err != nil { + return fmt.Errorf("failed to write metrics link to %s: %w", metricsPath, err) + } + if _, err := fmt.Fprintf(w, "\nMetrics: %s\nLink also saved to %s\n", metricsURL, metricsPath); err != nil { return err } From a3f61865a13f70808a70c0593a4752a5e895cf19 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 13:53:29 -0500 Subject: [PATCH 150/184] Add tx builder test --- wallet/chain/p/builder_test.go | 102 +++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/wallet/chain/p/builder_test.go b/wallet/chain/p/builder_test.go index f56a0eb0896b..bc03f30814e9 100644 --- a/wallet/chain/p/builder_test.go +++ b/wallet/chain/p/builder_test.go @@ -26,7 +26,9 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/vms/types" "github.com/ava-labs/avalanchego/wallet/chain/p/builder" @@ -747,6 +749,106 @@ func TestConvertSubnetTx(t *testing.T) { } } +func TestRegisterSubnetValidatorTx(t *testing.T) { + const ( + expiry = 1731005097 + weight = 7905001371 + + balance = units.Avax + ) + + sk, err := bls.NewSecretKey() + require.NoError(t, err) + pop := signer.NewProofOfPossession(sk) + + addressedCallPayload, err := message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + expiry, + message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + weight, + ) + require.NoError(t, err) + + addressedCall, err := payload.NewAddressedCall( + utils.RandomBytes(20), + addressedCallPayload.Bytes(), + ) + require.NoError(t, err) + + unsignedWarp, err := warp.NewUnsignedMessage( + constants.UnitTestID, + ids.GenerateTestID(), + addressedCall.Bytes(), + ) + require.NoError(t, err) + + signers := set.NewBits(0) + + unsignedBytes := unsignedWarp.Bytes() + sig := bls.Sign(sk, unsignedBytes) + sigBytes := [bls.SignatureLen]byte{} + copy(sigBytes[:], bls.SignatureToBytes(sig)) + + warp, err := warp.NewMessage( + unsignedWarp, + &warp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: sigBytes, + }, + ) + require.NoError(t, err) + warpMessageBytes := warp.Bytes() + + for _, e := range testEnvironmentPostEtna { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = wallet.NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr), e.context, backend) + ) + + utx, err := builder.NewRegisterSubnetValidatorTx( + balance, + pop.ProofOfPossession, + warpMessageBytes, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(balance, utx.Balance) + require.Equal(pop.ProofOfPossession, utx.ProofOfPossession) + require.Equal(types.JSONByteSlice(warpMessageBytes), utx.Message) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + map[ids.ID]uint64{ + e.context.AVAXAssetID: balance, // Balance of the validator + }, + ) + }) + } +} + func makeTestUTXOs(utxosKey *secp256k1.PrivateKey) []*avax.UTXO { // Note: we avoid ids.GenerateTestNodeID here to make sure that UTXO IDs // won't change run by run. This simplifies checking what utxos are included From f7678005a7961c890cbd12ab4a8f00fbc66bced0 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 14:16:22 -0500 Subject: [PATCH 151/184] add comments --- vms/platformvm/validators/manager.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index b90294206f98..6e620bdc7c70 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -26,9 +26,17 @@ import ( ) const ( + // MaxRecentlyAcceptedWindowSize is the maximum number of blocks that the + // recommended minimum height will lag behind the last accepted block. MaxRecentlyAcceptedWindowSize = 64 + // MinRecentlyAcceptedWindowSize is the minimum number of blocks that the + // recommended minimum height will lag behind the last accepted block. MinRecentlyAcceptedWindowSize = 0 - RecentlyAcceptedWindowTTL = 30 * time.Second + // RecentlyAcceptedWindowTTL is the amount of time after a block is accepted + // to avoid recommending it as the minimum height. If the evicting, or not + // evicting, the block would violate either the maxiumum or minimum window + // size, the block will not be evicted. + RecentlyAcceptedWindowTTL = 30 * time.Second validatorSetsCacheSize = 64 ) From 60e1a9f4b5c4fe40c4435350ace0d2b98a93259e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 14:33:15 -0500 Subject: [PATCH 152/184] Remove P-chain txsmock package (#3528) --- scripts/mocks.mockgen.source.txt | 1 - .../block/executor/verifier_test.go | 637 +++++++++--------- vms/platformvm/txs/txsmock/unsigned_tx.go | 138 ---- 3 files changed, 301 insertions(+), 475 deletions(-) delete mode 100644 vms/platformvm/txs/txsmock/unsigned_tx.go diff --git a/scripts/mocks.mockgen.source.txt b/scripts/mocks.mockgen.source.txt index 93f4c2fb5127..10cc5a678ebf 100644 --- a/scripts/mocks.mockgen.source.txt +++ b/scripts/mocks.mockgen.source.txt @@ -10,5 +10,4 @@ vms/platformvm/signer/signer.go==Signer=vms/platformvm/signer/signermock/signer. vms/platformvm/state/diff.go==MockDiff=vms/platformvm/state/mock_diff.go vms/platformvm/state/state.go=Chain=MockState=vms/platformvm/state/mock_state.go vms/platformvm/state/state.go=State=MockChain=vms/platformvm/state/mock_chain.go -vms/platformvm/txs/unsigned_tx.go==UnsignedTx=vms/platformvm/txs/txsmock/unsigned_tx.go vms/proposervm/block.go=Block=MockPostForkBlock=vms/proposervm/mock_post_fork_block.go diff --git a/vms/platformvm/block/executor/verifier_test.go b/vms/platformvm/block/executor/verifier_test.go index 0a5d9e3c662f..4de3a6c8b2dd 100644 --- a/vms/platformvm/block/executor/verifier_test.go +++ b/vms/platformvm/block/executor/verifier_test.go @@ -14,10 +14,13 @@ import ( "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/upgrade" "github.com/ava-labs/avalanchego/upgrade/upgradetest" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" @@ -40,7 +43,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/executor" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool" "github.com/ava-labs/avalanchego/vms/platformvm/txs/mempool/mempoolmock" - "github.com/ava-labs/avalanchego/vms/platformvm/txs/txsmock" "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -49,17 +51,36 @@ import ( validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) -func newTestVerifier(t testing.TB, s state.State) *verifier { +type testVerifierConfig struct { + DB database.Database + Upgrades upgrade.Config + Context *snow.Context +} + +func newTestVerifier(t testing.TB, c testVerifierConfig) *verifier { require := require.New(t) + if c.DB == nil { + c.DB = memdb.New() + } + if c.Upgrades == (upgrade.Config{}) { + c.Upgrades = upgradetest.GetConfig(upgradetest.Latest) + } + if c.Context == nil { + c.Context = snowtest.Context(t, constants.PlatformChainID) + } + mempool, err := mempool.New("", prometheus.NewRegistry(), nil) require.NoError(err) var ( - upgrades = upgradetest.GetConfig(upgradetest.Latest) - ctx = snowtest.Context(t, constants.PlatformChainID) - clock = &mockable.Clock{} - fx = &secp256k1fx.Fx{} + state = statetest.New(t, statetest.Config{ + DB: c.DB, + Upgrades: c.Upgrades, + Context: c.Context, + }) + clock = &mockable.Clock{} + fx = &secp256k1fx.Fx{} ) require.NoError(fx.InitializeVM(&secp256k1fx.TestVM{ Clk: *clock, @@ -69,10 +90,10 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { return &verifier{ backend: &backend{ Mempool: mempool, - lastAccepted: s.GetLastAccepted(), + lastAccepted: state.GetLastAccepted(), blkIDToState: make(map[ids.ID]*blockState), - state: s, - ctx: ctx, + state: state, + ctx: c.Context, }, txExecutorBackend: &executor.Backend{ Config: &config.Config{ @@ -81,13 +102,13 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, SybilProtectionEnabled: true, - UpgradeConfig: upgrades, + UpgradeConfig: c.Upgrades, }, - Ctx: ctx, + Ctx: c.Context, Clk: clock, Fx: fx, FlowChecker: utxo.NewVerifier( - ctx, + c.Context, clock, fx, ), @@ -97,271 +118,314 @@ func newTestVerifier(t testing.TB, s state.State) *verifier { } func TestVerifierVisitProposalBlock(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentOnAcceptState := state.NewMockDiff(ctrl) - timestamp := time.Now() - // One call for each of onCommitState and onAbortState. - parentOnAcceptState.EXPECT().GetTimestamp().Return(timestamp).Times(2) - parentOnAcceptState.EXPECT().GetFeeState().Return(gas.State{}).Times(2) - parentOnAcceptState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(2) - parentOnAcceptState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(2) - parentOnAcceptState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(2) - - backend := &backend{ - lastAccepted: parentID, - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentOnAcceptState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + var ( + require = require.New(t) + verifier = newTestVerifier(t, testVerifierConfig{ + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + }) + initialTimestamp = verifier.state.GetTimestamp() + newTimestamp = initialTimestamp.Add(time.Second) + proposalTx = &txs.Tx{ + Unsigned: &txs.AdvanceTimeTx{ + Time: uint64(newTimestamp.Unix()), }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + ) + require.NoError(proposalTx.Initialize(txs.Codec)) - blkTx := txsmock.NewUnsignedTx(ctrl) - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.ProposalTxExecutor{})).Return(nil).Times(1) + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotProposalBlock( - parentID, - 2, - &txs.Tx{ - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, + proposalBlock, err := block.NewApricotProposalBlock( + lastAcceptedID, + lastAccepted.Height()+1, + proposalTx, ) require.NoError(err) - apricotBlk.Tx.Unsigned = blkTx - // Set expectations for dependencies. - tx := apricotBlk.Txs()[0] - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - mempool.EXPECT().Remove([]*txs.Tx{tx}).Times(1) + // Execute the block. + require.NoError(proposalBlock.Visit(verifier)) - // Visit the block - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(timestamp, gotBlkState.timestamp) + // Verify that the block's execution was recorded as expected. + blkID := proposalBlock.ID() + require.Contains(verifier.blkIDToState, blkID) + executedBlockState := verifier.blkIDToState[blkID] + + txID := proposalTx.ID() - // Assert that the expected tx statuses are set. - _, gotStatus, err := gotBlkState.onCommitState.GetTx(tx.ID()) + onCommit := executedBlockState.onCommitState + require.NotNil(onCommit) + acceptedTx, acceptedStatus, err := onCommit.GetTx(txID) require.NoError(err) - require.Equal(status.Committed, gotStatus) + require.Equal(proposalTx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) - _, gotStatus, err = gotBlkState.onAbortState.GetTx(tx.ID()) + onAbort := executedBlockState.onAbortState + require.NotNil(onAbort) + acceptedTx, acceptedStatus, err = onAbort.GetTx(txID) require.NoError(err) - require.Equal(status.Aborted, gotStatus) + require.Equal(proposalTx, acceptedTx) + require.Equal(status.Aborted, acceptedStatus) + + require.Equal( + &blockState{ + proposalBlockState: proposalBlockState{ + onCommitState: onCommit, + onAbortState: onAbort, + }, + statelessBlock: proposalBlock, - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + timestamp: initialTimestamp, + verifiedHeights: set.Of[uint64](0), + }, + executedBlockState, + ) } func TestVerifierVisitAtomicBlock(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - grandparentID := ids.GenerateTestID() - parentState := state.NewMockDiff(ctrl) - - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + var ( + require = require.New(t) + verifier = newTestVerifier(t, testVerifierConfig{ + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhase4), + }) + wallet = txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + verifier.state, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + nil, // chainIDs + ) + exportedOutput = &avax.TransferableOutput{ + Asset: avax.Asset{ID: verifier.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: units.NanoAvax, + OutputOwners: secp256k1fx.OutputOwners{}, }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + initialTimestamp = verifier.state.GetTimestamp() + ) - onAccept := state.NewMockDiff(ctrl) - blkTx := txsmock.NewUnsignedTx(ctrl) - inputs := set.Of(ids.GenerateTestID()) - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.AtomicTxExecutor{})).DoAndReturn( - func(e *executor.AtomicTxExecutor) error { - e.OnAccept = onAccept - e.Inputs = inputs - return nil + // Build the transaction that will be executed. + atomicTx, err := wallet.IssueExportTx( + verifier.ctx.XChainID, + []*avax.TransferableOutput{ + exportedOutput, }, - ).Times(1) + ) + require.NoError(err) - // We can't serialize [blkTx] because it isn't registered with blocks.Codec. - // Serialize this block with a dummy tx and replace it after creation with - // the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotAtomicBlock( - parentID, - 2, - &txs.Tx{ - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) + + atomicBlock, err := block.NewApricotAtomicBlock( + lastAcceptedID, + lastAccepted.Height()+1, + atomicTx, ) require.NoError(err) - apricotBlk.Tx.Unsigned = blkTx - // Set expectations for dependencies. - timestamp := time.Now() - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - parentStatelessBlk.EXPECT().Parent().Return(grandparentID).Times(1) - mempool.EXPECT().Remove([]*txs.Tx{apricotBlk.Tx}).Times(1) - onAccept.EXPECT().AddTx(apricotBlk.Tx, status.Committed).Times(1) - onAccept.EXPECT().GetTimestamp().Return(timestamp).Times(1) + // Execute the block. + require.NoError(atomicBlock.Visit(verifier)) - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) + // Verify that the block's execution was recorded as expected. + blkID := atomicBlock.ID() + require.Contains(verifier.blkIDToState, blkID) + atomicBlockState := verifier.blkIDToState[blkID] + onAccept := atomicBlockState.onAcceptState + require.NotNil(onAccept) - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(onAccept, gotBlkState.onAcceptState) - require.Equal(inputs, gotBlkState.inputs) - require.Equal(timestamp, gotBlkState.timestamp) + txID := atomicTx.ID() + acceptedTx, acceptedStatus, err := onAccept.GetTx(txID) + require.NoError(err) + require.Equal(atomicTx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + exportedUTXO := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(len(atomicTx.UTXOs())), + }, + Asset: exportedOutput.Asset, + Out: exportedOutput.Out, + } + exportedUTXOID := exportedUTXO.InputID() + exportedUTXOBytes, err := txs.Codec.Marshal(txs.CodecVersion, exportedUTXO) + require.NoError(err) + + require.Equal( + &blockState{ + statelessBlock: atomicBlock, + + onAcceptState: onAccept, + + timestamp: initialTimestamp, + atomicRequests: map[ids.ID]*atomic.Requests{ + verifier.ctx.XChainID: { + PutRequests: []*atomic.Element{ + { + Key: exportedUTXOID[:], + Value: exportedUTXOBytes, + Traits: [][]byte{}, + }, + }, + }, + }, + verifiedHeights: set.Of[uint64](0), + }, + atomicBlockState, + ) } func TestVerifierVisitStandardBlock(t *testing.T) { require := require.New(t) - ctrl := gomock.NewController(t) - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentState := state.NewMockDiff(ctrl) + var ( + ctx = snowtest.Context(t, constants.PlatformChainID) - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, + baseDB = memdb.New() + stateDB = prefixdb.New([]byte{0}, baseDB) + amDB = prefixdb.New([]byte{1}, baseDB) + + am = atomic.NewMemory(amDB) + sm = am.NewSharedMemory(ctx.ChainID) + xChainSM = am.NewSharedMemory(ctx.XChainID) + + owner = secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{genesis.EWOQKey.Address()}, + } + utxo = &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: ids.GenerateTestID(), + OutputIndex: 1, }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - manager := &manager{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: units.Avax, + OutputOwners: owner, }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } + } + ) - blkTx := txsmock.NewUnsignedTx(ctrl) - atomicRequests := map[ids.ID]*atomic.Requests{ - ids.GenerateTestID(): { - RemoveRequests: [][]byte{{1}, {2}}, + inputID := utxo.InputID() + utxoBytes, err := txs.Codec.Marshal(txs.CodecVersion, utxo) + require.NoError(err) + + require.NoError(xChainSM.Apply(map[ids.ID]*atomic.Requests{ + ctx.ChainID: { PutRequests: []*atomic.Element{ { - Key: []byte{3}, - Value: []byte{4}, - Traits: [][]byte{{5}, {6}}, + Key: inputID[:], + Value: utxoBytes, + Traits: [][]byte{ + genesis.EWOQKey.Address().Bytes(), + }, }, }, }, - } - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.StandardTxExecutor{})).DoAndReturn( - func(e *executor.StandardTxExecutor) error { - e.OnAccept = func() {} - e.Inputs = set.Set[ids.ID]{} - e.AtomicRequests = atomicRequests - return nil - }, - ).Times(1) - - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - apricotBlk, err := block.NewApricotStandardBlock( - parentID, - 2, /*height*/ - []*txs.Tx{ - { - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, - }, + })) + + ctx.SharedMemory = sm + + var ( + verifier = newTestVerifier(t, testVerifierConfig{ + DB: stateDB, + Upgrades: upgradetest.GetConfig(upgradetest.ApricotPhase5), + Context: ctx, + }) + wallet = txstest.NewWallet( + t, + verifier.ctx, + verifier.txExecutorBackend.Config, + verifier.state, + secp256k1fx.NewKeychain(genesis.EWOQKey), + nil, // subnetIDs + []ids.ID{ctx.XChainID}, // Read the UTXO to import + ) + initialTimestamp = verifier.state.GetTimestamp() + ) + + // Build the transaction that will be executed. + tx, err := wallet.IssueImportTx( + verifier.ctx.XChainID, + &owner, ) require.NoError(err) - apricotBlk.Transactions[0].Unsigned = blkTx - // Set expectations for dependencies. - timestamp := time.Now() - parentState.EXPECT().GetTimestamp().Return(timestamp).Times(1) - parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) - parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) - parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) - parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) - parentState.EXPECT().GetActiveSubnetOnlyValidatorsIterator().Return(&iterator.Empty[state.SubnetOnlyValidator]{}, nil).Times(1) - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - mempool.EXPECT().Remove(apricotBlk.Txs()).Times(1) + // Verify that the transaction is only consuming the imported UTXO. + require.Len(tx.InputIDs(), 1) - blk := manager.NewBlock(apricotBlk) - require.NoError(blk.Verify(context.Background())) + // Build the block that will be executed on top of the last accepted block. + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) + require.NoError(err) - // Assert expected state. - require.Contains(manager.backend.blkIDToState, apricotBlk.ID()) - gotBlkState := manager.backend.blkIDToState[apricotBlk.ID()] - require.Equal(apricotBlk, gotBlkState.statelessBlock) - require.Equal(set.Set[ids.ID]{}, gotBlkState.inputs) - require.Equal(timestamp, gotBlkState.timestamp) + firstBlock, err := block.NewApricotStandardBlock( + lastAcceptedID, + lastAccepted.Height()+1, + []*txs.Tx{tx}, + ) + require.NoError(err) - // Visiting again should return nil without using dependencies. - require.NoError(blk.Verify(context.Background())) + // Execute the block. + require.NoError(firstBlock.Visit(verifier)) + + // Verify that the block's execution was recorded as expected. + firstBlockID := firstBlock.ID() + { + require.Contains(verifier.blkIDToState, firstBlockID) + atomicBlockState := verifier.blkIDToState[firstBlockID] + onAccept := atomicBlockState.onAcceptState + require.NotNil(onAccept) + + txID := tx.ID() + acceptedTx, acceptedStatus, err := onAccept.GetTx(txID) + require.NoError(err) + require.Equal(tx, acceptedTx) + require.Equal(status.Committed, acceptedStatus) + + require.Equal( + &blockState{ + statelessBlock: firstBlock, + + onAcceptState: onAccept, + + inputs: tx.InputIDs(), + timestamp: initialTimestamp, + atomicRequests: map[ids.ID]*atomic.Requests{ + verifier.ctx.XChainID: { + RemoveRequests: [][]byte{ + inputID[:], + }, + }, + }, + verifiedHeights: set.Of[uint64](0), + }, + atomicBlockState, + ) + } + + // Verify that the import transaction can not be replayed. + { + secondBlock, err := block.NewApricotStandardBlock( + firstBlockID, + firstBlock.Height()+1, + []*txs.Tx{tx}, // Replay the prior transaction + ) + require.NoError(err) + + err = secondBlock.Visit(verifier) + require.ErrorIs(err, errConflictingParentTxs) + + // Verify that the block's execution was not recorded. + require.NotContains(verifier.blkIDToState, secondBlock.ID()) + } } func TestVerifierVisitCommitBlock(t *testing.T) { @@ -737,104 +801,6 @@ func TestBanffCommitBlockTimestampChecks(t *testing.T) { } } -func TestVerifierVisitStandardBlockWithDuplicateInputs(t *testing.T) { - require := require.New(t) - ctrl := gomock.NewController(t) - - // Create mocked dependencies. - s := state.NewMockState(ctrl) - mempool := mempoolmock.NewMempool(ctrl) - - grandParentID := ids.GenerateTestID() - grandParentStatelessBlk := block.NewMockBlock(ctrl) - grandParentState := state.NewMockDiff(ctrl) - parentID := ids.GenerateTestID() - parentStatelessBlk := block.NewMockBlock(ctrl) - parentState := state.NewMockDiff(ctrl) - atomicInputs := set.Of(ids.GenerateTestID()) - - backend := &backend{ - blkIDToState: map[ids.ID]*blockState{ - grandParentID: { - statelessBlock: grandParentStatelessBlk, - onAcceptState: grandParentState, - inputs: atomicInputs, - }, - parentID: { - statelessBlock: parentStatelessBlk, - onAcceptState: parentState, - }, - }, - Mempool: mempool, - state: s, - ctx: &snow.Context{ - Log: logging.NoLog{}, - }, - } - verifier := &verifier{ - txExecutorBackend: &executor.Backend{ - Config: &config.Config{ - UpgradeConfig: upgradetest.GetConfig(upgradetest.ApricotPhasePost6), - }, - Clk: &mockable.Clock{}, - }, - backend: backend, - } - - blkTx := txsmock.NewUnsignedTx(ctrl) - atomicRequests := map[ids.ID]*atomic.Requests{ - ids.GenerateTestID(): { - RemoveRequests: [][]byte{{1}, {2}}, - PutRequests: []*atomic.Element{ - { - Key: []byte{3}, - Value: []byte{4}, - Traits: [][]byte{{5}, {6}}, - }, - }, - }, - } - blkTx.EXPECT().Visit(gomock.AssignableToTypeOf(&executor.StandardTxExecutor{})).DoAndReturn( - func(e *executor.StandardTxExecutor) error { - e.OnAccept = func() {} - e.Inputs = atomicInputs - e.AtomicRequests = atomicRequests - return nil - }, - ).Times(1) - - // We can't serialize [blkTx] because it isn't - // registered with the blocks.Codec. - // Serialize this block with a dummy tx - // and replace it after creation with the mock tx. - // TODO allow serialization of mock txs. - blk, err := block.NewApricotStandardBlock( - parentID, - 2, - []*txs.Tx{ - { - Unsigned: &txs.AdvanceTimeTx{}, - Creds: []verify.Verifiable{}, - }, - }, - ) - require.NoError(err) - blk.Transactions[0].Unsigned = blkTx - - // Set expectations for dependencies. - timestamp := time.Now() - parentStatelessBlk.EXPECT().Height().Return(uint64(1)).Times(1) - parentState.EXPECT().GetTimestamp().Return(timestamp).Times(1) - parentState.EXPECT().GetFeeState().Return(gas.State{}).Times(1) - parentState.EXPECT().GetSoVExcess().Return(gas.Gas(0)).Times(1) - parentState.EXPECT().GetAccruedFees().Return(uint64(0)).Times(1) - parentState.EXPECT().NumActiveSubnetOnlyValidators().Return(0).Times(1) - parentStatelessBlk.EXPECT().Parent().Return(grandParentID).Times(1) - - err = verifier.ApricotStandardBlock(blk) - require.ErrorIs(err, errConflictingParentTxs) -} - func TestVerifierVisitApricotStandardBlockWithProposalBlockParent(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) @@ -1124,13 +1090,12 @@ func TestVerifierVisitBanffAbortBlockUnexpectedParentState(t *testing.T) { } func TestBlockExecutionWithComplexity(t *testing.T) { - s := statetest.New(t, statetest.Config{}) - verifier := newTestVerifier(t, s) + verifier := newTestVerifier(t, testVerifierConfig{}) wallet := txstest.NewWallet( t, verifier.ctx, verifier.txExecutorBackend.Config, - s, + verifier.state, secp256k1fx.NewKeychain(genesis.EWOQKey), nil, // subnetIDs nil, // chainIDs @@ -1185,13 +1150,13 @@ func TestBlockExecutionWithComplexity(t *testing.T) { verifier.txExecutorBackend.Clk.Set(test.timestamp) timestamp, _, err := state.NextBlockTime( verifier.txExecutorBackend.Config.ValidatorFeeConfig, - s, + verifier.state, verifier.txExecutorBackend.Clk, ) require.NoError(err) - lastAcceptedID := s.GetLastAccepted() - lastAccepted, err := s.GetStatelessBlock(lastAcceptedID) + lastAcceptedID := verifier.state.GetLastAccepted() + lastAccepted, err := verifier.state.GetStatelessBlock(lastAcceptedID) require.NoError(err) blk, err := block.NewBanffStandardBlock( diff --git a/vms/platformvm/txs/txsmock/unsigned_tx.go b/vms/platformvm/txs/txsmock/unsigned_tx.go deleted file mode 100644 index a7ba0daac896..000000000000 --- a/vms/platformvm/txs/txsmock/unsigned_tx.go +++ /dev/null @@ -1,138 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: vms/platformvm/txs/unsigned_tx.go -// -// Generated by this command: -// -// mockgen -source=vms/platformvm/txs/unsigned_tx.go -destination=vms/platformvm/txs/txsmock/unsigned_tx.go -package=txsmock -exclude_interfaces= -mock_names=UnsignedTx=UnsignedTx -// - -// Package txsmock is a generated GoMock package. -package txsmock - -import ( - reflect "reflect" - - ids "github.com/ava-labs/avalanchego/ids" - snow "github.com/ava-labs/avalanchego/snow" - set "github.com/ava-labs/avalanchego/utils/set" - avax "github.com/ava-labs/avalanchego/vms/components/avax" - txs "github.com/ava-labs/avalanchego/vms/platformvm/txs" - gomock "go.uber.org/mock/gomock" -) - -// UnsignedTx is a mock of UnsignedTx interface. -type UnsignedTx struct { - ctrl *gomock.Controller - recorder *UnsignedTxMockRecorder -} - -// UnsignedTxMockRecorder is the mock recorder for UnsignedTx. -type UnsignedTxMockRecorder struct { - mock *UnsignedTx -} - -// NewUnsignedTx creates a new mock instance. -func NewUnsignedTx(ctrl *gomock.Controller) *UnsignedTx { - mock := &UnsignedTx{ctrl: ctrl} - mock.recorder = &UnsignedTxMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *UnsignedTx) EXPECT() *UnsignedTxMockRecorder { - return m.recorder -} - -// Bytes mocks base method. -func (m *UnsignedTx) Bytes() []byte { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Bytes") - ret0, _ := ret[0].([]byte) - return ret0 -} - -// Bytes indicates an expected call of Bytes. -func (mr *UnsignedTxMockRecorder) Bytes() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bytes", reflect.TypeOf((*UnsignedTx)(nil).Bytes)) -} - -// InitCtx mocks base method. -func (m *UnsignedTx) InitCtx(ctx *snow.Context) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "InitCtx", ctx) -} - -// InitCtx indicates an expected call of InitCtx. -func (mr *UnsignedTxMockRecorder) InitCtx(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitCtx", reflect.TypeOf((*UnsignedTx)(nil).InitCtx), ctx) -} - -// InputIDs mocks base method. -func (m *UnsignedTx) InputIDs() set.Set[ids.ID] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InputIDs") - ret0, _ := ret[0].(set.Set[ids.ID]) - return ret0 -} - -// InputIDs indicates an expected call of InputIDs. -func (mr *UnsignedTxMockRecorder) InputIDs() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InputIDs", reflect.TypeOf((*UnsignedTx)(nil).InputIDs)) -} - -// Outputs mocks base method. -func (m *UnsignedTx) Outputs() []*avax.TransferableOutput { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Outputs") - ret0, _ := ret[0].([]*avax.TransferableOutput) - return ret0 -} - -// Outputs indicates an expected call of Outputs. -func (mr *UnsignedTxMockRecorder) Outputs() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Outputs", reflect.TypeOf((*UnsignedTx)(nil).Outputs)) -} - -// SetBytes mocks base method. -func (m *UnsignedTx) SetBytes(unsignedBytes []byte) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetBytes", unsignedBytes) -} - -// SetBytes indicates an expected call of SetBytes. -func (mr *UnsignedTxMockRecorder) SetBytes(unsignedBytes any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBytes", reflect.TypeOf((*UnsignedTx)(nil).SetBytes), unsignedBytes) -} - -// SyntacticVerify mocks base method. -func (m *UnsignedTx) SyntacticVerify(ctx *snow.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SyntacticVerify", ctx) - ret0, _ := ret[0].(error) - return ret0 -} - -// SyntacticVerify indicates an expected call of SyntacticVerify. -func (mr *UnsignedTxMockRecorder) SyntacticVerify(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyntacticVerify", reflect.TypeOf((*UnsignedTx)(nil).SyntacticVerify), ctx) -} - -// Visit mocks base method. -func (m *UnsignedTx) Visit(visitor txs.Visitor) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Visit", visitor) - ret0, _ := ret[0].(error) - return ret0 -} - -// Visit indicates an expected call of Visit. -func (mr *UnsignedTxMockRecorder) Visit(visitor any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Visit", reflect.TypeOf((*UnsignedTx)(nil).Visit), visitor) -} From e64ca232caebf64a157ae3c1d3b675aa25cd898e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:01:02 -0500 Subject: [PATCH 153/184] Add RegisterSubnetValidatorTx serialization and SyntacticVerify tests --- .../txs/register_subnet_validator_tx_test.go | 393 ++++++++++++++++++ .../register_subnet_validator_tx_test.json | 175 ++++++++ 2 files changed, 568 insertions(+) create mode 100644 vms/platformvm/txs/register_subnet_validator_tx_test.go create mode 100644 vms/platformvm/txs/register_subnet_validator_tx_test.json diff --git a/vms/platformvm/txs/register_subnet_validator_tx_test.go b/vms/platformvm/txs/register_subnet_validator_tx_test.go new file mode 100644 index 000000000000..64055b6171e0 --- /dev/null +++ b/vms/platformvm/txs/register_subnet_validator_tx_test.go @@ -0,0 +1,393 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "encoding/hex" + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + _ "embed" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" +) + +//go:embed register_subnet_validator_tx_test.json +var registerSubnetValidatorTxJSON []byte + +func TestRegisterSubnetValidatorTxSerialization(t *testing.T) { + require := require.New(t) + + const balance = units.Avax + + skBytes, err := hex.DecodeString("6668fecd4595b81e4d568398c820bbf3f073cb222902279fa55ebb84764ed2e3") + require.NoError(err) + sk, err := bls.SecretKeyFromBytes(skBytes) + require.NoError(err) + + var ( + pop = signer.NewProofOfPossession(sk) + message = []byte("message") + addr = ids.ShortID{ + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + } + avaxAssetID = ids.ID{ + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + } + customAssetID = ids.ID{ + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + } + txID = ids.ID{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + } + ) + + var unsignedTx UnsignedTx = &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.UnitTestID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 87654321, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 12345678, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + }, + { + Asset: avax.Asset{ + ID: customAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 876543210, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 0xffffffffffffffff, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + }, + }, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.Avax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{2, 5}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 2, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &stakeable.LockIn{ + Locktime: 876543210, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 0xefffffffffffffff, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 3, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 0x1000000000000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }, + }, + Memo: types.JSONByteSlice("😅\nwell that's\x01\x23\x45!"), + }, + }, + Balance: balance, + ProofOfPossession: pop.ProofOfPossession, + Message: message, + } + txBytes, err := Codec.Marshal(CodecVersion, &unsignedTx) + require.NoError(err) + + expectedBytes := []byte{ + // Codec version + 0x00, 0x00, + // RegisterSubnetValidatorTx Type ID + 0x00, 0x00, 0x00, 0x24, + // Network ID + 0x00, 0x00, 0x00, 0x0a, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x02, + // Outputs[0] + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x05, 0x39, 0x7f, 0xb1, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x61, 0x4e, + // threshold + 0x00, 0x00, 0x00, 0x00, + // number of addresses + 0x00, 0x00, 0x00, 0x00, + // Outputs[1] + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // threshold + 0x00, 0x00, 0x00, 0x01, + // number of addresses + 0x00, 0x00, 0x00, 0x01, + // address[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + // number of inputs + 0x00, 0x00, 0x00, 0x03, + // inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 Avax + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x02, + // index of first signer + 0x00, 0x00, 0x00, 0x02, + // index of second signer + 0x00, 0x00, 0x00, 0x05, + // inputs[1] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x02, + // Custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked input type ID + 0x00, 0x00, 0x00, 0x15, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x00, + // inputs[2] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x03, + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x00, + // length of memo + 0x00, 0x00, 0x00, 0x14, + // memo + 0xf0, 0x9f, 0x98, 0x85, 0x0a, 0x77, 0x65, 0x6c, + 0x6c, 0x20, 0x74, 0x68, 0x61, 0x74, 0x27, 0x73, + 0x01, 0x23, 0x45, 0x21, + // balance + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // proof of possession + 0x8c, 0xfd, 0x79, 0x09, 0xd1, 0x53, 0xb9, 0x60, + 0x4b, 0x62, 0xb1, 0x43, 0xba, 0x36, 0x20, 0x7b, + 0xb7, 0xe6, 0x48, 0x67, 0x42, 0x44, 0x80, 0x20, + 0x2a, 0x67, 0xdc, 0x68, 0x76, 0x83, 0x46, 0xd9, + 0x5c, 0x90, 0x98, 0x3c, 0x2d, 0x27, 0x9c, 0x64, + 0xc4, 0x3c, 0x51, 0x13, 0x6b, 0x2a, 0x05, 0xe0, + 0x16, 0x02, 0xd5, 0x2a, 0xa6, 0x37, 0x6f, 0xda, + 0x17, 0xfa, 0x6e, 0x2a, 0x18, 0xa0, 0x83, 0xe4, + 0x9d, 0x9c, 0x45, 0x0e, 0xab, 0x7b, 0x89, 0xb1, + 0xd5, 0x55, 0x5d, 0xa5, 0xc4, 0x89, 0x87, 0x2e, + 0x02, 0xb7, 0xe5, 0x22, 0x7b, 0x77, 0x55, 0x0a, + 0xf1, 0x33, 0x0e, 0x5a, 0x71, 0xf8, 0xc3, 0x68, + // length of message + 0x00, 0x00, 0x00, 0x07, + // message + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + } + require.Equal(expectedBytes, txBytes) + + ctx := snowtest.Context(t, constants.PlatformChainID) + unsignedTx.InitCtx(ctx) + + txJSON, err := json.MarshalIndent(unsignedTx, "", "\t") + require.NoError(err) + require.Equal( + // Normalize newlines for Windows + strings.ReplaceAll(string(registerSubnetValidatorTxJSON), "\r\n", "\n"), + string(txJSON), + ) +} + +func TestRegisterSubnetValidatorTxSyntacticVerify(t *testing.T) { + ctx := snowtest.Context(t, ids.GenerateTestID()) + tests := []struct { + name string + tx *RegisterSubnetValidatorTx + expectedErr error + }{ + { + name: "nil tx", + tx: nil, + expectedErr: ErrNilTx, + }, + { + name: "already verified", + // The tx includes invalid data to verify that a cached result is + // returned. + tx: &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{ + SyntacticallyVerified: true, + }, + }, + expectedErr: nil, + }, + { + name: "invalid BaseTx", + tx: &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{}, + }, + expectedErr: avax.ErrWrongNetworkID, + }, + { + name: "passes verification", + tx: &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }, + }, + }, + expectedErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + err := test.tx.SyntacticVerify(ctx) + require.ErrorIs(err, test.expectedErr) + if test.expectedErr != nil { + return + } + require.True(test.tx.SyntacticallyVerified) + }) + } +} diff --git a/vms/platformvm/txs/register_subnet_validator_tx_test.json b/vms/platformvm/txs/register_subnet_validator_tx_test.json new file mode 100644 index 000000000000..f741e2fdbb37 --- /dev/null +++ b/vms/platformvm/txs/register_subnet_validator_tx_test.json @@ -0,0 +1,175 @@ +{ + "networkID": 10, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 87654321, + "output": { + "addresses": [], + "amount": 1, + "locktime": 12345678, + "threshold": 0 + } + } + }, + { + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 876543210, + "output": { + "addresses": [ + "P-testing1g32kvaugnx4tk3z4vemc3xd2hdz92enhgrdu9n" + ], + "amount": 18446744073709551615, + "locktime": 0, + "threshold": 1 + } + } + } + ], + "inputs": [ + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1000000000, + "signatureIndices": [ + 2, + 5 + ] + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 2, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 876543210, + "input": { + "amount": 17293822569102704639, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 3, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1152921504606846976, + "signatureIndices": [] + } + } + ], + "memo": "0xf09f98850a77656c6c2074686174277301234521", + "balance": 1000000000, + "proofOfPossession": [ + 140, + 253, + 121, + 9, + 209, + 83, + 185, + 96, + 75, + 98, + 177, + 67, + 186, + 54, + 32, + 123, + 183, + 230, + 72, + 103, + 66, + 68, + 128, + 32, + 42, + 103, + 220, + 104, + 118, + 131, + 70, + 217, + 92, + 144, + 152, + 60, + 45, + 39, + 156, + 100, + 196, + 60, + 81, + 19, + 107, + 42, + 5, + 224, + 22, + 2, + 213, + 42, + 166, + 55, + 111, + 218, + 23, + 250, + 110, + 42, + 24, + 160, + 131, + 228, + 157, + 156, + 69, + 14, + 171, + 123, + 137, + 177, + 213, + 85, + 93, + 165, + 196, + 137, + 135, + 46, + 2, + 183, + 229, + 34, + 123, + 119, + 85, + 10, + 241, + 51, + 14, + 90, + 113, + 248, + 195, + 104 + ], + "message": "0x6d657373616765" +} \ No newline at end of file From beebbe0f96d75b726e869294dd4e9dca2d14d101 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:05:44 -0500 Subject: [PATCH 154/184] Unexport all P-Chain visitors (#3525) --- vms/platformvm/block/builder/builder.go | 21 +- vms/platformvm/block/builder/helpers_test.go | 14 +- vms/platformvm/block/executor/helpers_test.go | 14 +- vms/platformvm/block/executor/manager.go | 13 +- vms/platformvm/block/executor/verifier.go | 72 ++- .../txs/executor/advance_time_test.go | 210 +++---- .../txs/executor/atomic_tx_executor.go | 109 ++-- .../txs/executor/create_chain_test.go | 79 ++- .../txs/executor/create_subnet_test.go | 13 +- vms/platformvm/txs/executor/export_test.go | 14 +- vms/platformvm/txs/executor/helpers_test.go | 14 +- vms/platformvm/txs/executor/import_test.go | 14 +- .../txs/executor/proposal_tx_executor.go | 250 ++++---- .../txs/executor/proposal_tx_executor_test.go | 240 ++++--- .../txs/executor/reward_validator_test.go | 196 +++--- .../txs/executor/standard_tx_executor.go | 416 ++++++------ .../txs/executor/standard_tx_executor_test.go | 590 +++++++++--------- 17 files changed, 1142 insertions(+), 1137 deletions(-) diff --git a/vms/platformvm/block/builder/builder.go b/vms/platformvm/block/builder/builder.go index 3c88e8277929..40ce8a3633c7 100644 --- a/vms/platformvm/block/builder/builder.go +++ b/vms/platformvm/block/builder/builder.go @@ -516,32 +516,29 @@ func executeTx( return false, err } - executor := &txexecutor.StandardTxExecutor{ - Backend: backend, - State: txDiff, - FeeCalculator: feeCalculator, - Tx: tx, - } - - err = tx.Unsigned.Visit(executor) + txInputs, _, _, err := txexecutor.StandardTx( + backend, + feeCalculator, + tx, + txDiff, + ) if err != nil { txID := tx.ID() mempool.MarkDropped(txID, err) return false, nil } - if inputs.Overlaps(executor.Inputs) { + if inputs.Overlaps(txInputs) { txID := tx.ID() mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs) return false, nil } - err = manager.VerifyUniqueInputs(parentID, executor.Inputs) - if err != nil { + if err := manager.VerifyUniqueInputs(parentID, txInputs); err != nil { txID := tx.ID() mempool.MarkDropped(txID, err) return false, nil } - inputs.Union(executor.Inputs) + inputs.Union(txInputs) txDiff.AddTx(tx, status.Committed) return true, txDiff.Apply(stateDiff) diff --git a/vms/platformvm/block/builder/helpers_test.go b/vms/platformvm/block/builder/helpers_test.go index faba1b1ed695..c779609baf54 100644 --- a/vms/platformvm/block/builder/helpers_test.go +++ b/vms/platformvm/block/builder/helpers_test.go @@ -247,13 +247,13 @@ func addSubnet(t *testing.T, env *environment) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := txexecutor.StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: testSubnet1, - } - require.NoError(testSubnet1.Unsigned.Visit(&executor)) + _, _, _, err = txexecutor.StandardTx( + &env.backend, + feeCalculator, + testSubnet1, + stateDiff, + ) + require.NoError(err) stateDiff.AddTx(testSubnet1, status.Committed) require.NoError(stateDiff.Apply(env.state)) diff --git a/vms/platformvm/block/executor/helpers_test.go b/vms/platformvm/block/executor/helpers_test.go index 4e3da112739f..d0616a8e374c 100644 --- a/vms/platformvm/block/executor/helpers_test.go +++ b/vms/platformvm/block/executor/helpers_test.go @@ -253,13 +253,13 @@ func addSubnet(t testing.TB, env *environment) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := executor.StandardTxExecutor{ - Backend: env.backend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: testSubnet1, - } - require.NoError(testSubnet1.Unsigned.Visit(&executor)) + _, _, _, err = executor.StandardTx( + env.backend, + feeCalculator, + testSubnet1, + stateDiff, + ) + require.NoError(err) stateDiff.AddTx(testSubnet1, status.Committed) require.NoError(stateDiff.Apply(env.state)) diff --git a/vms/platformvm/block/executor/manager.go b/vms/platformvm/block/executor/manager.go index 5c419500e5f7..7f153a83be3f 100644 --- a/vms/platformvm/block/executor/manager.go +++ b/vms/platformvm/block/executor/manager.go @@ -142,12 +142,13 @@ func (m *manager) VerifyTx(tx *txs.Tx) error { } feeCalculator := state.PickFeeCalculator(m.txExecutorBackend.Config, stateDiff) - return tx.Unsigned.Visit(&executor.StandardTxExecutor{ - Backend: m.txExecutorBackend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: tx, - }) + _, _, _, err = executor.StandardTx( + m.txExecutorBackend, + feeCalculator, + tx, + stateDiff, + ) + return err } func (m *manager) VerifyUniqueInputs(blkID ids.ID, inputs set.Set[ids.ID]) error { diff --git a/vms/platformvm/block/executor/verifier.go b/vms/platformvm/block/executor/verifier.go index 1570a1b3c2ac..80e925dbc192 100644 --- a/vms/platformvm/block/executor/verifier.go +++ b/vms/platformvm/block/executor/verifier.go @@ -230,23 +230,22 @@ func (v *verifier) ApricotAtomicBlock(b *block.ApricotAtomicBlock) error { } feeCalculator := state.NewStaticFeeCalculator(v.txExecutorBackend.Config, currentTimestamp) - atomicExecutor := executor.AtomicTxExecutor{ - Backend: v.txExecutorBackend, - FeeCalculator: feeCalculator, - ParentID: parentID, - StateVersions: v, - Tx: b.Tx, - } - - if err := b.Tx.Unsigned.Visit(&atomicExecutor); err != nil { + onAcceptState, atomicInputs, atomicRequests, err := executor.AtomicTx( + v.txExecutorBackend, + feeCalculator, + parentID, + v, + b.Tx, + ) + if err != nil { txID := b.Tx.ID() v.MarkDropped(txID, err) // cache tx as dropped - return fmt.Errorf("tx %s failed semantic verification: %w", txID, err) + return err } - atomicExecutor.OnAccept.AddTx(b.Tx, status.Committed) + onAcceptState.AddTx(b.Tx, status.Committed) - if err := v.verifyUniqueInputs(parentID, atomicExecutor.Inputs); err != nil { + if err := v.verifyUniqueInputs(parentID, atomicInputs); err != nil { return err } @@ -256,11 +255,11 @@ func (v *verifier) ApricotAtomicBlock(b *block.ApricotAtomicBlock) error { v.blkIDToState[blkID] = &blockState{ statelessBlock: b, - onAcceptState: atomicExecutor.OnAccept, + onAcceptState: onAcceptState, - inputs: atomicExecutor.Inputs, - timestamp: atomicExecutor.OnAccept.GetTimestamp(), - atomicRequests: atomicExecutor.AtomicRequests, + inputs: atomicInputs, + timestamp: onAcceptState.GetTimestamp(), + atomicRequests: atomicRequests, verifiedHeights: set.Of(v.pChainHeight), } return nil @@ -395,15 +394,14 @@ func (v *verifier) proposalBlock( atomicRequests map[ids.ID]*atomic.Requests, onAcceptFunc func(), ) error { - txExecutor := executor.ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: v.txExecutorBackend, - FeeCalculator: feeCalculator, - Tx: tx, - } - - if err := tx.Unsigned.Visit(&txExecutor); err != nil { + err := executor.ProposalTx( + v.txExecutorBackend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) + if err != nil { txID := tx.ID() v.MarkDropped(txID, err) // cache tx as dropped return err @@ -519,30 +517,30 @@ func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calcula atomicRequests = make(map[ids.ID]*atomic.Requests) ) for _, tx := range txs { - txExecutor := executor.StandardTxExecutor{ - Backend: v.txExecutorBackend, - State: diff, - FeeCalculator: feeCalculator, - Tx: tx, - } - if err := tx.Unsigned.Visit(&txExecutor); err != nil { + txInputs, txAtomicRequests, onAccept, err := executor.StandardTx( + v.txExecutorBackend, + feeCalculator, + tx, + diff, + ) + if err != nil { txID := tx.ID() v.MarkDropped(txID, err) // cache tx as dropped return nil, nil, nil, err } // ensure it doesn't overlap with current input batch - if inputs.Overlaps(txExecutor.Inputs) { + if inputs.Overlaps(txInputs) { return nil, nil, nil, ErrConflictingBlockTxs } // Add UTXOs to batch - inputs.Union(txExecutor.Inputs) + inputs.Union(txInputs) diff.AddTx(tx, status.Committed) - if txExecutor.OnAccept != nil { - funcs = append(funcs, txExecutor.OnAccept) + if onAccept != nil { + funcs = append(funcs, onAccept) } - for chainID, txRequests := range txExecutor.AtomicRequests { + for chainID, txRequests := range txAtomicRequests { // Add/merge in the atomic requests represented by [tx] chainRequests, exists := atomicRequests[chainID] if !exists { diff --git a/vms/platformvm/txs/executor/advance_time_test.go b/vms/platformvm/txs/executor/advance_time_test.go index fa7d3583c68f..5acb4f29fca3 100644 --- a/vms/platformvm/txs/executor/advance_time_test.go +++ b/vms/platformvm/txs/executor/advance_time_test.go @@ -66,32 +66,31 @@ func TestAdvanceTimeTxUpdatePrimaryNetworkStakers(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - validatorStaker, err := executor.OnCommitState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + validatorStaker, err := onCommitState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) require.NoError(err) require.Equal(addPendingValidatorTx.ID(), validatorStaker.TxID) require.Equal(uint64(1370), validatorStaker.PotentialReward) // See rewards tests to explain why 1370 - _, err = executor.OnCommitState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) + _, err = onCommitState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) require.ErrorIs(err, database.ErrNotFound) - _, err = executor.OnAbortState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) + _, err = onAbortState.GetCurrentValidator(constants.PrimaryNetworkID, nodeID) require.ErrorIs(err, database.ErrNotFound) - validatorStaker, err = executor.OnAbortState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) + validatorStaker, err = onAbortState.GetPendingValidator(constants.PrimaryNetworkID, nodeID) require.NoError(err) require.Equal(addPendingValidatorTx.ID(), validatorStaker.TxID) // Test VM validators - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -115,14 +114,13 @@ func TestAdvanceTimeTxTimestampTooEarly(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrChildBlockEarlierThanParent) } @@ -152,14 +150,13 @@ func TestAdvanceTimeTxTimestampTooLate(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrChildBlockAfterStakerChangeTime) } @@ -183,14 +180,13 @@ func TestAdvanceTimeTxTimestampTooLate(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrChildBlockAfterStakerChangeTime) } } @@ -428,16 +424,15 @@ func TestAdvanceTimeTxUpdateStakers(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) - - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) + + require.NoError(onCommitState.Apply(env.state)) } env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -562,20 +557,19 @@ func TestAdvanceTimeTxRemoveSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) - - _, err = executor.OnCommitState.GetCurrentValidator(subnetID, subnetValidatorNodeID) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) + + _, err = onCommitState.GetCurrentValidator(subnetID, subnetValidatorNodeID) require.ErrorIs(err, database.ErrNotFound) // Check VM Validators are removed successfully - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -644,16 +638,15 @@ func TestTrackedSubnet(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -694,16 +687,15 @@ func TestAdvanceTimeTxDelegatorStakerWeight(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -753,16 +745,15 @@ func TestAdvanceTimeTxDelegatorStakerWeight(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - executor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -796,16 +787,15 @@ func TestAdvanceTimeTxDelegatorStakers(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -854,16 +844,15 @@ func TestAdvanceTimeTxDelegatorStakers(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - executor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - require.NoError(executor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -895,14 +884,13 @@ func TestAdvanceTimeTxAfterBanff(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrAdvanceTimeTxIssuedAfterBanff) } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 190c5ec114c9..ea5294c2559c 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -4,6 +4,8 @@ package executor import ( + "fmt" + "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" @@ -12,106 +14,127 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" ) -var _ txs.Visitor = (*AtomicTxExecutor)(nil) +var _ txs.Visitor = (*atomicTxExecutor)(nil) + +// AtomicTx executes the atomic transaction [tx] and returns the resulting state +// modifications. +// +// This is only used to execute atomic transactions pre-AP5. After AP5 the +// execution was moved to [StandardTx]. +func AtomicTx( + backend *Backend, + feeCalculator fee.Calculator, + parentID ids.ID, + stateVersions state.Versions, + tx *txs.Tx, +) (state.Diff, set.Set[ids.ID], map[ids.ID]*atomic.Requests, error) { + atomicExecutor := atomicTxExecutor{ + backend: backend, + feeCalculator: feeCalculator, + parentID: parentID, + stateVersions: stateVersions, + tx: tx, + } + if err := tx.Unsigned.Visit(&atomicExecutor); err != nil { + txID := tx.ID() + return nil, nil, nil, fmt.Errorf("atomic tx %s failed execution: %w", txID, err) + } + return atomicExecutor.onAccept, atomicExecutor.inputs, atomicExecutor.atomicRequests, nil +} -// atomicTxExecutor is used to execute atomic transactions pre-AP5. After AP5 -// the execution was moved to be performed inside of the standardTxExecutor. -type AtomicTxExecutor struct { +type atomicTxExecutor struct { // inputs, to be filled before visitor methods are called - *Backend - FeeCalculator fee.Calculator - ParentID ids.ID - StateVersions state.Versions - Tx *txs.Tx + backend *Backend + feeCalculator fee.Calculator + parentID ids.ID + stateVersions state.Versions + tx *txs.Tx // outputs of visitor execution - OnAccept state.Diff - Inputs set.Set[ids.ID] - AtomicRequests map[ids.ID]*atomic.Requests + onAccept state.Diff + inputs set.Set[ids.ID] + atomicRequests map[ids.ID]*atomic.Requests } -func (*AtomicTxExecutor) AddValidatorTx(*txs.AddValidatorTx) error { +func (*atomicTxExecutor) AddValidatorTx(*txs.AddValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { +func (*atomicTxExecutor) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*atomicTxExecutor) AddDelegatorTx(*txs.AddDelegatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) CreateChainTx(*txs.CreateChainTx) error { +func (*atomicTxExecutor) CreateChainTx(*txs.CreateChainTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { +func (*atomicTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { +func (*atomicTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { +func (*atomicTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (*atomicTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { +func (*atomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) BaseTx(*txs.BaseTx) error { +func (*atomicTxExecutor) BaseTx(*txs.BaseTx) error { return ErrWrongTxType } -func (*AtomicTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (*atomicTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } -func (e *AtomicTxExecutor) ImportTx(tx *txs.ImportTx) error { - return e.atomicTx(tx) +func (e *atomicTxExecutor) ImportTx(*txs.ImportTx) error { + return e.atomicTx() } -func (e *AtomicTxExecutor) ExportTx(tx *txs.ExportTx) error { - return e.atomicTx(tx) +func (e *atomicTxExecutor) ExportTx(*txs.ExportTx) error { + return e.atomicTx() } -func (e *AtomicTxExecutor) atomicTx(tx txs.UnsignedTx) error { +func (e *atomicTxExecutor) atomicTx() error { onAccept, err := state.NewDiff( - e.ParentID, - e.StateVersions, + e.parentID, + e.stateVersions, ) if err != nil { return err } - e.OnAccept = onAccept - executor := StandardTxExecutor{ - Backend: e.Backend, - State: e.OnAccept, - FeeCalculator: e.FeeCalculator, - Tx: e.Tx, - } - err = tx.Visit(&executor) - e.Inputs = executor.Inputs - e.AtomicRequests = executor.AtomicRequests + e.onAccept = onAccept + e.inputs, e.atomicRequests, _, err = StandardTx( + e.backend, + e.feeCalculator, + e.tx, + onAccept, + ) return err } diff --git a/vms/platformvm/txs/executor/create_chain_test.go b/vms/platformvm/txs/executor/create_chain_test.go index 7f9919e4a8f5..f271b81dbebf 100644 --- a/vms/platformvm/txs/executor/create_chain_test.go +++ b/vms/platformvm/txs/executor/create_chain_test.go @@ -52,13 +52,12 @@ func TestCreateChainTxInsufficientControlSigs(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -96,13 +95,12 @@ func TestCreateChainTxWrongControlSig(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -137,13 +135,12 @@ func TestCreateChainTxNoSuchSubnet(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, builderDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, database.ErrNotFound) } @@ -175,13 +172,13 @@ func TestCreateChainTxValid(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, builderDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) + require.NoError(err) } func TestCreateChainTxAP3FeeChange(t *testing.T) { @@ -248,13 +245,12 @@ func TestCreateChainTxAP3FeeChange(t *testing.T) { stateDiff.SetTimestamp(test.time) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, test.expectedError) }) } @@ -296,12 +292,11 @@ func TestEtnaCreateChainTxInvalidWithManagedSubnet(t *testing.T) { ) feeCalculator := state.PickFeeCalculator(env.config, builderDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errIsImmutable) } diff --git a/vms/platformvm/txs/executor/create_subnet_test.go b/vms/platformvm/txs/executor/create_subnet_test.go index 0290d1811401..482e5175c945 100644 --- a/vms/platformvm/txs/executor/create_subnet_test.go +++ b/vms/platformvm/txs/executor/create_subnet_test.go @@ -79,13 +79,12 @@ func TestCreateSubnetTxAP3FeeChange(t *testing.T) { stateDiff.SetTimestamp(test.time) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, test.expectedErr) }) } diff --git a/vms/platformvm/txs/executor/export_test.go b/vms/platformvm/txs/executor/export_test.go index 34b1e8046454..3ecbc2e590bb 100644 --- a/vms/platformvm/txs/executor/export_test.go +++ b/vms/platformvm/txs/executor/export_test.go @@ -66,13 +66,13 @@ func TestNewExportTx(t *testing.T) { stateDiff.SetTimestamp(tt.timestamp) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - verifier := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&verifier)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) + require.NoError(err) }) } } diff --git a/vms/platformvm/txs/executor/helpers_test.go b/vms/platformvm/txs/executor/helpers_test.go index 129e0d351f38..bf6a6f7214c1 100644 --- a/vms/platformvm/txs/executor/helpers_test.go +++ b/vms/platformvm/txs/executor/helpers_test.go @@ -220,13 +220,13 @@ func addSubnet(t *testing.T, env *environment) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, env.state) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: testSubnet1, - } - require.NoError(testSubnet1.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + testSubnet1, + stateDiff, + ) + require.NoError(err) stateDiff.AddTx(testSubnet1, status.Committed) require.NoError(stateDiff.Apply(env.state)) diff --git a/vms/platformvm/txs/executor/import_test.go b/vms/platformvm/txs/executor/import_test.go index d3c48f17e186..f25a831bb3d4 100644 --- a/vms/platformvm/txs/executor/import_test.go +++ b/vms/platformvm/txs/executor/import_test.go @@ -156,13 +156,13 @@ func TestNewImportTx(t *testing.T) { stateDiff.SetTimestamp(tt.timestamp) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - verifier := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: stateDiff, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&verifier)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) + require.NoError(err) }) } } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 4c4c1f5c2b91..03b03c889a23 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -30,7 +30,7 @@ const ( ) var ( - _ txs.Visitor = (*ProposalTxExecutor)(nil) + _ txs.Visitor = (*proposalTxExecutor)(nil) ErrRemoveStakerTooEarly = errors.New("attempting to remove staker before their end time") ErrRemoveWrongStaker = errors.New("attempting to remove wrong staker") @@ -42,259 +42,287 @@ var ( ErrAdvanceTimeTxIssuedAfterBanff = errors.New("AdvanceTimeTx issued after Banff") ) -type ProposalTxExecutor struct { +// ProposalTx executes the proposal transaction [tx]. +// +// [onCommitState] will be modified to reflect the changes made to the state if +// the proposal is committed. +// +// [onAbortState] will be modified to reflect the changes made to the state if +// the proposal is aborted. +// +// Invariant: It is assumed that [onCommitState] and [onAbortState] represent +// the same state when passed into this function. +func ProposalTx( + backend *Backend, + feeCalculator fee.Calculator, + tx *txs.Tx, + onCommitState state.Diff, + onAbortState state.Diff, +) error { + proposalExecutor := proposalTxExecutor{ + backend: backend, + feeCalculator: feeCalculator, + tx: tx, + onCommitState: onCommitState, + onAbortState: onAbortState, + } + if err := tx.Unsigned.Visit(&proposalExecutor); err != nil { + txID := tx.ID() + return fmt.Errorf("proposal tx %s failed execution: %w", txID, err) + } + return nil +} + +type proposalTxExecutor struct { // inputs, to be filled before visitor methods are called - *Backend - FeeCalculator fee.Calculator - Tx *txs.Tx - // [OnCommitState] is the state used for validation. - // [OnCommitState] is modified by this struct's methods to + backend *Backend + feeCalculator fee.Calculator + tx *txs.Tx + // [onCommitState] is the state used for validation. + // [onCommitState] is modified by this struct's methods to // reflect changes made to the state if the proposal is committed. - // - // Invariant: Both [OnCommitState] and [OnAbortState] represent the same - // state when provided to this struct. - OnCommitState state.Diff - // [OnAbortState] is modified by this struct's methods to + onCommitState state.Diff + // [onAbortState] is modified by this struct's methods to // reflect changes made to the state if the proposal is aborted. - OnAbortState state.Diff + onAbortState state.Diff } -func (*ProposalTxExecutor) CreateChainTx(*txs.CreateChainTx) error { +func (*proposalTxExecutor) CreateChainTx(*txs.CreateChainTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { +func (*proposalTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) ImportTx(*txs.ImportTx) error { +func (*proposalTxExecutor) ImportTx(*txs.ImportTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) ExportTx(*txs.ExportTx) error { +func (*proposalTxExecutor) ExportTx(*txs.ExportTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (*proposalTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { +func (*proposalTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*proposalTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*proposalTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*proposalTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) BaseTx(*txs.BaseTx) error { +func (*proposalTxExecutor) BaseTx(*txs.BaseTx) error { return ErrWrongTxType } -func (*ProposalTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (*proposalTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } -func (e *ProposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { +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 // StandardBlocks. - currentTimestamp := e.OnCommitState.GetTimestamp() - if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { + currentTimestamp := e.onCommitState.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { return fmt.Errorf( "%w: timestamp (%s) >= Banff fork time (%s)", ErrProposedAddStakerTxAfterBanff, currentTimestamp, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } onAbortOuts, err := verifyAddValidatorTx( - e.Backend, - e.FeeCalculator, - e.OnCommitState, - e.Tx, + e.backend, + e.feeCalculator, + e.onCommitState, + e.tx, tx, ) if err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Set up the state if this tx is committed // Consume the UTXOs - avax.Consume(e.OnCommitState, tx.Ins) + avax.Consume(e.onCommitState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnCommitState, txID, tx.Outs) + avax.Produce(e.onCommitState, txID, tx.Outs) newStaker, err := state.NewPendingStaker(txID, tx) if err != nil { return err } - if err := e.OnCommitState.PutPendingValidator(newStaker); err != nil { + if err := e.onCommitState.PutPendingValidator(newStaker); err != nil { return err } // Set up the state if this tx is aborted // Consume the UTXOs - avax.Consume(e.OnAbortState, tx.Ins) + avax.Consume(e.onAbortState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnAbortState, txID, onAbortOuts) + avax.Produce(e.onAbortState, txID, onAbortOuts) return nil } -func (e *ProposalTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { +func (e *proposalTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { // AddSubnetValidatorTx is a proposal transaction until the Banff fork // activation. Following the activation, AddSubnetValidatorTxs must be // issued into StandardBlocks. - currentTimestamp := e.OnCommitState.GetTimestamp() - if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { + currentTimestamp := e.onCommitState.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { return fmt.Errorf( "%w: timestamp (%s) >= Banff fork time (%s)", ErrProposedAddStakerTxAfterBanff, currentTimestamp, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } if err := verifyAddSubnetValidatorTx( - e.Backend, - e.FeeCalculator, - e.OnCommitState, - e.Tx, + e.backend, + e.feeCalculator, + e.onCommitState, + e.tx, tx, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Set up the state if this tx is committed // Consume the UTXOs - avax.Consume(e.OnCommitState, tx.Ins) + avax.Consume(e.onCommitState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnCommitState, txID, tx.Outs) + avax.Produce(e.onCommitState, txID, tx.Outs) newStaker, err := state.NewPendingStaker(txID, tx) if err != nil { return err } - if err := e.OnCommitState.PutPendingValidator(newStaker); err != nil { + if err := e.onCommitState.PutPendingValidator(newStaker); err != nil { return err } // Set up the state if this tx is aborted // Consume the UTXOs - avax.Consume(e.OnAbortState, tx.Ins) + avax.Consume(e.onAbortState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnAbortState, txID, tx.Outs) + avax.Produce(e.onAbortState, txID, tx.Outs) return nil } -func (e *ProposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { +func (e *proposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { // AddDelegatorTx is a proposal transaction until the Banff fork // activation. Following the activation, AddDelegatorTxs must be issued into // StandardBlocks. - currentTimestamp := e.OnCommitState.GetTimestamp() - if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { + currentTimestamp := e.onCommitState.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) { return fmt.Errorf( "%w: timestamp (%s) >= Banff fork time (%s)", ErrProposedAddStakerTxAfterBanff, currentTimestamp, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } onAbortOuts, err := verifyAddDelegatorTx( - e.Backend, - e.FeeCalculator, - e.OnCommitState, - e.Tx, + e.backend, + e.feeCalculator, + e.onCommitState, + e.tx, tx, ) if err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Set up the state if this tx is committed // Consume the UTXOs - avax.Consume(e.OnCommitState, tx.Ins) + avax.Consume(e.onCommitState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnCommitState, txID, tx.Outs) + avax.Produce(e.onCommitState, txID, tx.Outs) newStaker, err := state.NewPendingStaker(txID, tx) if err != nil { return err } - e.OnCommitState.PutPendingDelegator(newStaker) + e.onCommitState.PutPendingDelegator(newStaker) // Set up the state if this tx is aborted // Consume the UTXOs - avax.Consume(e.OnAbortState, tx.Ins) + avax.Consume(e.onAbortState, tx.Ins) // Produce the UTXOs - avax.Produce(e.OnAbortState, txID, onAbortOuts) + avax.Produce(e.onAbortState, txID, onAbortOuts) return nil } -func (e *ProposalTxExecutor) AdvanceTimeTx(tx *txs.AdvanceTimeTx) error { +func (e *proposalTxExecutor) AdvanceTimeTx(tx *txs.AdvanceTimeTx) error { switch { case tx == nil: return txs.ErrNilTx - case len(e.Tx.Creds) != 0: + case len(e.tx.Creds) != 0: return errWrongNumberOfCredentials } // Validate [newChainTime] newChainTime := tx.Timestamp() - if e.Config.UpgradeConfig.IsBanffActivated(newChainTime) { + if e.backend.Config.UpgradeConfig.IsBanffActivated(newChainTime) { return fmt.Errorf( "%w: proposed timestamp (%s) >= Banff fork time (%s)", ErrAdvanceTimeTxIssuedAfterBanff, newChainTime, - e.Config.UpgradeConfig.BanffTime, + e.backend.Config.UpgradeConfig.BanffTime, ) } - now := e.Clk.Time() + now := e.backend.Clk.Time() if err := VerifyNewChainTime( - e.Config.ValidatorFeeConfig, + e.backend.Config.ValidatorFeeConfig, newChainTime, now, - e.OnCommitState, + e.onCommitState, ); err != nil { return err } // Note that state doesn't change if this proposal is aborted - _, err := AdvanceTimeTo(e.Backend, e.OnCommitState, newChainTime) + _, err := AdvanceTimeTo(e.backend, e.onCommitState, newChainTime) return err } -func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error { +func (e *proposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error { switch { case tx == nil: return txs.ErrNilTx case tx.TxID == ids.Empty: return ErrInvalidID - case len(e.Tx.Creds) != 0: + case len(e.tx.Creds) != 0: return errWrongNumberOfCredentials } - currentStakerIterator, err := e.OnCommitState.GetCurrentStakerIterator() + currentStakerIterator, err := e.onCommitState.GetCurrentStakerIterator() if err != nil { return err } @@ -314,7 +342,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error } // Verify that the chain's timestamp is the validator's end time - currentChainTime := e.OnCommitState.GetTimestamp() + currentChainTime := e.onCommitState.GetTimestamp() if !stakerToReward.EndTime.Equal(currentChainTime) { return fmt.Errorf( "%w: TxID = %s with %s < %s", @@ -325,7 +353,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error ) } - stakerTx, _, err := e.OnCommitState.GetTx(stakerToReward.TxID) + stakerTx, _, err := e.onCommitState.GetTx(stakerToReward.TxID) if err != nil { return fmt.Errorf("failed to get next removed staker tx: %w", err) } @@ -339,16 +367,16 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error } // Handle staker lifecycle. - e.OnCommitState.DeleteCurrentValidator(stakerToReward) - e.OnAbortState.DeleteCurrentValidator(stakerToReward) + e.onCommitState.DeleteCurrentValidator(stakerToReward) + e.onAbortState.DeleteCurrentValidator(stakerToReward) case txs.DelegatorTx: if err := e.rewardDelegatorTx(uStakerTx, stakerToReward); err != nil { return err } // Handle staker lifecycle. - e.OnCommitState.DeleteCurrentDelegator(stakerToReward) - e.OnAbortState.DeleteCurrentDelegator(stakerToReward) + e.onCommitState.DeleteCurrentDelegator(stakerToReward) + e.onAbortState.DeleteCurrentDelegator(stakerToReward) default: // Invariant: Permissioned stakers are removed by the advancement of // time and the current chain timestamp is == this staker's @@ -358,7 +386,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error } // If the reward is aborted, then the current supply should be decreased. - currentSupply, err := e.OnAbortState.GetCurrentSupply(stakerToReward.SubnetID) + currentSupply, err := e.onAbortState.GetCurrentSupply(stakerToReward.SubnetID) if err != nil { return err } @@ -366,11 +394,11 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error if err != nil { return err } - e.OnAbortState.SetCurrentSupply(stakerToReward.SubnetID, newSupply) + e.onAbortState.SetCurrentSupply(stakerToReward.SubnetID, newSupply) return nil } -func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, validator *state.Staker) error { +func (e *proposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, validator *state.Staker) error { var ( txID = validator.TxID stake = uValidatorTx.Stake() @@ -390,8 +418,8 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: out.Asset, Out: out.Output(), } - e.OnCommitState.AddUTXO(utxo) - e.OnAbortState.AddUTXO(utxo) + e.onCommitState.AddUTXO(utxo) + e.onAbortState.AddUTXO(utxo) } utxosOffset := 0 @@ -400,7 +428,7 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val reward := validator.PotentialReward if reward > 0 { validationRewardsOwner := uValidatorTx.ValidationRewardsOwner() - outIntf, err := e.Fx.CreateOutput(reward, validationRewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(reward, validationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -417,14 +445,14 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: stakeAsset, Out: out, } - e.OnCommitState.AddUTXO(utxo) - e.OnCommitState.AddRewardUTXO(txID, utxo) + e.onCommitState.AddUTXO(utxo) + e.onCommitState.AddRewardUTXO(txID, utxo) utxosOffset++ } // Provide the accrued delegatee rewards from successful delegations here. - delegateeReward, err := e.OnCommitState.GetDelegateeReward( + delegateeReward, err := e.onCommitState.GetDelegateeReward( validator.SubnetID, validator.NodeID, ) @@ -437,7 +465,7 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val } delegationRewardsOwner := uValidatorTx.DelegationRewardsOwner() - outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -454,8 +482,8 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: stakeAsset, Out: out, } - e.OnCommitState.AddUTXO(onCommitUtxo) - e.OnCommitState.AddRewardUTXO(txID, onCommitUtxo) + e.onCommitState.AddUTXO(onCommitUtxo) + e.onCommitState.AddRewardUTXO(txID, onCommitUtxo) // Note: There is no [offset] if the RewardValidatorTx is // aborted, because the validator reward is not awarded. @@ -467,12 +495,12 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val Asset: stakeAsset, Out: out, } - e.OnAbortState.AddUTXO(onAbortUtxo) - e.OnAbortState.AddRewardUTXO(txID, onAbortUtxo) + e.onAbortState.AddUTXO(onAbortUtxo) + e.onAbortState.AddRewardUTXO(txID, onAbortUtxo) return nil } -func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, delegator *state.Staker) error { +func (e *proposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, delegator *state.Staker) error { var ( txID = delegator.TxID stake = uDelegatorTx.Stake() @@ -492,18 +520,18 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del Asset: out.Asset, Out: out.Output(), } - e.OnCommitState.AddUTXO(utxo) - e.OnAbortState.AddUTXO(utxo) + e.onCommitState.AddUTXO(utxo) + e.onAbortState.AddUTXO(utxo) } // We're (possibly) rewarding a delegator, so we need to fetch // the validator they are delegated to. - validator, err := e.OnCommitState.GetCurrentValidator(delegator.SubnetID, delegator.NodeID) + validator, err := e.onCommitState.GetCurrentValidator(delegator.SubnetID, delegator.NodeID) if err != nil { return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err) } - vdrTxIntf, _, err := e.OnCommitState.GetTx(validator.TxID) + vdrTxIntf, _, err := e.onCommitState.GetTx(validator.TxID) if err != nil { return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err) } @@ -526,7 +554,7 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del reward := delegatorReward if reward > 0 { rewardsOwner := uDelegatorTx.RewardsOwner() - outIntf, err := e.Fx.CreateOutput(reward, rewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(reward, rewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -543,8 +571,8 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del Out: out, } - e.OnCommitState.AddUTXO(utxo) - e.OnCommitState.AddRewardUTXO(txID, utxo) + e.onCommitState.AddUTXO(utxo) + e.onCommitState.AddRewardUTXO(txID, utxo) utxosOffset++ } @@ -554,8 +582,8 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del } // Reward the delegatee here - if e.Config.UpgradeConfig.IsCortinaActivated(validator.StartTime) { - previousDelegateeReward, err := e.OnCommitState.GetDelegateeReward( + if e.backend.Config.UpgradeConfig.IsCortinaActivated(validator.StartTime) { + previousDelegateeReward, err := e.onCommitState.GetDelegateeReward( validator.SubnetID, validator.NodeID, ) @@ -570,7 +598,7 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del // For any validators starting after [CortinaTime], we defer rewarding the // [reward] until their staking period is over. - err = e.OnCommitState.SetDelegateeReward( + err = e.onCommitState.SetDelegateeReward( validator.SubnetID, validator.NodeID, newDelegateeReward, @@ -582,7 +610,7 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del // For any validators who started prior to [CortinaTime], we issue the // [delegateeReward] immediately. delegationRewardsOwner := vdrTx.DelegationRewardsOwner() - outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) + outIntf, err := e.backend.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) } @@ -599,8 +627,8 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del Out: out, } - e.OnCommitState.AddUTXO(utxo) - e.OnCommitState.AddRewardUTXO(txID, utxo) + e.onCommitState.AddUTXO(utxo) + e.onCommitState.AddRewardUTXO(txID, utxo) } return nil } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor_test.go b/vms/platformvm/txs/executor/proposal_tx_executor_test.go index eed2e6a6ddeb..d6759ee54767 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor_test.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor_test.go @@ -268,14 +268,13 @@ func TestProposalTxExecuteAddDelegator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, tt.expectedErr) }) } @@ -316,14 +315,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -355,14 +353,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) } // Add a validator to pending validator set of primary network @@ -414,14 +411,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrNotValidator) } @@ -468,14 +464,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -505,14 +500,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -542,14 +536,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) } // Case: Proposed validator start validating at/before current timestamp @@ -581,14 +574,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -652,14 +644,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: duplicateSubnetTx, - } - err = duplicateSubnetTx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + duplicateSubnetTx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrDuplicateValidator) } @@ -699,14 +690,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -740,14 +730,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -791,14 +780,13 @@ func TestProposalTxExecuteAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrDuplicateValidator) } } @@ -838,14 +826,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -873,14 +860,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -922,14 +908,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -965,14 +950,13 @@ func TestProposalTxExecuteAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - executor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrFlowCheckFailed) } } diff --git a/vms/platformvm/txs/executor/reward_validator_test.go b/vms/platformvm/txs/executor/reward_validator_test.go index 86fb83f994ed..e10da2bfd20e 100644 --- a/vms/platformvm/txs/executor/reward_validator_test.go +++ b/vms/platformvm/txs/executor/reward_validator_test.go @@ -61,14 +61,13 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAbortState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveStakerTooEarly) // Advance chain timestamp to time that next validator leaves @@ -84,14 +83,13 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveWrongStaker) // Case 3: Happy path @@ -104,16 +102,15 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - onCommitStakerIterator, err := txExecutor.OnCommitState.GetCurrentStakerIterator() + onCommitStakerIterator, err := onCommitState.GetCurrentStakerIterator() require.NoError(err) require.True(onCommitStakerIterator.Next()) @@ -128,7 +125,7 @@ func TestRewardValidatorTxExecuteOnCommit(t *testing.T) { oldBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -165,14 +162,13 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAbortState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveStakerTooEarly) // Advance chain timestamp to time that next validator leaves @@ -182,14 +178,13 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { tx, err = newRewardValidatorTx(t, ids.GenerateTestID()) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&txExecutor) + err = ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + ) require.ErrorIs(err, ErrRemoveWrongStaker) // Case 3: Happy path @@ -202,16 +197,15 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) - onAbortStakerIterator, err := txExecutor.OnAbortState.GetCurrentStakerIterator() + onAbortStakerIterator, err := onAbortState.GetCurrentStakerIterator() require.NoError(err) require.True(onAbortStakerIterator.Next()) @@ -226,7 +220,7 @@ func TestRewardValidatorTxExecuteOnAbort(t *testing.T) { oldBalance, err := avax.GetBalance(env.state, stakeOwners) require.NoError(err) - require.NoError(txExecutor.OnAbortState.Apply(env.state)) + require.NoError(onAbortState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -322,14 +316,13 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) vdrDestSet := set.Of(vdrRewardAddress) delDestSet := set.Of(delRewardAddress) @@ -341,7 +334,7 @@ func TestRewardDelegatorTxExecuteOnCommitPreDelegateeDeferral(t *testing.T) { oldDelBalance, err := avax.GetBalance(env.state, delDestSet) require.NoError(err) - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -464,14 +457,13 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) // 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. @@ -498,7 +490,7 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { require.ErrorIs(err, database.ErrNotFound) // Commit Delegator Diff - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -513,14 +505,13 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { onAbortState, err = state.NewDiff(lastAcceptedID, env) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) require.NotEqual(vdrStaker.TxID, delStaker.TxID) @@ -569,7 +560,7 @@ func TestRewardDelegatorTxExecuteOnCommitPostDelegateeDeferral(t *testing.T) { require.ErrorIs(err, database.ErrNotFound) // Commit Validator Diff - require.NoError(txExecutor.OnCommitState.Apply(env.state)) + require.NoError(onCommitState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) @@ -687,14 +678,13 @@ func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t * require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, delOnCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: delOnCommitState, - OnAbortState: delOnAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + delOnCommitState, + delOnAbortState, + )) // Create Validator Diffs testID := ids.GenerateTestID() @@ -709,14 +699,13 @@ func TestRewardDelegatorTxAndValidatorTxExecuteOnCommitPostDelegateeDeferral(t * tx, err = newRewardValidatorTx(t, vdrTx.ID()) require.NoError(err) - txExecutor = ProposalTxExecutor{ - OnCommitState: vdrOnCommitState, - OnAbortState: vdrOnAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + vdrOnCommitState, + vdrOnAbortState, + )) // aborted validator tx should still distribute accrued delegator rewards numVdrStakeUTXOs := uint32(len(delTx.Unsigned.InputIDs())) @@ -849,14 +838,13 @@ func TestRewardDelegatorTxExecuteOnAbort(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onCommitState) - txExecutor := ProposalTxExecutor{ - OnCommitState: onCommitState, - OnAbortState: onAbortState, - Backend: &env.backend, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&txExecutor)) + require.NoError(ProposalTx( + &env.backend, + feeCalculator, + tx, + onCommitState, + onAbortState, + )) vdrDestSet := set.Of(vdrRewardAddress) delDestSet := set.Of(delRewardAddress) @@ -868,7 +856,7 @@ func TestRewardDelegatorTxExecuteOnAbort(t *testing.T) { oldDelBalance, err := avax.GetBalance(env.state, delDestSet) require.NoError(err) - require.NoError(txExecutor.OnAbortState.Apply(env.state)) + require.NoError(onAbortState.Apply(env.state)) env.state.SetHeight(dummyHeight) require.NoError(env.state.Commit()) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index d30049cc92c3..7c561473a7bd 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -27,7 +27,7 @@ import ( ) var ( - _ txs.Visitor = (*StandardTxExecutor)(nil) + _ txs.Visitor = (*standardTxExecutor)(nil) errEmptyNodeID = errors.New("validator nodeID cannot be empty") errMaxStakeDurationTooLarge = errors.New("max stake duration must be less than or equal to the global max stake duration") @@ -37,161 +37,191 @@ var ( errMaxNumActiveValidators = errors.New("already at the max number of active validators") ) -type StandardTxExecutor struct { +// StandardTx executes the standard transaction [tx]. +// +// [state] is modified to represent the state of the chain after the execution +// of [tx]. +// +// Returns: +// - The IDs of any import UTXOs consumed. +// - The, potentially nil, atomic requests that should be performed against +// shared memory when this transaction is accepted. +// - A, potentially nil, function that should be called when this transaction +// is accepted. +func StandardTx( + backend *Backend, + feeCalculator fee.Calculator, + tx *txs.Tx, + state state.Diff, +) (set.Set[ids.ID], map[ids.ID]*atomic.Requests, func(), error) { + standardExecutor := standardTxExecutor{ + backend: backend, + feeCalculator: feeCalculator, + tx: tx, + state: state, + } + if err := tx.Unsigned.Visit(&standardExecutor); err != nil { + txID := tx.ID() + return nil, nil, nil, fmt.Errorf("standard tx %s failed execution: %w", txID, err) + } + return standardExecutor.inputs, standardExecutor.atomicRequests, standardExecutor.onAccept, nil +} + +type standardTxExecutor struct { // inputs, to be filled before visitor methods are called - *Backend - State state.Diff // state is expected to be modified - FeeCalculator fee.Calculator - Tx *txs.Tx + backend *Backend + state state.Diff // state is expected to be modified + feeCalculator fee.Calculator + tx *txs.Tx // outputs of visitor execution - OnAccept func() // may be nil - Inputs set.Set[ids.ID] - AtomicRequests map[ids.ID]*atomic.Requests // may be nil + onAccept func() // may be nil + inputs set.Set[ids.ID] + atomicRequests map[ids.ID]*atomic.Requests // may be nil } -func (*StandardTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { +func (*standardTxExecutor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { return ErrWrongTxType } -func (*StandardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { +func (*standardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } -func (e *StandardTxExecutor) CreateChainTx(tx *txs.CreateChainTx) error { - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { +func (e *standardTxExecutor) CreateChainTx(tx *txs.CreateChainTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } - baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.SubnetID, tx.SubnetAuth) + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.SubnetID, tx.SubnetAuth) if err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, baseTxCreds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Add the new chain to the database - e.State.AddChain(e.Tx) + e.state.AddChain(e.tx) // If this proposal is committed and this node is a member of the subnet // that validates the blockchain, create the blockchain - e.OnAccept = func() { - e.Config.CreateChain(txID, tx) + e.onAccept = func() { + e.backend.Config.CreateChain(txID, tx) } return nil } -func (e *StandardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { +func (e *standardTxExecutor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { // Make sure this transaction is well formed. - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Add the new subnet to the database - e.State.AddSubnet(txID) - e.State.SetSubnetOwner(txID, tx.Owner) + e.state.AddSubnet(txID) + e.state.SetSubnetOwner(txID, tx.Owner) return nil } -func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { +func (e *standardTxExecutor) ImportTx(tx *txs.ImportTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } - e.Inputs = set.NewSet[ids.ID](len(tx.ImportedInputs)) + e.inputs = set.NewSet[ids.ID](len(tx.ImportedInputs)) utxoIDs := make([][]byte, len(tx.ImportedInputs)) for i, in := range tx.ImportedInputs { utxoID := in.UTXOID.InputID() - e.Inputs.Add(utxoID) + e.inputs.Add(utxoID) utxoIDs[i] = utxoID[:] } // Skip verification of the shared memory inputs if the other primary // network chains are not guaranteed to be up-to-date. - if e.Bootstrapped.Get() && !e.Config.PartialSyncPrimaryNetwork { - if err := verify.SameSubnet(context.TODO(), e.Ctx, tx.SourceChain); err != nil { + if e.backend.Bootstrapped.Get() && !e.backend.Config.PartialSyncPrimaryNetwork { + if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.SourceChain); err != nil { return err } - allUTXOBytes, err := e.Ctx.SharedMemory.Get(tx.SourceChain, utxoIDs) + allUTXOBytes, err := e.backend.Ctx.SharedMemory.Get(tx.SourceChain, utxoIDs) if err != nil { return fmt.Errorf("failed to get shared memory: %w", err) } utxos := make([]*avax.UTXO, len(tx.Ins)+len(tx.ImportedInputs)) for index, input := range tx.Ins { - utxo, err := e.State.GetUTXO(input.InputID()) + utxo, err := e.state.GetUTXO(input.InputID()) if err != nil { return fmt.Errorf("failed to get UTXO %s: %w", &input.UTXOID, err) } @@ -210,35 +240,35 @@ func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { copy(ins[len(tx.Ins):], tx.ImportedInputs) // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpendUTXOs( + if err := e.backend.FlowChecker.VerifySpendUTXOs( tx, utxos, ins, tx.Outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Note: We apply atomic requests even if we are not verifying atomic // requests to ensure the shared state will be correct if we later start // verifying the requests. - e.AtomicRequests = map[ids.ID]*atomic.Requests{ + e.atomicRequests = map[ids.ID]*atomic.Requests{ tx.SourceChain: { RemoveRequests: utxoIDs, }, @@ -246,14 +276,14 @@ func (e *StandardTxExecutor) ImportTx(tx *txs.ImportTx) error { return nil } -func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { +func (e *standardTxExecutor) ExportTx(tx *txs.ExportTx) error { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } var ( - currentTimestamp = e.State.GetTimestamp() - isDurangoActive = e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + currentTimestamp = e.state.GetTimestamp() + isDurangoActive = e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) ) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err @@ -263,36 +293,36 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { copy(outs, tx.Outs) copy(outs[len(tx.Outs):], tx.ExportedOutputs) - if e.Bootstrapped.Get() { - if err := verify.SameSubnet(context.TODO(), e.Ctx, tx.DestinationChain); err != nil { + if e.backend.Bootstrapped.Get() { + if err := verify.SameSubnet(context.TODO(), e.backend.Ctx, tx.DestinationChain); err != nil { return err } } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return fmt.Errorf("failed verifySpend: %w", err) } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Note: We apply atomic requests even if we are not verifying atomic // requests to ensure the shared state will be correct if we later start @@ -323,7 +353,7 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { elems[i] = elem } - e.AtomicRequests = map[ids.ID]*atomic.Requests{ + e.atomicRequests = map[ids.ID]*atomic.Requests{ tx.DestinationChain: { PutRequests: elems, }, @@ -331,16 +361,16 @@ func (e *StandardTxExecutor) ExportTx(tx *txs.ExportTx) error { return nil } -func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { +func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { if tx.Validator.NodeID == ids.EmptyNodeID { return errEmptyNodeID } if _, err := verifyAddValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -350,12 +380,12 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) - if e.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.Ctx.NodeID { - e.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", zap.String("reason", "primary network is not being fully synced"), zap.Stringer("txID", txID), zap.String("txType", "addValidator"), @@ -365,12 +395,12 @@ func (e *StandardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { return nil } -func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { +func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { if err := verifyAddSubnetValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -380,18 +410,18 @@ func (e *StandardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { +func (e *standardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { if _, err := verifyAddDelegatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -401,9 +431,9 @@ func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } @@ -412,12 +442,12 @@ func (e *StandardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { // transaction will result in [tx.NodeID] being removed as a validator of // [tx.SubnetID]. // Note: [tx.NodeID] may be either a current or pending validator. -func (e *StandardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { +func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { staker, isCurrentValidator, err := verifyRemoveSubnetValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ) if err != nil { @@ -425,55 +455,55 @@ func (e *StandardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidat } if isCurrentValidator { - e.State.DeleteCurrentValidator(staker) + e.state.DeleteCurrentValidator(staker) } else { - e.State.DeletePendingValidator(staker) + e.state.DeletePendingValidator(staker) } // Invariant: There are no permissioned subnet delegators to remove. - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { - currentTimestamp := e.State.GetTimestamp() - if e.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { +func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { + currentTimestamp := e.state.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { return errTransformSubnetTxPostEtna } - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } - isDurangoActive := e.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + isDurangoActive := e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { return err } // Note: math.MaxInt32 * time.Second < math.MaxInt64 - so this can never // overflow. - if time.Duration(tx.MaxStakeDuration)*time.Second > e.Backend.Config.MaxStakeDuration { + if time.Duration(tx.MaxStakeDuration)*time.Second > e.backend.Config.MaxStakeDuration { return errMaxStakeDurationTooLarge } - baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.Subnet, tx.SubnetAuth) + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) if err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } totalRewardAmount := tx.MaximumSupply - tx.InitialSupply - if err := e.Backend.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, baseTxCreds, @@ -481,35 +511,35 @@ func (e *StandardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error // entry in this map literal from being overwritten by the // second entry. map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, - tx.AssetID: totalRewardAmount, + e.backend.Ctx.AVAXAssetID: fee, + tx.AssetID: totalRewardAmount, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Transform the new subnet in the database - e.State.AddSubnetTransformation(e.Tx) - e.State.SetCurrentSupply(tx.Subnet, tx.InitialSupply) + e.state.AddSubnetTransformation(e.tx) + e.state.SetCurrentSupply(tx.Subnet, tx.InitialSupply) return nil } -func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { +func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { var ( - currentTimestamp = e.State.GetTimestamp() - upgrades = e.Backend.Config.UpgradeConfig + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig ) if !upgrades.IsEtnaActivated(currentTimestamp) { return errEtnaUpgradeNotActive } - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } @@ -517,20 +547,20 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return err } - baseTxCreds, err := verifyPoASubnetAuthorization(e.Backend, e.State, e.Tx, tx.Subnet, tx.SubnetAuth) + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) if err != nil { return err } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } var ( startTime = uint64(currentTimestamp.Unix()) - currentFees = e.State.GetAccruedFees() + currentFees = e.state.GetAccruedFees() subnetConversionData = message.SubnetConversionData{ SubnetID: tx.Subnet, ManagerChainID: tx.ChainID, @@ -567,7 +597,7 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { } if vdr.Balance != 0 { // We are attempting to add an active validator - if gas.Gas(e.State.NumActiveSubnetOnlyValidators()) >= e.Backend.Config.ValidatorFeeConfig.Capacity { + if gas.Gas(e.state.NumActiveSubnetOnlyValidators()) >= e.backend.Config.ValidatorFeeConfig.Capacity { return errMaxNumActiveValidators } @@ -582,7 +612,7 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { } } - if err := e.State.PutSubnetOnlyValidator(sov); err != nil { + if err := e.state.PutSubnetOnlyValidator(sov); err != nil { return err } @@ -592,14 +622,14 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { Weight: vdr.Weight, } } - if err := e.Backend.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, baseTxCreds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err @@ -610,14 +640,14 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) // Track the subnet conversion in the database - e.State.SetSubnetConversion( + e.state.SetSubnetConversion( tx.Subnet, state.SubnetConversion{ ConversionID: conversionID, @@ -628,12 +658,12 @@ func (e *StandardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return nil } -func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { +func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if err := verifyAddPermissionlessValidatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -643,14 +673,14 @@ func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionl return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) - if e.Config.PartialSyncPrimaryNetwork && + if e.backend.Config.PartialSyncPrimaryNetwork && tx.Subnet == constants.PrimaryNetworkID && - tx.Validator.NodeID == e.Ctx.NodeID { - e.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", zap.String("reason", "primary network is not being fully synced"), zap.Stringer("txID", txID), zap.String("txType", "addPermissionlessValidator"), @@ -661,12 +691,12 @@ func (e *StandardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionl return nil } -func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { +func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { if err := verifyAddPermissionlessDelegatorTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ); err != nil { return err @@ -676,9 +706,9 @@ func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionl return err } - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } @@ -686,37 +716,37 @@ func (e *StandardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionl // [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. // This transaction will result in the ownership of [tx.Subnet] being transferred // to [tx.Owner]. -func (e *StandardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { +func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { err := verifyTransferSubnetOwnershipTx( - e.Backend, - e.FeeCalculator, - e.State, - e.Tx, + e.backend, + e.feeCalculator, + e.state, + e.tx, tx, ) if err != nil { return err } - e.State.SetSubnetOwner(tx.Subnet, tx.Owner) + e.state.SetSubnetOwner(tx.Subnet, tx.Owner) - txID := e.Tx.ID() - avax.Consume(e.State, tx.Ins) - avax.Produce(e.State, txID, tx.Outs) + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) return nil } -func (e *StandardTxExecutor) BaseTx(tx *txs.BaseTx) error { +func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { var ( - currentTimestamp = e.State.GetTimestamp() - upgrades = e.Backend.Config.UpgradeConfig + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig ) if !upgrades.IsDurangoActivated(currentTimestamp) { return ErrDurangoUpgradeNotActive } // Verify the tx is well-formed - if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } @@ -725,41 +755,41 @@ func (e *StandardTxExecutor) BaseTx(tx *txs.BaseTx) error { } // Verify the flowcheck - fee, err := e.FeeCalculator.CalculateFee(tx) + fee, err := e.feeCalculator.CalculateFee(tx) if err != nil { return err } - if err := e.FlowChecker.VerifySpend( + if err := e.backend.FlowChecker.VerifySpend( tx, - e.State, + e.state, tx.Ins, tx.Outs, - e.Tx.Creds, + e.tx.Creds, map[ids.ID]uint64{ - e.Ctx.AVAXAssetID: fee, + e.backend.Ctx.AVAXAssetID: fee, }, ); err != nil { return err } - txID := e.Tx.ID() + txID := e.tx.ID() // Consume the UTXOS - avax.Consume(e.State, tx.Ins) + avax.Consume(e.state, tx.Ins) // Produce the UTXOS - avax.Produce(e.State, txID, tx.Outs) + avax.Produce(e.state, txID, tx.Outs) return nil } // Creates the staker as defined in [stakerTx] and adds it to [e.State]. -func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { +func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( - chainTime = e.State.GetTimestamp() - txID = e.Tx.ID() + chainTime = e.state.GetTimestamp() + txID = e.tx.ID() staker *state.Staker err error ) - if !e.Config.UpgradeConfig.IsDurangoActivated(chainTime) { + if !e.backend.Config.UpgradeConfig.IsDurangoActivated(chainTime) { // Pre-Durango, stakers set a future [StartTime] and are added to the // pending staker set. They are promoted to the current staker set once // the chain time reaches [StartTime]. @@ -775,12 +805,12 @@ func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { var potentialReward uint64 if !stakerTx.CurrentPriority().IsPermissionedValidator() { subnetID := stakerTx.SubnetID() - currentSupply, err := e.State.GetCurrentSupply(subnetID) + currentSupply, err := e.state.GetCurrentSupply(subnetID) if err != nil { return err } - rewards, err := GetRewardsCalculator(e.Backend, e.State, subnetID) + rewards, err := GetRewardsCalculator(e.backend, e.state, subnetID) if err != nil { return err } @@ -794,7 +824,7 @@ func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { currentSupply, ) - e.State.SetCurrentSupply(subnetID, currentSupply+potentialReward) + e.state.SetCurrentSupply(subnetID, currentSupply+potentialReward) } staker, err = state.NewCurrentStaker(txID, stakerTx, chainTime, potentialReward) @@ -805,17 +835,17 @@ func (e *StandardTxExecutor) putStaker(stakerTx txs.Staker) error { switch priority := staker.Priority; { case priority.IsCurrentValidator(): - if err := e.State.PutCurrentValidator(staker); err != nil { + if err := e.state.PutCurrentValidator(staker); err != nil { return err } case priority.IsCurrentDelegator(): - e.State.PutCurrentDelegator(staker) + e.state.PutCurrentDelegator(staker) case priority.IsPendingValidator(): - if err := e.State.PutPendingValidator(staker); err != nil { + if err := e.state.PutPendingValidator(staker); err != nil { return err } case priority.IsPendingDelegator(): - e.State.PutPendingDelegator(staker) + e.state.PutPendingDelegator(staker) default: return fmt.Errorf("staker %s, unexpected priority %d", staker.TxID, priority) } diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 1080013ed776..8a2e574ccfc5 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -101,13 +101,12 @@ func TestStandardTxExecutorAddValidatorTxEmptyID(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, stateDiff) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: stateDiff, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + stateDiff, + ) require.ErrorIs(err, errEmptyNodeID) } } @@ -353,13 +352,12 @@ func TestStandardTxExecutorAddDelegator(t *testing.T) { env.config.UpgradeConfig.BanffTime = onAcceptState.GetTimestamp() feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, tt.expectedExecutionErr) }) } @@ -400,13 +398,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -435,13 +432,13 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) + require.NoError(err) } // Add a validator to pending validator set of primary network @@ -488,13 +485,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrNotValidator) } @@ -538,13 +534,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -571,13 +566,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrPeriodMismatch) } @@ -604,13 +598,13 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - require.NoError(tx.Unsigned.Visit(&executor)) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) + require.NoError(err) } // Case: Proposed validator start validating at/before current timestamp @@ -639,13 +633,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -707,13 +700,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrDuplicateValidator) } @@ -751,13 +743,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, secp256k1fx.ErrInputIndicesNotSortedUnique) } @@ -791,13 +782,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -829,13 +819,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errUnauthorizedSubnetModification) } @@ -877,13 +866,12 @@ func TestApricotStandardTxExecutorAddSubnetValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrDuplicateValidator) } } @@ -925,12 +913,13 @@ func TestEtnaStandardTxExecutorAddSubnetValidator(t *testing.T) { }, ) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errIsImmutable) } @@ -965,13 +954,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrTimestampNotBeforeStartTime) } @@ -1008,13 +996,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { onAcceptState.AddTx(tx, status.Committed) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -1048,13 +1035,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { onAcceptState.AddTx(tx, status.Committed) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrAlreadyValidator) } @@ -1089,13 +1075,12 @@ func TestBanffStandardTxExecutorAddValidator(t *testing.T) { } feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - executor := StandardTxExecutor{ - Backend: &env.backend, - FeeCalculator: feeCalculator, - State: onAcceptState, - Tx: tx, - } - err = tx.Unsigned.Visit(&executor) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, ErrFlowCheckFailed) } } @@ -1188,14 +1173,13 @@ func TestDurangoDisabledTransactions(t *testing.T) { require.NoError(err) tx := tt.buildTx(t, env) - feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - err = tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - }) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, tt.expectedErr) }) } @@ -1401,12 +1385,13 @@ func TestDurangoMemoField(t *testing.T) { require.NoError(err) feeCalculator := state.PickFeeCalculator(env.config, onAcceptState) - require.NoError(subnetValTx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: subnetValTx, - })) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + subnetValTx, + onAcceptState, + ) + require.NoError(err) tx, err := wallet.IssueRemoveSubnetValidatorTx( primaryValidator.NodeID, @@ -1592,22 +1577,23 @@ func TestDurangoMemoField(t *testing.T) { // Populated memo field should error tx, onAcceptState := tt.setupTest(t, env, []byte{'m', 'e', 'm', 'o'}) - err := tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - }) + _, _, _, err := StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, avax.ErrMemoTooLarge) // Empty memo field should not error tx, onAcceptState = tt.setupTest(t, env, []byte{}) - require.NoError(tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - FeeCalculator: feeCalculator, - Tx: tx, - })) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) + require.NoError(err) }) } } @@ -1623,15 +1609,16 @@ func TestEtnaDisabledTransactions(t *testing.T) { onAcceptState, err := state.NewDiff(env.state.GetLastAccepted(), env) require.NoError(err) + feeCalculator := state.PickFeeCalculator(env.config, env.state) tx := &txs.Tx{ Unsigned: &txs.TransformSubnetTx{}, } - - err = tx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &env.backend, - State: onAcceptState, - Tx: tx, - }) + _, _, _, err = StandardTx( + &env.backend, + feeCalculator, + tx, + onAcceptState, + ) require.ErrorIs(err, errTransformSubnetTxPostEtna) } @@ -1735,14 +1722,14 @@ func newValidRemoveSubnetValidatorTxVerifyEnv(t *testing.T, ctrl *gomock.Control func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { type test struct { name string - newExecutor func(*gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) + newExecutor func(*gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) expectedErr error } tests := []test{ { name: "valid tx", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) // Set dependency expectations. @@ -1774,26 +1761,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Etna, env.latestForkTime), } feeCalculator := state.NewStaticFeeCalculator(cfg, env.state.GetTimestamp()) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: nil, }, { name: "tx fails syntactic verification", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) // Setting the subnet ID to the Primary Network ID makes the tx fail syntactic verification env.tx.Unsigned.(*txs.RemoveSubnetValidatorTx).Subnet = constants.PrimaryNetworkID @@ -1804,26 +1790,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: txs.ErrRemovePrimaryNetworkValidator, }, { name: "node isn't a validator of the subnet", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1834,26 +1819,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: ErrNotValidator, }, { name: "validator is permissionless", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) staker := *env.staker @@ -1867,26 +1851,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: ErrRemovePermissionlessValidator, }, { name: "tx has no credentials", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) // Remove credentials env.tx.Creds = nil @@ -1898,26 +1881,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: errWrongNumberOfCredentials, }, { name: "can't find subnet", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1928,26 +1910,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: database.ErrNotFound, }, { name: "no permission to remove validator", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1960,26 +1941,25 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: errUnauthorizedSubnetModification, }, { name: "flow checker failed", - newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.RemoveSubnetValidatorTx, *standardTxExecutor) { env := newValidRemoveSubnetValidatorTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) env.state.EXPECT().GetTimestamp().Return(env.latestForkTime).AnyTimes() @@ -1995,19 +1975,18 @@ func TestStandardExecutorRemoveSubnetValidatorTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, expectedErr: ErrFlowCheckFailed, @@ -2137,14 +2116,14 @@ func newValidTransformSubnetTxVerifyEnv(t *testing.T, ctrl *gomock.Controller) t func TestStandardExecutorTransformSubnetTx(t *testing.T) { type test struct { name string - newExecutor func(*gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) + newExecutor func(*gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) err error } tests := []test{ { name: "tx fails syntactic verification", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Setting the tx to nil makes the tx fail syntactic verification env.tx.Unsigned = (*txs.TransformSubnetTx)(nil) @@ -2155,26 +2134,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: txs.ErrNilTx, }, { name: "max stake duration too large", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) env.unsignedTx.MaxStakeDuration = math.MaxUint32 env.state = state.NewMockDiff(ctrl) @@ -2184,26 +2162,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { UpgradeConfig: upgradetest.GetConfigWithUpgradeTime(upgradetest.Durango, env.latestForkTime), } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: errMaxStakeDurationTooLarge, }, { name: "fail subnet authorization", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Remove credentials env.tx.Creds = nil @@ -2216,26 +2193,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: errWrongNumberOfCredentials, }, { name: "flow checker failed", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) env.state = state.NewMockDiff(ctrl) subnetOwner := fxmock.NewOwner(ctrl) @@ -2257,26 +2233,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: ErrFlowCheckFailed, }, { name: "invalid after subnet conversion", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Set dependency expectations. @@ -2299,26 +2274,25 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { MaxStakeDuration: math.MaxInt64, } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: errIsImmutable, }, { name: "valid tx", - newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *StandardTxExecutor) { + newExecutor: func(ctrl *gomock.Controller) (*txs.TransformSubnetTx, *standardTxExecutor) { env := newValidTransformSubnetTxVerifyEnv(t, ctrl) // Set dependency expectations. @@ -2345,19 +2319,18 @@ func TestStandardExecutorTransformSubnetTx(t *testing.T) { } feeCalculator := state.PickFeeCalculator(cfg, env.state) - e := &StandardTxExecutor{ - Backend: &Backend{ + e := &standardTxExecutor{ + backend: &Backend{ Config: cfg, - Bootstrapped: &utils.Atomic[bool]{}, + Bootstrapped: utils.NewAtomic(true), Fx: env.fx, FlowChecker: env.flowChecker, Ctx: &snow.Context{}, }, - FeeCalculator: feeCalculator, - Tx: env.tx, - State: env.state, + feeCalculator: feeCalculator, + tx: env.tx, + state: env.state, } - e.Bootstrapped.Set(true) return env.unsignedTx, e }, err: nil, @@ -2419,18 +2392,19 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { diff, err := state.NewDiffOn(baseState) require.NoError(t, err) - require.NoError(t, createSubnetTx.Unsigned.Visit(&StandardTxExecutor{ - Backend: &Backend{ + _, _, _, err = StandardTx( + &Backend{ Config: defaultConfig, Bootstrapped: utils.NewAtomic(true), Fx: fx, FlowChecker: flowChecker, Ctx: ctx, }, - FeeCalculator: state.PickFeeCalculator(defaultConfig, baseState), - Tx: createSubnetTx, - State: diff, - })) + state.PickFeeCalculator(defaultConfig, baseState), + createSubnetTx, + diff, + ) + require.NoError(t, err) require.NoError(t, diff.Apply(baseState)) require.NoError(t, baseState.Commit()) @@ -2441,13 +2415,13 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { tests := []struct { name string builderOptions []common.Option - updateExecutor func(executor *StandardTxExecutor) error + updateExecutor func(executor *standardTxExecutor) error expectedErr error }{ { name: "invalid prior to E-Upgrade", - updateExecutor: func(e *StandardTxExecutor) error { - e.Backend.Config = &config.Config{ + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ UpgradeConfig: upgradetest.GetConfig(upgradetest.Durango), } return nil @@ -2456,8 +2430,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "tx fails syntactic verification", - updateExecutor: func(e *StandardTxExecutor) error { - e.Backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) return nil }, expectedErr: avax.ErrWrongChainID, @@ -2471,8 +2445,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "fail subnet authorization", - updateExecutor: func(e *StandardTxExecutor) error { - e.State.SetSubnetOwner(subnetID, &secp256k1fx.OutputOwners{ + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetOwner(subnetID, &secp256k1fx.OutputOwners{ Threshold: 1, Addrs: []ids.ShortID{ ids.GenerateTestShortID(), @@ -2484,8 +2458,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid if subnet is transformed", - updateExecutor: func(e *StandardTxExecutor) error { - e.State.AddSubnetTransformation(&txs.Tx{Unsigned: &txs.TransformSubnetTx{ + updateExecutor: func(e *standardTxExecutor) error { + e.state.AddSubnetTransformation(&txs.Tx{Unsigned: &txs.TransformSubnetTx{ Subnet: subnetID, }}) return nil @@ -2494,8 +2468,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid if subnet is converted", - updateExecutor: func(e *StandardTxExecutor) error { - e.State.SetSubnetConversion( + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion( subnetID, state.SubnetConversion{ ConversionID: ids.GenerateTestID(), @@ -2509,16 +2483,16 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid fee calculation", - updateExecutor: func(e *StandardTxExecutor) error { - e.FeeCalculator = txfee.NewStaticCalculator(e.Config.StaticFeeConfig) + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewStaticCalculator(e.backend.Config.StaticFeeConfig) return nil }, expectedErr: txfee.ErrUnsupportedTx, }, { name: "too many active validators", - updateExecutor: func(e *StandardTxExecutor) error { - e.Backend.Config = &config.Config{ + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, ValidatorFeeConfig: validatorfee.Config{ Capacity: 0, @@ -2534,8 +2508,8 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "invalid subnet only validator", - updateExecutor: func(e *StandardTxExecutor) error { - return e.State.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ + updateExecutor: func(e *standardTxExecutor) error { + return e.state.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ ValidationID: ids.GenerateTestID(), SubnetID: subnetID, NodeID: nodeID, @@ -2546,9 +2520,9 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }, { name: "insufficient fee", - updateExecutor: func(e *StandardTxExecutor) error { - e.FeeCalculator = txfee.NewDynamicCalculator( - e.Config.DynamicFeeConfig.Weights, + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewDynamicCalculator( + e.backend.Config.DynamicFeeConfig.Weights, 100*genesis.LocalParams.DynamicFeeConfig.MinPrice, ) return nil @@ -2607,17 +2581,17 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { diff, err := state.NewDiffOn(baseState) require.NoError(err) - executor := &StandardTxExecutor{ - Backend: &Backend{ + executor := &standardTxExecutor{ + backend: &Backend{ Config: defaultConfig, Bootstrapped: utils.NewAtomic(true), Fx: fx, FlowChecker: flowChecker, Ctx: ctx, }, - FeeCalculator: state.PickFeeCalculator(defaultConfig, baseState), - Tx: convertSubnetTx, - State: diff, + feeCalculator: state.PickFeeCalculator(defaultConfig, baseState), + tx: convertSubnetTx, + state: diff, } if test.updateExecutor != nil { require.NoError(test.updateExecutor(executor)) From 4c866494af08207bc31cbf9419c87eb7ab654914 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:12:41 -0500 Subject: [PATCH 155/184] add TODO --- vms/platformvm/txs/executor/standard_tx_executor.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 404d6bd84943..b9331b116bfe 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -30,6 +30,8 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" ) +// TODO: Before Etna, ensure that the maximum number of expiries to track is +// limited to a reasonable number by this window. const ( second = 1 minute = 60 * second From 987c4e2871672afed7c933d7f9c3255fe92fd574 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:26:09 -0500 Subject: [PATCH 156/184] reduce diff --- vms/platformvm/txs/executor/standard_tx_executor.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index b9331b116bfe..78cbe4f307c0 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -838,9 +838,6 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal if err != nil { return err } - if warpMessage.NetworkID != e.backend.Ctx.NetworkID { - return fmt.Errorf("expected networkID %d but got %d", e.backend.Ctx.NetworkID, warpMessage.NetworkID) - } addressedCall, err := payload.ParseAddressedCall(warpMessage.Payload) if err != nil { From 98ffb01a75005627a961aab50157c897e6502a8c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:38:52 -0500 Subject: [PATCH 157/184] remove dead code --- vms/platformvm/txs/executor/standard_tx_executor.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 78cbe4f307c0..ab60bd03dc7d 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -867,15 +867,8 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal if msg.Expiry <= currentTimestampUnix { return fmt.Errorf("expected expiry to be after %d but got %d", currentTimestampUnix, msg.Expiry) } - maxAllowedExpiry, err := math.Add(currentTimestampUnix, RegisterSubnetValidatorTxExpiryWindow) - if err != nil { - // This should never happen, as it would imply that either - // currentTimestampUnix or RegisterSubnetValidatorTxExpiryWindow is - // significantly larger than expected. - return err - } - if msg.Expiry > maxAllowedExpiry { - return fmt.Errorf("expected expiry not to be after %d but got %d", maxAllowedExpiry, msg.Expiry) + if secondsUntilExpiry := msg.Expiry - currentTimestampUnix; secondsUntilExpiry > RegisterSubnetValidatorTxExpiryWindow { + return fmt.Errorf("expected expiry not to be more than %d seconds in the future but got %d", RegisterSubnetValidatorTxExpiryWindow, secondsUntilExpiry) } pop := signer.ProofOfPossession{ From 5b06d93a84847414ca151aeff55f4be03514c3aa Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:43:16 -0500 Subject: [PATCH 158/184] Standardize P-Chain tx visitor order (#3529) --- vms/platformvm/metrics/tx_metrics.go | 8 +- .../txs/executor/atomic_tx_executor.go | 6 +- .../txs/executor/standard_tx_executor.go | 368 +++++++++--------- vms/platformvm/txs/fee/complexity.go | 232 +++++------ vms/platformvm/txs/fee/static_calculator.go | 22 +- vms/platformvm/txs/visitor.go | 9 +- wallet/chain/p/signer/visitor.go | 32 +- wallet/chain/p/wallet/backend_visitor.go | 40 +- 8 files changed, 362 insertions(+), 355 deletions(-) diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index fd6c63494f7c..7957cb6ab2e9 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -132,16 +132,16 @@ func (m *txMetrics) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) er return nil } -func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (m *txMetrics) BaseTx(*txs.BaseTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "convert_subnet", + txLabel: "base", }).Inc() return nil } -func (m *txMetrics) BaseTx(*txs.BaseTx) error { +func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { m.numTxs.With(prometheus.Labels{ - txLabel: "base", + txLabel: "convert_subnet", }).Inc() return nil } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index ea5294c2559c..1977608d09c1 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -92,15 +92,15 @@ func (*atomicTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return ErrWrongTxType } -func (*atomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*atomicTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return ErrWrongTxType } diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 7c561473a7bd..3ccc059bfa8e 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -88,6 +88,82 @@ func (*standardTxExecutor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrWrongTxType } +func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { + if tx.Validator.NodeID == ids.EmptyNodeID { + return errEmptyNodeID + } + + if _, err := verifyAddValidatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); err != nil { + return err + } + + txID := e.tx.ID() + avax.Consume(e.state, tx.Ins) + avax.Produce(e.state, txID, tx.Outs) + + if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + zap.String("reason", "primary network is not being fully synced"), + zap.Stringer("txID", txID), + zap.String("txType", "addValidator"), + zap.Stringer("nodeID", tx.Validator.NodeID), + ) + } + return nil +} + +func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { + if err := verifyAddSubnetValidatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) AddDelegatorTx(tx *txs.AddDelegatorTx) error { + if _, err := verifyAddDelegatorTx( + e.backend, + e.feeCalculator, + e.state, + e.tx, + tx, + ); err != nil { + return err + } + + if err := e.putStaker(tx); 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) CreateChainTx(tx *txs.CreateChainTx) error { if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err @@ -361,42 +437,101 @@ func (e *standardTxExecutor) ExportTx(tx *txs.ExportTx) error { return nil } -func (e *standardTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { - if tx.Validator.NodeID == ids.EmptyNodeID { - return errEmptyNodeID - } - - if _, err := verifyAddValidatorTx( +// Verifies a [*txs.RemoveSubnetValidatorTx] and, if it passes, executes it on +// [e.State]. For verification rules, see [verifyRemoveSubnetValidatorTx]. This +// transaction will result in [tx.NodeID] being removed as a validator of +// [tx.SubnetID]. +// Note: [tx.NodeID] may be either a current or pending validator. +func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + staker, isCurrentValidator, err := verifyRemoveSubnetValidatorTx( e.backend, e.feeCalculator, e.state, e.tx, tx, - ); err != nil { + ) + if err != nil { return err } - if err := e.putStaker(tx); err != nil { - return err + if isCurrentValidator { + e.state.DeleteCurrentValidator(staker) + } else { + e.state.DeletePendingValidator(staker) } + // Invariant: There are no permissioned subnet delegators to remove. + txID := e.tx.ID() avax.Consume(e.state, tx.Ins) avax.Produce(e.state, txID, tx.Outs) - if e.backend.Config.PartialSyncPrimaryNetwork && tx.Validator.NodeID == e.backend.Ctx.NodeID { - e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", - zap.String("reason", "primary network is not being fully synced"), - zap.Stringer("txID", txID), - zap.String("txType", "addValidator"), - zap.Stringer("nodeID", tx.Validator.NodeID), - ) + return nil +} + +func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { + currentTimestamp := e.state.GetTimestamp() + if e.backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { + return errTransformSubnetTxPostEtna + } + + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { + return err + } + + isDurangoActive := e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) + if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { + return err + } + + // Note: math.MaxInt32 * time.Second < math.MaxInt64 - so this can never + // overflow. + if time.Duration(tx.MaxStakeDuration)*time.Second > e.backend.Config.MaxStakeDuration { + return errMaxStakeDurationTooLarge + } + + baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + + // Verify the flowcheck + fee, err := e.feeCalculator.CalculateFee(tx) + if err != nil { + return err } + totalRewardAmount := tx.MaximumSupply - tx.InitialSupply + if err := e.backend.FlowChecker.VerifySpend( + tx, + e.state, + tx.Ins, + tx.Outs, + baseTxCreds, + // Invariant: [tx.AssetID != e.Ctx.AVAXAssetID]. This prevents the first + // entry in this map literal from being overwritten by the + // second entry. + map[ids.ID]uint64{ + e.backend.Ctx.AVAXAssetID: fee, + tx.AssetID: totalRewardAmount, + }, + ); err != nil { + return err + } + + txID := e.tx.ID() + + // Consume the UTXOS + avax.Consume(e.state, tx.Ins) + // Produce the UTXOS + avax.Produce(e.state, txID, tx.Outs) + // Transform the new subnet in the database + e.state.AddSubnetTransformation(e.tx) + e.state.SetCurrentSupply(tx.Subnet, tx.InitialSupply) return nil } -func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { - if err := verifyAddSubnetValidatorTx( +func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + if err := verifyAddPermissionlessValidatorTx( e.backend, e.feeCalculator, e.state, @@ -413,11 +548,23 @@ func (e *standardTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) txID := e.tx.ID() avax.Consume(e.state, tx.Ins) avax.Produce(e.state, txID, tx.Outs) + + if e.backend.Config.PartialSyncPrimaryNetwork && + tx.Subnet == constants.PrimaryNetworkID && + tx.Validator.NodeID == e.backend.Ctx.NodeID { + e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", + zap.String("reason", "primary network is not being fully synced"), + zap.Stringer("txID", txID), + zap.String("txType", "addPermissionlessValidator"), + zap.Stringer("nodeID", tx.Validator.NodeID), + ) + } + return nil } -func (e *standardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { - if _, err := verifyAddDelegatorTx( +func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + if err := verifyAddPermissionlessDelegatorTx( e.backend, e.feeCalculator, e.state, @@ -437,13 +584,12 @@ func (e *standardTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { return nil } -// Verifies a [*txs.RemoveSubnetValidatorTx] and, if it passes, executes it on -// [e.State]. For verification rules, see [verifyRemoveSubnetValidatorTx]. This -// transaction will result in [tx.NodeID] being removed as a validator of -// [tx.SubnetID]. -// Note: [tx.NodeID] may be either a current or pending validator. -func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { - staker, isCurrentValidator, err := verifyRemoveSubnetValidatorTx( +// Verifies a [*txs.TransferSubnetOwnershipTx] and, if it passes, executes it on +// [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. +// This transaction will result in the ownership of [tx.Subnet] being transferred +// to [tx.Owner]. +func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + err := verifyTransferSubnetOwnershipTx( e.backend, e.feeCalculator, e.state, @@ -454,44 +600,29 @@ func (e *standardTxExecutor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidat return err } - if isCurrentValidator { - e.state.DeleteCurrentValidator(staker) - } else { - e.state.DeletePendingValidator(staker) - } - - // Invariant: There are no permissioned subnet delegators to remove. + e.state.SetSubnetOwner(tx.Subnet, tx.Owner) txID := e.tx.ID() avax.Consume(e.state, tx.Ins) avax.Produce(e.state, txID, tx.Outs) - return nil } -func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { - currentTimestamp := e.state.GetTimestamp() - if e.backend.Config.UpgradeConfig.IsEtnaActivated(currentTimestamp) { - return errTransformSubnetTxPostEtna +func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { + var ( + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig + ) + if !upgrades.IsDurangoActivated(currentTimestamp) { + return ErrDurangoUpgradeNotActive } + // Verify the tx is well-formed if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { return err } - isDurangoActive := e.backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) - if err := avax.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil { - return err - } - - // Note: math.MaxInt32 * time.Second < math.MaxInt64 - so this can never - // overflow. - if time.Duration(tx.MaxStakeDuration)*time.Second > e.backend.Config.MaxStakeDuration { - return errMaxStakeDurationTooLarge - } - - baseTxCreds, err := verifyPoASubnetAuthorization(e.backend, e.state, e.tx, tx.Subnet, tx.SubnetAuth) - if err != nil { + if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { return err } @@ -500,33 +631,24 @@ func (e *standardTxExecutor) TransformSubnetTx(tx *txs.TransformSubnetTx) error if err != nil { return err } - totalRewardAmount := tx.MaximumSupply - tx.InitialSupply if err := e.backend.FlowChecker.VerifySpend( tx, e.state, tx.Ins, tx.Outs, - baseTxCreds, - // Invariant: [tx.AssetID != e.Ctx.AVAXAssetID]. This prevents the first - // entry in this map literal from being overwritten by the - // second entry. + e.tx.Creds, map[ids.ID]uint64{ e.backend.Ctx.AVAXAssetID: fee, - tx.AssetID: totalRewardAmount, }, ); err != nil { return err } txID := e.tx.ID() - // Consume the UTXOS avax.Consume(e.state, tx.Ins) // Produce the UTXOS avax.Produce(e.state, txID, tx.Outs) - // Transform the new subnet in the database - e.state.AddSubnetTransformation(e.tx) - e.state.SetCurrentSupply(tx.Subnet, tx.InitialSupply) return nil } @@ -658,128 +780,6 @@ func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return nil } -func (e *standardTxExecutor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - if err := verifyAddPermissionlessValidatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - - if e.backend.Config.PartialSyncPrimaryNetwork && - tx.Subnet == constants.PrimaryNetworkID && - tx.Validator.NodeID == e.backend.Ctx.NodeID { - e.backend.Ctx.Log.Warn("verified transaction that would cause this node to become unhealthy", - zap.String("reason", "primary network is not being fully synced"), - zap.Stringer("txID", txID), - zap.String("txType", "addPermissionlessValidator"), - zap.Stringer("nodeID", tx.Validator.NodeID), - ) - } - - return nil -} - -func (e *standardTxExecutor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - if err := verifyAddPermissionlessDelegatorTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ); err != nil { - return err - } - - if err := e.putStaker(tx); err != nil { - return err - } - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - return nil -} - -// Verifies a [*txs.TransferSubnetOwnershipTx] and, if it passes, executes it on -// [e.State]. For verification rules, see [verifyTransferSubnetOwnershipTx]. -// This transaction will result in the ownership of [tx.Subnet] being transferred -// to [tx.Owner]. -func (e *standardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - err := verifyTransferSubnetOwnershipTx( - e.backend, - e.feeCalculator, - e.state, - e.tx, - tx, - ) - if err != nil { - return err - } - - e.state.SetSubnetOwner(tx.Subnet, tx.Owner) - - txID := e.tx.ID() - avax.Consume(e.state, tx.Ins) - avax.Produce(e.state, txID, tx.Outs) - return nil -} - -func (e *standardTxExecutor) BaseTx(tx *txs.BaseTx) error { - var ( - currentTimestamp = e.state.GetTimestamp() - upgrades = e.backend.Config.UpgradeConfig - ) - if !upgrades.IsDurangoActivated(currentTimestamp) { - return ErrDurangoUpgradeNotActive - } - - // Verify the tx is well-formed - if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { - return err - } - - if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { - return err - } - - // Verify the flowcheck - fee, err := e.feeCalculator.CalculateFee(tx) - if err != nil { - return err - } - if err := e.backend.FlowChecker.VerifySpend( - tx, - e.state, - tx.Ins, - tx.Outs, - e.tx.Creds, - map[ids.ID]uint64{ - e.backend.Ctx.AVAXAssetID: fee, - }, - ); err != nil { - return err - } - - txID := e.tx.ID() - // Consume the UTXOS - avax.Consume(e.state, tx.Ins) - // Produce the UTXOS - avax.Produce(e.state, txID, tx.Outs) - return nil -} - // Creates the staker as defined in [stakerTx] and adds it to [e.State]. func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 5683ac6b3b6a..e1891a3d51a1 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -83,29 +83,6 @@ const ( var ( _ txs.Visitor = (*complexityVisitor)(nil) - IntrinsicAddPermissionlessValidatorTxComplexities = gas.Dimensions{ - gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - intrinsicValidatorBandwidth + // validator - ids.IDLen + // subnetID - wrappers.IntLen + // signer typeID - wrappers.IntLen + // num stake outs - wrappers.IntLen + // validator rewards typeID - wrappers.IntLen + // delegator rewards typeID - wrappers.IntLen, // delegation shares - gas.DBRead: 1, - gas.DBWrite: 1, - gas.Compute: 0, - } - IntrinsicAddPermissionlessDelegatorTxComplexities = gas.Dimensions{ - gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - intrinsicValidatorBandwidth + // validator - ids.IDLen + // subnetID - wrappers.IntLen + // num stake outs - wrappers.IntLen, // delegator rewards typeID - gas.DBRead: 1, - gas.DBWrite: 1, - gas.Compute: 0, - } IntrinsicAddSubnetValidatorTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + intrinsicSubnetValidatorBandwidth + // subnetValidator @@ -115,19 +92,6 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } - IntrinsicBaseTxComplexities = gas.Dimensions{ - gas.Bandwidth: codec.VersionSize + // codecVersion - wrappers.IntLen + // typeID - wrappers.IntLen + // networkID - ids.IDLen + // blockchainID - wrappers.IntLen + // number of outputs - wrappers.IntLen + // number of inputs - wrappers.IntLen + // length of memo - wrappers.IntLen, // number of credentials - gas.DBRead: 0, - gas.DBWrite: 0, - gas.Compute: 0, - } IntrinsicCreateChainTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + ids.IDLen + // subnetID @@ -148,18 +112,18 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } - IntrinsicExportTxComplexities = gas.Dimensions{ + IntrinsicImportTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - ids.IDLen + // destination chainID - wrappers.IntLen, // num exported outputs + ids.IDLen + // source chainID + wrappers.IntLen, // num importing inputs gas.DBRead: 0, gas.DBWrite: 0, gas.Compute: 0, } - IntrinsicImportTxComplexities = gas.Dimensions{ + IntrinsicExportTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + - ids.IDLen + // source chainID - wrappers.IntLen, // num importing inputs + ids.IDLen + // destination chainID + wrappers.IntLen, // num exported outputs gas.DBRead: 0, gas.DBWrite: 0, gas.Compute: 0, @@ -174,6 +138,29 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } + IntrinsicAddPermissionlessValidatorTxComplexities = gas.Dimensions{ + gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + + intrinsicValidatorBandwidth + // validator + ids.IDLen + // subnetID + wrappers.IntLen + // signer typeID + wrappers.IntLen + // num stake outs + wrappers.IntLen + // validator rewards typeID + wrappers.IntLen + // delegator rewards typeID + wrappers.IntLen, // delegation shares + gas.DBRead: 1, + gas.DBWrite: 1, + gas.Compute: 0, + } + IntrinsicAddPermissionlessDelegatorTxComplexities = gas.Dimensions{ + gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + + intrinsicValidatorBandwidth + // validator + ids.IDLen + // subnetID + wrappers.IntLen + // num stake outs + wrappers.IntLen, // delegator rewards typeID + gas.DBRead: 1, + gas.DBWrite: 1, + gas.Compute: 0, + } IntrinsicTransferSubnetOwnershipTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + ids.IDLen + // subnetID @@ -184,6 +171,19 @@ var ( gas.DBWrite: 1, gas.Compute: 0, } + IntrinsicBaseTxComplexities = gas.Dimensions{ + gas.Bandwidth: codec.VersionSize + // codecVersion + wrappers.IntLen + // typeID + wrappers.IntLen + // networkID + ids.IDLen + // blockchainID + wrappers.IntLen + // number of outputs + wrappers.IntLen + // number of inputs + wrappers.IntLen + // length of memo + wrappers.IntLen, // number of credentials + gas.DBRead: 0, + gas.DBWrite: 0, + gas.Compute: 0, + } IntrinsicConvertSubnetTxComplexities = gas.Dimensions{ gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + ids.IDLen + // subnetID @@ -440,11 +440,11 @@ type complexityVisitor struct { output gas.Dimensions } -func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { return ErrUnsupportedTx } -func (*complexityVisitor) AddValidatorTx(*txs.AddValidatorTx) error { +func (*complexityVisitor) AddDelegatorTx(*txs.AddDelegatorTx) error { return ErrUnsupportedTx } @@ -460,60 +460,6 @@ func (*complexityVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { return ErrUnsupportedTx } -func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - signerComplexity, err := SignerComplexity(tx.Signer) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) - if err != nil { - return err - } - delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( - &baseTxComplexity, - &signerComplexity, - &outputsComplexity, - &validatorOwnerComplexity, - &delegatorOwnerComplexity, - ) - return err -} - -func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { - // TODO: Should we include additional complexity for subnets? - baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) - if err != nil { - return err - } - ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) - if err != nil { - return err - } - outputsComplexity, err := OutputComplexity(tx.StakeOuts...) - if err != nil { - return err - } - c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( - &baseTxComplexity, - &ownerComplexity, - &outputsComplexity, - ) - return err -} - func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { @@ -530,15 +476,6 @@ func (c *complexityVisitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) e return err } -func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { - baseTxComplexity, err := baseTxComplexity(tx) - if err != nil { - return err - } - c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) - return err -} - func (c *complexityVisitor) CreateChainTx(tx *txs.CreateChainTx) error { bandwidth, err := math.Mul(uint64(len(tx.FxIDs)), ids.IDLen) if err != nil { @@ -591,6 +528,23 @@ func (c *complexityVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return err } +func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + // TODO: Should imported inputs be more complex? + inputsComplexity, err := InputComplexity(tx.ImportedInputs...) + if err != nil { + return err + } + c.output, err = IntrinsicImportTxComplexities.Add( + &baseTxComplexity, + &inputsComplexity, + ) + return err +} + func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { @@ -608,35 +562,72 @@ func (c *complexityVisitor) ExportTx(tx *txs.ExportTx) error { return err } -func (c *complexityVisitor) ImportTx(tx *txs.ImportTx) error { +func (c *complexityVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - // TODO: Should imported inputs be more complex? - inputsComplexity, err := InputComplexity(tx.ImportedInputs...) + authComplexity, err := AuthComplexity(tx.SubnetAuth) if err != nil { return err } - c.output, err = IntrinsicImportTxComplexities.Add( + c.output, err = IntrinsicRemoveSubnetValidatorTxComplexities.Add( &baseTxComplexity, - &inputsComplexity, + &authComplexity, ) return err } -func (c *complexityVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { +func (c *complexityVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { + // TODO: Should we include additional complexity for subnets? baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { return err } - authComplexity, err := AuthComplexity(tx.SubnetAuth) + signerComplexity, err := SignerComplexity(tx.Signer) if err != nil { return err } - c.output, err = IntrinsicRemoveSubnetValidatorTxComplexities.Add( + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + validatorOwnerComplexity, err := OwnerComplexity(tx.ValidatorRewardsOwner) + if err != nil { + return err + } + delegatorOwnerComplexity, err := OwnerComplexity(tx.DelegatorRewardsOwner) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessValidatorTxComplexities.Add( &baseTxComplexity, - &authComplexity, + &signerComplexity, + &outputsComplexity, + &validatorOwnerComplexity, + &delegatorOwnerComplexity, + ) + return err +} + +func (c *complexityVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + // TODO: Should we include additional complexity for subnets? + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + ownerComplexity, err := OwnerComplexity(tx.DelegationRewardsOwner) + if err != nil { + return err + } + outputsComplexity, err := OutputComplexity(tx.StakeOuts...) + if err != nil { + return err + } + c.output, err = IntrinsicAddPermissionlessDelegatorTxComplexities.Add( + &baseTxComplexity, + &ownerComplexity, + &outputsComplexity, ) return err } @@ -662,6 +653,15 @@ func (c *complexityVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwne return err } +func (c *complexityVisitor) BaseTx(tx *txs.BaseTx) error { + baseTxComplexity, err := baseTxComplexity(tx) + if err != nil { + return err + } + c.output, err = IntrinsicBaseTxComplexities.Add(&baseTxComplexity) + return err +} + func (c *complexityVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) if err != nil { diff --git a/vms/platformvm/txs/fee/static_calculator.go b/vms/platformvm/txs/fee/static_calculator.go index 888ccba8621c..1b97349ce2cb 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -76,21 +76,26 @@ func (c *staticVisitor) CreateSubnetTx(*txs.CreateSubnetTx) error { return nil } -func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (c *staticVisitor) ImportTx(*txs.ImportTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { - c.fee = c.config.TransformSubnetTxFee +func (c *staticVisitor) ExportTx(*txs.ExportTx) error { + c.fee = c.config.TxFee return nil } -func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (c *staticVisitor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { c.fee = c.config.TxFee return nil } +func (c *staticVisitor) TransformSubnetTx(*txs.TransformSubnetTx) error { + c.fee = c.config.TransformSubnetTxFee + return nil +} + func (c *staticVisitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { if tx.Subnet != constants.PrimaryNetworkID { c.fee = c.config.AddSubnetValidatorFee @@ -109,17 +114,12 @@ func (c *staticVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDe return nil } -func (c *staticVisitor) BaseTx(*txs.BaseTx) error { - c.fee = c.config.TxFee - return nil -} - -func (c *staticVisitor) ImportTx(*txs.ImportTx) error { +func (c *staticVisitor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { c.fee = c.config.TxFee return nil } -func (c *staticVisitor) ExportTx(*txs.ExportTx) error { +func (c *staticVisitor) BaseTx(*txs.BaseTx) error { c.fee = c.config.TxFee return nil } diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 142a6115d7af..21c46476fa5f 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -5,6 +5,7 @@ package txs // Allow vm to execute custom logic against the underlying transaction types. type Visitor interface { + // Apricot Transactions: AddValidatorTx(*AddValidatorTx) error AddSubnetValidatorTx(*AddSubnetValidatorTx) error AddDelegatorTx(*AddDelegatorTx) error @@ -14,11 +15,17 @@ type Visitor interface { ExportTx(*ExportTx) error AdvanceTimeTx(*AdvanceTimeTx) error RewardValidatorTx(*RewardValidatorTx) error + + // Banff Transactions: RemoveSubnetValidatorTx(*RemoveSubnetValidatorTx) error TransformSubnetTx(*TransformSubnetTx) error AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error + + // Durango Transactions: TransferSubnetOwnershipTx(*TransferSubnetOwnershipTx) error - ConvertSubnetTx(*ConvertSubnetTx) error BaseTx(*BaseTx) error + + // Etna Transactions: + ConvertSubnetTx(*ConvertSubnetTx) error } diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index 38d501c908b0..b358e1f5d5ea 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -51,14 +51,6 @@ func (*visitor) RewardValidatorTx(*txs.RewardValidatorTx) error { return ErrUnsupportedTxType } -func (s *visitor) BaseTx(tx *txs.BaseTx) error { - txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) - if err != nil { - return err - } - return sign(s.tx, false, txSigners) -} - func (s *visitor) AddValidatorTx(tx *txs.AddValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { @@ -143,7 +135,7 @@ func (s *visitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error return sign(s.tx, true, txSigners) } -func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { +func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -156,20 +148,23 @@ func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) e return sign(s.tx, true, txSigners) } -func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { +func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + return sign(s.tx, true, txSigners) +} + +func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } -func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { +func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err @@ -182,19 +177,24 @@ func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return sign(s.tx, true, txSigners) } -func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { +func (s *visitor) BaseTx(tx *txs.BaseTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, false, txSigners) } -func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { +func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil { return err } + subnetAuthSigners, err := s.getSubnetSigners(tx.Subnet, tx.SubnetAuth) + if err != nil { + return err + } + txSigners = append(txSigners, subnetAuthSigners) return sign(s.tx, true, txSigners) } diff --git a/wallet/chain/p/wallet/backend_visitor.go b/wallet/chain/p/wallet/backend_visitor.go index 8e44ee3b5e2d..f2f9e646edf8 100644 --- a/wallet/chain/p/wallet/backend_visitor.go +++ b/wallet/chain/p/wallet/backend_visitor.go @@ -58,26 +58,6 @@ func (b *backendVisitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { return b.baseTx(&tx.BaseTx) } -func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { - b.b.setSubnetOwner( - tx.Subnet, - tx.Owner, - ) - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { - return b.baseTx(&tx.BaseTx) -} - -func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { - return b.baseTx(tx) -} - func (b *backendVisitor) ImportTx(tx *txs.ImportTx) error { err := b.b.removeUTXOs( b.ctx, @@ -111,6 +91,10 @@ func (b *backendVisitor) ExportTx(tx *txs.ExportTx) error { return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return b.baseTx(&tx.BaseTx) } @@ -123,6 +107,22 @@ func (b *backendVisitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessD return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { + b.b.setSubnetOwner( + tx.Subnet, + tx.Owner, + ) + return b.baseTx(&tx.BaseTx) +} + +func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { + return b.baseTx(tx) +} + +func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) baseTx(tx *txs.BaseTx) error { return b.b.removeUTXOs( b.ctx, From 6b23e82bba4c42661417cf7cdd9e446efc5d966e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:51:01 -0500 Subject: [PATCH 159/184] Cleanup code --- .../txs/executor/standard_tx_executor.go | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index ab60bd03dc7d..3563666b054d 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -834,16 +834,15 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return err } + // Parse the warp message. warpMessage, err := warp.ParseMessage(tx.Message) if err != nil { return err } - addressedCall, err := payload.ParseAddressedCall(warpMessage.Payload) if err != nil { return err } - msg, err := message.ParseRegisterSubnetValidator(addressedCall.Payload) if err != nil { return err @@ -852,6 +851,8 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return err } + // Verify that the warp message was sent from the expected chain and + // address. subnetConversion, err := e.state.GetSubnetConversion(msg.SubnetID) if err != nil { return err @@ -863,6 +864,7 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return fmt.Errorf("expected address %s but got %s", subnetConversion.Addr, addressedCall.SourceAddress) } + // Verify that the message contains a valid expiry time. currentTimestampUnix := uint64(currentTimestamp.Unix()) if msg.Expiry <= currentTimestampUnix { return fmt.Errorf("expected expiry to be after %d but got %d", currentTimestampUnix, msg.Expiry) @@ -871,14 +873,7 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return fmt.Errorf("expected expiry not to be more than %d seconds in the future but got %d", RegisterSubnetValidatorTxExpiryWindow, secondsUntilExpiry) } - pop := signer.ProofOfPossession{ - PublicKey: msg.BLSPublicKey, - ProofOfPossession: tx.ProofOfPossession, - } - if err := pop.Verify(); err != nil { - return err - } - + // Verify that this warp message isn't being replayed. validationID := msg.ValidationID() expiry := state.ExpiryEntry{ Timestamp: msg.Expiry, @@ -892,21 +887,29 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return fmt.Errorf("expiry for %s already exists", validationID) } + // Verify proof of possession provided by the transaction against the public + // key provided by the warp message. + pop := signer.ProofOfPossession{ + PublicKey: msg.BLSPublicKey, + ProofOfPossession: tx.ProofOfPossession, + } + if err := pop.Verify(); err != nil { + return err + } + + // Create the SoV. nodeID, err := ids.ToNodeID(msg.NodeID) if err != nil { return err } - remainingBalanceOwner, err := txs.Codec.Marshal(txs.CodecVersion, &msg.RemainingBalanceOwner) if err != nil { return err } - deactivationOwner, err := txs.Codec.Marshal(txs.CodecVersion, &msg.DisableOwner) if err != nil { return err } - sov := state.SubnetOnlyValidator{ ValidationID: validationID, SubnetID: msg.SubnetID, @@ -919,12 +922,15 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal MinNonce: 0, EndAccumulatedFee: 0, // If Balance is 0, this is 0 } + + // If the balance is non-zero, this validator should be initially active. if tx.Balance != 0 { - // We are attempting to add an active validator + // Verify that there is space for an active validator. if gas.Gas(e.state.NumActiveSubnetOnlyValidators()) >= e.backend.Config.ValidatorFeeConfig.Capacity { return errMaxNumActiveValidators } + // Mark the validator as active. currentFees := e.state.GetAccruedFees() sov.EndAccumulatedFee, err = math.Add(tx.Balance, currentFees) if err != nil { From c8f5157454c17b39ef607835e49c863b321e854e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 15:52:29 -0500 Subject: [PATCH 160/184] nit --- vms/platformvm/txs/executor/standard_tx_executor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 3563666b054d..82a561b3969d 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -729,7 +729,7 @@ func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { StartTime: startTime, Weight: vdr.Weight, MinNonce: 0, - EndAccumulatedFee: 0, // If Balance is 0, this is 0 + EndAccumulatedFee: 0, // If Balance is 0, this is will remain 0 } if vdr.Balance != 0 { // We are attempting to add an active validator From af6171a91d7a3eef64bb6edd14243b25ca4d0de1 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 19:22:56 -0500 Subject: [PATCH 161/184] Add execution tests --- vms/platformvm/signer/proof_of_possession.go | 4 +- .../signer/proof_of_possession_test.go | 2 +- .../txs/executor/standard_tx_executor.go | 27 +- .../txs/executor/standard_tx_executor_test.go | 538 ++++++++++++++++++ vms/platformvm/warp/payload/addressed_call.go | 2 +- vms/platformvm/warp/payload/hash.go | 2 +- vms/platformvm/warp/payload/payload.go | 2 +- vms/platformvm/warp/payload/payload_test.go | 4 +- 8 files changed, 562 insertions(+), 19 deletions(-) diff --git a/vms/platformvm/signer/proof_of_possession.go b/vms/platformvm/signer/proof_of_possession.go index 245d2a96c6f0..f63365d985f4 100644 --- a/vms/platformvm/signer/proof_of_possession.go +++ b/vms/platformvm/signer/proof_of_possession.go @@ -14,7 +14,7 @@ import ( var ( _ Signer = (*ProofOfPossession)(nil) - errInvalidProofOfPossession = errors.New("invalid proof of possession") + ErrInvalidProofOfPossession = errors.New("invalid proof of possession") ) type ProofOfPossession struct { @@ -52,7 +52,7 @@ func (p *ProofOfPossession) Verify() error { return err } if !bls.VerifyProofOfPossession(publicKey, signature, p.PublicKey[:]) { - return errInvalidProofOfPossession + return ErrInvalidProofOfPossession } p.publicKey = publicKey diff --git a/vms/platformvm/signer/proof_of_possession_test.go b/vms/platformvm/signer/proof_of_possession_test.go index 9f4f3feefa3c..9674d63985fc 100644 --- a/vms/platformvm/signer/proof_of_possession_test.go +++ b/vms/platformvm/signer/proof_of_possession_test.go @@ -35,7 +35,7 @@ func TestProofOfPossession(t *testing.T) { require.NoError(err) newBLSPOP.ProofOfPossession = blsPOP.ProofOfPossession err = newBLSPOP.Verify() - require.ErrorIs(err, errInvalidProofOfPossession) + require.ErrorIs(err, ErrInvalidProofOfPossession) } func TestNewProofOfPossessionDeterministic(t *testing.T) { diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 9b066d498ccb..da70c00ba713 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -43,12 +43,17 @@ const ( var ( _ txs.Visitor = (*standardTxExecutor)(nil) - errEmptyNodeID = errors.New("validator nodeID cannot be empty") - errMaxStakeDurationTooLarge = errors.New("max stake duration must be less than or equal to the global max stake duration") - errMissingStartTimePreDurango = errors.New("staker transactions must have a StartTime pre-Durango") - errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") - errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") - errMaxNumActiveValidators = errors.New("already at the max number of active validators") + errEmptyNodeID = errors.New("validator nodeID cannot be empty") + errMaxStakeDurationTooLarge = errors.New("max stake duration must be less than or equal to the global max stake duration") + errMissingStartTimePreDurango = errors.New("staker transactions must have a StartTime pre-Durango") + errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") + errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") + errMaxNumActiveValidators = errors.New("already at the max number of active validators") + errWrongWarpMessageSourceChainID = errors.New("wrong warp message source chain ID") + errWrongWarpMessageSourceAddress = errors.New("wrong warp message source address") + errWarpMessageExpired = errors.New("warp message expired") + errWarpMessageNotYetAllowed = errors.New("warp message not yet allowed") + errWarpMessageAlreadyIssued = errors.New("warp message already issued") ) // StandardTx executes the standard transaction [tx]. @@ -858,19 +863,19 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return err } if warpMessage.SourceChainID != subnetConversion.ChainID { - return fmt.Errorf("expected chainID %s but got %s", subnetConversion.ChainID, warpMessage.SourceChainID) + return fmt.Errorf("%w expected %s but had %s", errWrongWarpMessageSourceChainID, subnetConversion.ChainID, warpMessage.SourceChainID) } if !bytes.Equal(addressedCall.SourceAddress, subnetConversion.Addr) { - return fmt.Errorf("expected address %s but got %s", subnetConversion.Addr, addressedCall.SourceAddress) + return fmt.Errorf("%w expected 0x%x but got 0x%x", errWrongWarpMessageSourceAddress, subnetConversion.Addr, addressedCall.SourceAddress) } // Verify that the message contains a valid expiry time. currentTimestampUnix := uint64(currentTimestamp.Unix()) if msg.Expiry <= currentTimestampUnix { - return fmt.Errorf("expected expiry to be after %d but got %d", currentTimestampUnix, msg.Expiry) + return fmt.Errorf("%w at %d and it is currently %d", errWarpMessageExpired, msg.Expiry, currentTimestampUnix) } if secondsUntilExpiry := msg.Expiry - currentTimestampUnix; secondsUntilExpiry > RegisterSubnetValidatorTxExpiryWindow { - return fmt.Errorf("expected expiry not to be more than %d seconds in the future but got %d", RegisterSubnetValidatorTxExpiryWindow, secondsUntilExpiry) + return fmt.Errorf("%w because time is %d seconds in the future but the limit is %d", errWarpMessageNotYetAllowed, secondsUntilExpiry, RegisterSubnetValidatorTxExpiryWindow) } // Verify that this warp message isn't being replayed. @@ -884,7 +889,7 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal return err } if isDuplicate { - return fmt.Errorf("expiry for %s already exists", validationID) + return fmt.Errorf("%w for validationID %s", errWarpMessageAlreadyIssued, validationID) } // Verify proof of possession provided by the transaction against the public diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 8a2e574ccfc5..d969b6357095 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" @@ -25,6 +26,7 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -40,10 +42,13 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/platformvm/utxo/utxomock" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + safemath "github.com/ava-labs/avalanchego/utils/math" txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) @@ -2670,3 +2675,536 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }) } } + +func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { + var ( + fx = &secp256k1fx.Fx{} + vm = &secp256k1fx.TestVM{ + Log: logging.NoLog{}, + } + ) + require.NoError(t, fx.InitializeVM(vm)) + + var ( + ctx = snowtest.Context(t, constants.PlatformChainID) + defaultConfig = &config.Config{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + baseState = statetest.New(t, statetest.Config{ + Upgrades: defaultConfig.UpgradeConfig, + Context: ctx, + }) + wallet = txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // chainIDs + ) + flowChecker = utxo.NewVerifier( + ctx, + &vm.Clk, + fx, + ) + + backend = &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + } + feeCalculator = state.PickFeeCalculator(defaultConfig, baseState) + ) + + // Create the initial state + diff, err := state.NewDiffOn(baseState) + require.NoError(t, err) + + // Create the subnet + createSubnetTx, err := wallet.IssueCreateSubnetTx( + &secp256k1fx.OutputOwners{}, + ) + require.NoError(t, err) + + // Execute the subnet creation + _, _, _, err = StandardTx( + backend, + feeCalculator, + createSubnetTx, + diff, + ) + require.NoError(t, err) + + // Create the subnet conversion + initialSK, err := bls.NewSecretKey() + require.NoError(t, err) + + const ( + initialWeight = 1 + initialBalance = units.Avax + ) + var ( + subnetID = createSubnetTx.ID() + chainID = ids.GenerateTestID() + address = utils.RandomBytes(32) + initialNodeID = ids.GenerateTestNodeID() + initialPoP = signer.NewProofOfPossession(initialSK) + validator = &txs.ConvertSubnetValidator{ + NodeID: initialNodeID.Bytes(), + Weight: initialWeight, + Balance: initialBalance, + Signer: *initialPoP, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + } + ) + convertSubnetTx, err := wallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + []*txs.ConvertSubnetValidator{ + validator, + }, + ) + require.NoError(t, err) + + // Execute the subnet conversion + _, _, _, err = StandardTx( + backend, + feeCalculator, + convertSubnetTx, + diff, + ) + require.NoError(t, err) + require.NoError(t, diff.Apply(baseState)) + require.NoError(t, baseState.Commit()) + + var ( + nodeID = ids.GenerateTestNodeID() + lastAcceptedTime = baseState.GetTimestamp() + expiryTime = lastAcceptedTime.Add(5 * time.Minute) + expiry = uint64(expiryTime.Unix()) // The warp message will expire in 5 minutes + ) + + const weight = 1 + + // Create the Warp message + sk, err := bls.NewSecretKey() + require.NoError(t, err) + pop := signer.NewProofOfPossession(sk) + pk := bls.PublicFromSecretKey(sk) + pkBytes := bls.PublicKeyToUncompressedBytes(pk) + + remainingBalanceOwner := message.PChainOwner{} + remainingBalanceOwnerBytes, err := txs.Codec.Marshal(txs.CodecVersion, &remainingBalanceOwner) + require.NoError(t, err) + + deactivationOwner := message.PChainOwner{} + deactivationOwnerBytes, err := txs.Codec.Marshal(txs.CodecVersion, &deactivationOwner) + require.NoError(t, err) + + addressedCallPayload := must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + expiry, + remainingBalanceOwner, + deactivationOwner, + weight, + )) + unsignedWarp := must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + addressedCallPayload.Bytes(), + )).Bytes(), + )) + warpSignature := &warp.BitSetSignature{ + Signers: set.NewBits(0).Bytes(), + Signature: ([bls.SignatureLen]byte)(bls.SignatureToBytes( + bls.Sign( + sk, + unsignedWarp.Bytes(), + ), + )), + } + warpMessage := must[*warp.Message](t)(warp.NewMessage( + unsignedWarp, + warpSignature, + )) + + validationID := addressedCallPayload.ValidationID() + tests := []struct { + name string + balance uint64 + message []byte + builderOptions []common.Option + updateTx func(*txs.RegisterSubnetValidatorTx) + updateExecutor func(*standardTxExecutor) error + expectedErr error + }{ + { + name: "invalid prior to E-Upgrade", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ + UpgradeConfig: upgradetest.GetConfig(upgradetest.Durango), + } + return nil + }, + expectedErr: errEtnaUpgradeNotActive, + }, + { + name: "tx fails syntactic verification", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) + return nil + }, + expectedErr: avax.ErrWrongChainID, + }, + { + name: "invalid memo length", + builderOptions: []common.Option{ + common.WithMemo([]byte("memo!")), + }, + expectedErr: avax.ErrMemoTooLarge, + }, + { + name: "invalid fee calculation", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewStaticCalculator(e.backend.Config.StaticFeeConfig) + return nil + }, + expectedErr: txfee.ErrUnsupportedTx, + }, + { + name: "fee calculation overflow", + updateTx: func(tx *txs.RegisterSubnetValidatorTx) { + tx.Balance = math.MaxUint64 + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "insufficient fee", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewDynamicCalculator( + e.backend.Config.DynamicFeeConfig.Weights, + 100*genesis.LocalParams.DynamicFeeConfig.MinPrice, + ) + return nil + }, + expectedErr: utxo.ErrInsufficientUnlockedFunds, + }, + { + name: "invalid warp message", + message: []byte{}, + expectedErr: codec.ErrCantUnpackVersion, + }, + { + name: "invalid warp payload", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.Hash](t)(payload.NewHash(ids.Empty)).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: payload.ErrWrongType, + }, + { + name: "invalid addressed call", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetConversion](t)(message.NewSubnetConversion(ids.Empty)).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: message.ErrWrongType, + }, + { + name: "invalid addressed call payload", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + expiry, + remainingBalanceOwner, + deactivationOwner, + 0, // weight = 0 is invalid + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: message.ErrInvalidWeight, + }, + { + name: "subnet conversion not found", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + ids.GenerateTestID(), // invalid subnetID + nodeID, + pop.PublicKey, + expiry, + remainingBalanceOwner, + deactivationOwner, + weight, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: database.ErrNotFound, + }, + { + name: "invalid source chain", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion(subnetID, state.SubnetConversion{}) + return nil + }, + expectedErr: errWrongWarpMessageSourceChainID, + }, + { + name: "invalid source chain", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion(subnetID, state.SubnetConversion{ + ChainID: chainID, + }) + return nil + }, + expectedErr: errWrongWarpMessageSourceAddress, + }, + { + name: "message expired", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetTimestamp(expiryTime) + return nil + }, + expectedErr: errWarpMessageExpired, + }, + { + name: "message too far in the future", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + math.MaxUint64, // expiry too far in the future + remainingBalanceOwner, + deactivationOwner, + weight, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: errWarpMessageNotYetAllowed, + }, + { + name: "duplicate SoV", + balance: 1, + updateExecutor: func(e *standardTxExecutor) error { + e.state.PutExpiry(state.ExpiryEntry{ + Timestamp: expiry, + ValidationID: validationID, + }) + return nil + }, + expectedErr: errWarpMessageAlreadyIssued, + }, + { + name: "invalid PoP", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + initialPoP.PublicKey, // Wrong public key + expiry, + remainingBalanceOwner, + deactivationOwner, + weight, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: signer.ErrInvalidProofOfPossession, + }, + { + name: "too many active validators", + balance: 1, + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: validatorfee.Config{ + Capacity: 0, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + return nil + }, + expectedErr: errMaxNumActiveValidators, + }, + { + name: "accrued fees overflow", + balance: 1, + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetAccruedFees(math.MaxUint64) + return nil + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "duplicate SoV", + balance: 1, + updateExecutor: func(e *standardTxExecutor) error { + return e.state.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: subnetID, + NodeID: nodeID, + PublicKey: bls.PublicKeyToUncompressedBytes(bls.PublicFromSecretKey(initialSK)), + Weight: 1, + }) + }, + expectedErr: state.ErrDuplicateSubnetOnlyValidator, + }, + { + name: "valid tx", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + // Create the RegisterSubnetValidatorTx + wallet := txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // chainIDs + ) + + message := test.message + if message == nil { + message = warpMessage.Bytes() + } + registerSubnetValidatorTx, err := wallet.IssueRegisterSubnetValidatorTx( + test.balance, + pop.ProofOfPossession, + message, + test.builderOptions..., + ) + require.NoError(err) + + if test.updateTx != nil { + unsignedTx := registerSubnetValidatorTx.Unsigned.(*txs.RegisterSubnetValidatorTx) + test.updateTx(unsignedTx) + } + + diff, err := state.NewDiffOn(baseState) + require.NoError(err) + + executor := &standardTxExecutor{ + backend: &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + }, + feeCalculator: state.PickFeeCalculator(defaultConfig, baseState), + tx: registerSubnetValidatorTx, + state: diff, + } + if test.updateExecutor != nil { + require.NoError(test.updateExecutor(executor)) + } + + err = registerSubnetValidatorTx.Unsigned.Visit(executor) + require.ErrorIs(err, test.expectedErr) + if err != nil { + return + } + + for utxoID := range registerSubnetValidatorTx.InputIDs() { + _, err := diff.GetUTXO(utxoID) + require.ErrorIs(err, database.ErrNotFound) + } + + for _, expectedUTXO := range registerSubnetValidatorTx.UTXOs() { + utxoID := expectedUTXO.InputID() + utxo, err := diff.GetUTXO(utxoID) + require.NoError(err) + require.Equal(expectedUTXO, utxo) + } + + sov, err := diff.GetSubnetOnlyValidator(validationID) + require.NoError(err) + + var expectedEndAccumulatedFee uint64 + if test.balance != 0 { + expectedEndAccumulatedFee = test.balance + diff.GetAccruedFees() + } + require.Equal( + state.SubnetOnlyValidator{ + ValidationID: validationID, + SubnetID: subnetID, + NodeID: nodeID, + PublicKey: pkBytes, + RemainingBalanceOwner: remainingBalanceOwnerBytes, + DeactivationOwner: deactivationOwnerBytes, + StartTime: uint64(diff.GetTimestamp().Unix()), + Weight: weight, + MinNonce: 0, + EndAccumulatedFee: expectedEndAccumulatedFee, + }, + sov, + ) + }) + } +} + +func must[T any](t require.TestingT) func(T, error) T { + return func(val T, err error) T { + require.NoError(t, err) + return val + } +} diff --git a/vms/platformvm/warp/payload/addressed_call.go b/vms/platformvm/warp/payload/addressed_call.go index b3617ce487da..9950be42ec0d 100644 --- a/vms/platformvm/warp/payload/addressed_call.go +++ b/vms/platformvm/warp/payload/addressed_call.go @@ -37,7 +37,7 @@ func ParseAddressedCall(b []byte) (*AddressedCall, error) { } payload, ok := payloadIntf.(*AddressedCall) if !ok { - return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf) + return nil, fmt.Errorf("%w: %T", ErrWrongType, payloadIntf) } return payload, nil } diff --git a/vms/platformvm/warp/payload/hash.go b/vms/platformvm/warp/payload/hash.go index 330f74fd869d..73804d169bdf 100644 --- a/vms/platformvm/warp/payload/hash.go +++ b/vms/platformvm/warp/payload/hash.go @@ -33,7 +33,7 @@ func ParseHash(b []byte) (*Hash, error) { } payload, ok := payloadIntf.(*Hash) if !ok { - return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf) + return nil, fmt.Errorf("%w: %T", ErrWrongType, payloadIntf) } return payload, nil } diff --git a/vms/platformvm/warp/payload/payload.go b/vms/platformvm/warp/payload/payload.go index c5c09464803e..0f7831c6b343 100644 --- a/vms/platformvm/warp/payload/payload.go +++ b/vms/platformvm/warp/payload/payload.go @@ -8,7 +8,7 @@ import ( "fmt" ) -var errWrongType = errors.New("wrong payload type") +var ErrWrongType = errors.New("wrong payload type") // Payload provides a common interface for all payloads implemented by this // package. diff --git a/vms/platformvm/warp/payload/payload_test.go b/vms/platformvm/warp/payload/payload_test.go index 86b584ae33db..6ae1fcc09c3c 100644 --- a/vms/platformvm/warp/payload/payload_test.go +++ b/vms/platformvm/warp/payload/payload_test.go @@ -33,10 +33,10 @@ func TestParseWrongPayloadType(t *testing.T) { require.NoError(err) _, err = ParseAddressedCall(hashPayload.Bytes()) - require.ErrorIs(err, errWrongType) + require.ErrorIs(err, ErrWrongType) _, err = ParseHash(addressedPayload.Bytes()) - require.ErrorIs(err, errWrongType) + require.ErrorIs(err, ErrWrongType) } func TestParse(t *testing.T) { From e3f6ab3730e5dc45b1afd26a9426183a1e0bf1b6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 19:30:03 -0500 Subject: [PATCH 162/184] nit --- vms/platformvm/txs/executor/standard_tx_executor.go | 3 ++- vms/platformvm/txs/executor/standard_tx_executor_test.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index da70c00ba713..b64972452eba 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -49,6 +49,7 @@ var ( errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") errMaxNumActiveValidators = errors.New("already at the max number of active validators") + errCouldNotLoadSubnetConversion = errors.New("could not load subnet conversion") errWrongWarpMessageSourceChainID = errors.New("wrong warp message source chain ID") errWrongWarpMessageSourceAddress = errors.New("wrong warp message source address") errWarpMessageExpired = errors.New("warp message expired") @@ -860,7 +861,7 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal // address. subnetConversion, err := e.state.GetSubnetConversion(msg.SubnetID) if err != nil { - return err + return fmt.Errorf("%w for %s with: %w", errCouldNotLoadSubnetConversion, msg.SubnetID, err) } if warpMessage.SourceChainID != subnetConversion.ChainID { return fmt.Errorf("%w expected %s but had %s", errWrongWarpMessageSourceChainID, subnetConversion.ChainID, warpMessage.SourceChainID) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index d969b6357095..4ba046722c4d 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -2976,7 +2976,7 @@ func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { )), warpSignature, )).Bytes(), - expectedErr: database.ErrNotFound, + expectedErr: errCouldNotLoadSubnetConversion, }, { name: "invalid source chain", From fc9440cabaa2bb3c3fcf413f89aa839a419debba Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 19:32:40 -0500 Subject: [PATCH 163/184] nit --- vms/platformvm/txs/executor/standard_tx_executor_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 4ba046722c4d..59aa8d45445d 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -3198,6 +3198,13 @@ func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { }, sov, ) + + hasExpiry, err := diff.HasExpiry(state.ExpiryEntry{ + Timestamp: expiry, + ValidationID: validationID, + }) + require.NoError(err) + require.True(hasExpiry) }) } } From f41cff6ca46f9fcb2a7e1f2d301f051d519dc238 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 19:42:46 -0500 Subject: [PATCH 164/184] remove usage of defer --- tests/e2e/p/l1.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 6ee961450a34..5543218914d6 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -204,11 +204,6 @@ var _ = e2e.DescribePChain("[L1]", func() { }), ) require.NoError(err) - defer func() { - genesisPeerMessages.Close() - genesisPeer.StartClose() - require.NoError(genesisPeer.AwaitClosed(tc.DefaultContext())) - }() address := []byte{} tc.By("issuing a ConvertSubnetTx", func() { @@ -372,6 +367,10 @@ var _ = e2e.DescribePChain("[L1]", func() { }) }) + genesisPeerMessages.Close() + genesisPeer.StartClose() + require.NoError(genesisPeer.AwaitClosed(tc.DefaultContext())) + _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) }) }) From 38f63f9ad0e020850347eb5e97c8b25a857f824b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 20:24:34 -0500 Subject: [PATCH 165/184] nit --- tests/e2e/p/l1.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 098a2875c2de..98ecfc634abf 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -330,15 +330,13 @@ var _ = e2e.DescribePChain("[L1]", func() { require.True(ok) tc.By("creating the signed warp message to register the validator") - signers := set.NewBits(0) // [signers] has weight from the genesis peer - - var sigBytes [bls.SignatureLen]byte - copy(sigBytes[:], bls.SignatureToBytes(registerSubnetValidatorSignature)) registerSubnetValidator, err := warp.NewMessage( unsignedRegisterSubnetValidator, &warp.BitSetSignature{ - Signers: signers.Bytes(), - Signature: sigBytes, + Signers: set.NewBits(0).Bytes(), // [signers] has weight from the genesis peer + Signature: ([bls.SignatureLen]byte)( + bls.SignatureToBytes(registerSubnetValidatorSignature), + ), }, ) require.NoError(err) @@ -401,8 +399,7 @@ var _ = e2e.DescribePChain("[L1]", func() { require.True(ok) tc.By("creating the signed warp message to increase the weight of the validator") - signers := set.NewBits() - signers.Add(0) // [signers] has weight from the genesis peer + signers := set.NewBits(0) // [signers] has weight from the genesis peer var sigBytes [bls.SignatureLen]byte copy(sigBytes[:], bls.SignatureToBytes(setSubnetValidatorWeightSignature)) From aa4e85907db531288556d7e8aa918d06d47333a9 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 20:25:48 -0500 Subject: [PATCH 166/184] nit --- tests/e2e/p/l1.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 98ecfc634abf..99c94874eebd 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -399,15 +399,13 @@ var _ = e2e.DescribePChain("[L1]", func() { require.True(ok) tc.By("creating the signed warp message to increase the weight of the validator") - signers := set.NewBits(0) // [signers] has weight from the genesis peer - - var sigBytes [bls.SignatureLen]byte - copy(sigBytes[:], bls.SignatureToBytes(setSubnetValidatorWeightSignature)) registerSubnetValidator, err := warp.NewMessage( unsignedSubnetValidatorWeight, &warp.BitSetSignature{ - Signers: signers.Bytes(), - Signature: sigBytes, + Signers: set.NewBits(0).Bytes(), // [signers] has weight from the genesis peer + Signature: ([bls.SignatureLen]byte)( + bls.SignatureToBytes(setSubnetValidatorWeightSignature), + ), }, ) require.NoError(err) From a35c877873900a37876b0e9f003e67457f44471c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 20:27:08 -0500 Subject: [PATCH 167/184] nit --- tests/e2e/p/l1.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 5543218914d6..8a102b4e080a 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -328,18 +328,15 @@ var _ = e2e.DescribePChain("[L1]", func() { require.True(ok) tc.By("creating the signed warp message to register the validator") - signers := set.NewBits(0) // [signers] has weight from the genesis peer - - var sigBytes [bls.SignatureLen]byte - copy(sigBytes[:], bls.SignatureToBytes(registerSubnetValidatorSignature)) registerSubnetValidator, err := warp.NewMessage( unsignedRegisterSubnetValidator, &warp.BitSetSignature{ - Signers: signers.Bytes(), - Signature: sigBytes, + Signers: set.NewBits(0).Bytes(), // [signers] has weight from the genesis peer + Signature: ([bls.SignatureLen]byte)( + bls.SignatureToBytes(registerSubnetValidatorSignature), + ), }, ) - require.NoError(err) tc.By("issuing a RegisterSubnetValidatorTx", func() { _, err := pWallet.IssueRegisterSubnetValidatorTx( From 28b478b63c86ea973b39562de63c7ee8fbe8c4ae Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 20:28:14 -0500 Subject: [PATCH 168/184] oops --- tests/e2e/p/l1.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 8a102b4e080a..73c1e08eed1e 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -337,6 +337,7 @@ var _ = e2e.DescribePChain("[L1]", func() { ), }, ) + require.NoError(err) tc.By("issuing a RegisterSubnetValidatorTx", func() { _, err := pWallet.IssueRegisterSubnetValidatorTx( From 3b1d5ca1c480717970330aee702f9e049d3d929b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 20:29:00 -0500 Subject: [PATCH 169/184] nit --- tests/e2e/p/l1.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index 99c94874eebd..ce3fefd8bc2d 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -399,7 +399,7 @@ var _ = e2e.DescribePChain("[L1]", func() { require.True(ok) tc.By("creating the signed warp message to increase the weight of the validator") - registerSubnetValidator, err := warp.NewMessage( + setSubnetValidatorWeight, err := warp.NewMessage( unsignedSubnetValidatorWeight, &warp.BitSetSignature{ Signers: set.NewBits(0).Bytes(), // [signers] has weight from the genesis peer @@ -412,7 +412,7 @@ var _ = e2e.DescribePChain("[L1]", func() { tc.By("issuing a SetSubnetValidatorWeightTx", func() { _, err := pWallet.IssueSetSubnetValidatorWeightTx( - registerSubnetValidator.Bytes(), + setSubnetValidatorWeight.Bytes(), ) require.NoError(err) }) From 95716935354405609fe48cc7d340f7b9a7cc9bc3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 20:37:44 -0500 Subject: [PATCH 170/184] nit --- tests/e2e/p/l1.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index ce3fefd8bc2d..96156ddff36b 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -369,7 +369,7 @@ var _ = e2e.DescribePChain("[L1]", func() { var nextNonce uint64 setWeight := func(validationID ids.ID, weight uint64) { - tc.By("creating the unsigned warp message") + tc.By("creating the unsigned SubnetValidatorWeightMessage") unsignedSubnetValidatorWeight := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( networkID, chainID, From 30ac509ac3d4643aedbe621e0746adc7640f327e Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Thu, 7 Nov 2024 22:21:11 -0500 Subject: [PATCH 171/184] Address PR comments --- vms/platformvm/txs/executor/standard_tx_executor_test.go | 4 ++-- vms/platformvm/txs/fee/complexity.go | 2 ++ vms/platformvm/validators/manager.go | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 59aa8d45445d..ba4f5853b7b6 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -2987,7 +2987,7 @@ func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { expectedErr: errWrongWarpMessageSourceChainID, }, { - name: "invalid source chain", + name: "invalid source address", updateExecutor: func(e *standardTxExecutor) error { e.state.SetSubnetConversion(subnetID, state.SubnetConversion{ ChainID: chainID, @@ -3005,7 +3005,7 @@ func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { expectedErr: errWarpMessageExpired, }, { - name: "message too far in the future", + name: "message expiry too far in the future", message: must[*warp.Message](t)(warp.NewMessage( must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( ctx.NetworkID, diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index 66465ba0bb67..5083296d2936 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -1,6 +1,8 @@ // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. +// TODO: Before Etna, address all TODOs in this package and ensure ACP-103 +// compliance. package fee import ( diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index 6e620bdc7c70..be37e6ee73e2 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -33,9 +33,8 @@ const ( // recommended minimum height will lag behind the last accepted block. MinRecentlyAcceptedWindowSize = 0 // RecentlyAcceptedWindowTTL is the amount of time after a block is accepted - // to avoid recommending it as the minimum height. If the evicting, or not - // evicting, the block would violate either the maxiumum or minimum window - // size, the block will not be evicted. + // to avoid recommending it as the minimum height. The size constraints take + // precedence over this time constraint. RecentlyAcceptedWindowTTL = 30 * time.Second validatorSetsCacheSize = 64 From f16cc58058e02d25c772dbd5870c76c2595f8bdc Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 09:53:48 -0500 Subject: [PATCH 172/184] Address PR comments --- .../txs/executor/standard_tx_executor.go | 56 +++++++++++-------- wallet/chain/p/builder/builder.go | 2 +- .../set-subnet-validator-weight/main.go | 36 +++++------- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 9e7632007c42..dda138e9a36f 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -862,15 +862,8 @@ func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetVal // Verify that the warp message was sent from the expected chain and // address. - subnetConversion, err := e.state.GetSubnetConversion(msg.SubnetID) - if err != nil { - return fmt.Errorf("%w for %s with: %w", errCouldNotLoadSubnetConversion, msg.SubnetID, err) - } - if warpMessage.SourceChainID != subnetConversion.ChainID { - return fmt.Errorf("%w expected %s but had %s", errWrongWarpMessageSourceChainID, subnetConversion.ChainID, warpMessage.SourceChainID) - } - if !bytes.Equal(addressedCall.SourceAddress, subnetConversion.Addr) { - return fmt.Errorf("%w expected 0x%x but got 0x%x", errWrongWarpMessageSourceAddress, subnetConversion.Addr, addressedCall.SourceAddress) + if err := verifyL1Conversion(e.state, msg.SubnetID, warpMessage.SourceChainID, addressedCall.SourceAddress); err != nil { + return err } // Verify that the message contains a valid expiry time. @@ -1022,16 +1015,11 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return fmt.Errorf("expected nonce to be at least %d but got %d", sov.MinNonce, msg.Nonce) } - subnetConversion, err := e.state.GetSubnetConversion(sov.SubnetID) - if err != nil { + // Verify that the warp message was sent from the expected chain and + // address. + if err := verifyL1Conversion(e.state, sov.SubnetID, warpMessage.SourceChainID, addressedCall.SourceAddress); err != nil { return err } - if warpMessage.SourceChainID != subnetConversion.ChainID { - return fmt.Errorf("expected chainID %s but got %s", subnetConversion.ChainID, warpMessage.SourceChainID) - } - if !bytes.Equal(addressedCall.SourceAddress, subnetConversion.Addr) { - return fmt.Errorf("expected address %s but got %s", subnetConversion.Addr, addressedCall.SourceAddress) - } txID := e.tx.ID() @@ -1055,9 +1043,9 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat accruedFees := e.state.GetAccruedFees() if sov.EndAccumulatedFee <= accruedFees { - // This check should be unreachable. However, including it ensures - // that AVAX can't get minted out of thin air due to state - // corruption. + // This check should be unreachable. However, it prevents AVAX + // from being minted due to state corruption. This also prevents + // invalid UTXOs from being created (with 0 value). return fmt.Errorf("%w: validator should have already been disabled", errStateCorruption) } remainingBalance := sov.EndAccumulatedFee - accruedFees @@ -1082,8 +1070,11 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat } } - // If the weight is being set to 0, the validator is being removed and the - // nonce doesn't matter. + // If the weight is being set to 0, it is possible for the nonce increment + // to overflow. However, the validator is being removed and the nonce + // doesn't matter. If weight is not 0, [msg.Nonce] is enforced by + // [msg.Verify()] to be less than MaxUInt64 and can therefore be incremented + // without overflow. sov.MinNonce = msg.Nonce + 1 sov.Weight = msg.Weight if err := e.state.PutSubnetOnlyValidator(sov); err != nil { @@ -1168,3 +1159,24 @@ func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { } return nil } + +// verifyL1Conversion verifies that the L1 conversion of [subnetID] references +// the [expectedChainID] and [expectedAddress]. +func verifyL1Conversion( + state state.Chain, + subnetID ids.ID, + expectedChainID ids.ID, + expectedAddress []byte, +) error { + subnetConversion, err := state.GetSubnetConversion(subnetID) + if err != nil { + return fmt.Errorf("%w for %s with: %w", errCouldNotLoadSubnetConversion, subnetID, err) + } + if expectedChainID != subnetConversion.ChainID { + return fmt.Errorf("%w expected %s but had %s", errWrongWarpMessageSourceChainID, subnetConversion.ChainID, expectedChainID) + } + if !bytes.Equal(expectedAddress, subnetConversion.Addr) { + return fmt.Errorf("%w expected 0x%x but got 0x%x", errWrongWarpMessageSourceAddress, subnetConversion.Addr, expectedAddress) + } + return nil +} diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index b8f0227f0969..9bb1364d9fe7 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -962,7 +962,7 @@ func (b *builder) NewSetSubnetValidatorWeightTx( if err != nil { return nil, err } - complexity, err := fee.IntrinsicRegisterSubnetValidatorTxComplexities.Add( + complexity, err := fee.IntrinsicSetSubnetValidatorWeightTxComplexities.Add( &memoComplexity, &warpComplexity, ) diff --git a/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go b/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go index 35e7ce264104..a796ae9b51e7 100644 --- a/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go +++ b/wallet/subnet/primary/examples/set-subnet-validator-weight/main.go @@ -8,7 +8,6 @@ import ( "encoding/hex" "encoding/json" "log" - "os" "time" "github.com/ava-labs/avalanchego/genesis" @@ -27,28 +26,24 @@ func main() { uri := primary.LocalAPIURI kc := secp256k1fx.NewKeychain(key) chainID := ids.FromStringOrPanic("2BMFrJ9xeh5JdwZEx6uuFcjfZC2SV2hdbMT8ee5HrvjtfJb5br") - addressHex := "" + address := []byte{} validationID := ids.FromStringOrPanic("2Y3ZZZXxpzm46geqVuqFXeSFVbeKihgrfeXRDaiF4ds6R2N8M5") nonce := uint64(1) weight := uint64(2) + blsSKHex := "3f783929b295f16cd1172396acb23b20eed057b9afb1caa419e9915f92860b35" - address, err := hex.DecodeString(addressHex) + blsSKBytes, err := hex.DecodeString(blsSKHex) if err != nil { - log.Fatalf("failed to decode address %q: %s\n", addressHex, err) + log.Fatalf("failed to decode secret key: %s\n", err) } - skBytes, err := os.ReadFile("/Users/stephen/.avalanchego/staking/signer.key") - if err != nil { - log.Fatalf("failed to read signer key: %s\n", err) - } - - sk, err := bls.SecretKeyFromBytes(skBytes) + sk, err := bls.SecretKeyFromBytes(blsSKBytes) if err != nil { log.Fatalf("failed to parse secret key: %s\n", err) } // MakeWallet fetches the available UTXOs owned by [kc] on the network that - // [uri] is hosting and registers [subnetID]. + // [uri] is hosting. walletSyncStartTime := time.Now() ctx := context.Background() wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{ @@ -96,19 +91,18 @@ func main() { log.Fatalf("failed to create unsigned Warp message: %s\n", err) } - signers := set.NewBits() - signers.Add(0) // [signers] has weight from [vdr[0]] - - unsignedBytes := unsignedWarp.Bytes() - sig := bls.Sign(sk, unsignedBytes) - sigBytes := [bls.SignatureLen]byte{} - copy(sigBytes[:], bls.SignatureToBytes(sig)) - warp, err := warp.NewMessage( unsignedWarp, &warp.BitSetSignature{ - Signers: signers.Bytes(), - Signature: sigBytes, + Signers: set.NewBits(0).Bytes(), + Signature: ([bls.SignatureLen]byte)( + bls.SignatureToBytes( + bls.Sign( + sk, + unsignedWarp.Bytes(), + ), + ), + ), }, ) if err != nil { From 2947902c81bec399f7c08e2d60af41f213d16e7a Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 09:59:50 -0500 Subject: [PATCH 173/184] comment nits --- vms/platformvm/txs/executor/standard_tx_executor.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index dda138e9a36f..222a5458d06a 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -991,6 +991,7 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return err } + // Parse the warp message. warpMessage, err := warp.ParseMessage(tx.Message) if err != nil { return err @@ -1007,6 +1008,7 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return err } + // Verify that the message contains a valid nonce for a current validator. sov, err := e.state.GetSubnetOnlyValidator(msg.ValidationID) if err != nil { return err @@ -1023,8 +1025,9 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat txID := e.tx.ID() - // We are removing the validator + // Check if we are removing the validator. if msg.Weight == 0 { + // Verify that we are not removing the last validator. weight, err := e.state.WeightOfSubnetOnlyValidators(sov.SubnetID) if err != nil { return err @@ -1033,7 +1036,7 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return errRemovingLastValidator } - // The validator is currently active, we need to refund the remaining + // If the validator is currently active, we need to refund the remaining // balance. if sov.EndAccumulatedFee != 0 { var remainingBalanceOwner message.PChainOwner From 6357f5a22a0ccf4035e98a5f184594bef87dde6b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 10:09:08 -0500 Subject: [PATCH 174/184] improve error reporting --- vms/platformvm/txs/executor/standard_tx_executor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 222a5458d06a..06d234353603 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -56,6 +56,7 @@ var ( errWarpMessageExpired = errors.New("warp message expired") errWarpMessageNotYetAllowed = errors.New("warp message not yet allowed") errWarpMessageAlreadyIssued = errors.New("warp message already issued") + errWarpMessageContainsStaleNonce = errors.New("warp message contains stale nonce") errRemovingLastValidator = errors.New("attempting to remove the last SoV from a converted subnet") errStateCorruption = errors.New("state corruption") ) @@ -1014,7 +1015,7 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat return err } if msg.Nonce < sov.MinNonce { - return fmt.Errorf("expected nonce to be at least %d but got %d", sov.MinNonce, msg.Nonce) + return fmt.Errorf("%w %d must be at least %d", errWarpMessageContainsStaleNonce, msg.Nonce, sov.MinNonce) } // Verify that the warp message was sent from the expected chain and From eaa7ea3f788adfcd2f77d9b131cb6773b2464e5b Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 10:17:32 -0500 Subject: [PATCH 175/184] nit --- wallet/chain/p/builder/builder.go | 3 +-- wallet/chain/p/wallet/wallet.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index 9bb1364d9fe7..8ca24ba8f7d9 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -179,8 +179,7 @@ type Builder interface { options ...common.Option, ) (*txs.RegisterSubnetValidatorTx, error) - // NewSetSubnetValidatorWeightTx sets the weight of a validator on a - // Permissionless L1. + // NewSetSubnetValidatorWeightTx sets the weight of a validator on an L1. // // - [message] is the Warp message that authorizes this validator's weight // to be changed diff --git a/wallet/chain/p/wallet/wallet.go b/wallet/chain/p/wallet/wallet.go index 72842f24a426..26ad5a4b5a97 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -167,7 +167,7 @@ type Wallet interface { ) (*txs.Tx, error) // IssueSetSubnetValidatorWeightTx creates, signs, and issues a transaction - // that sets the weight of a validator on a Permissionless L1. + // that sets the weight of a validator on an L1. // // - [message] is the Warp message that authorizes this validator's weight // to be changed From e2451ab3b347c6f11eae3f9d76a58b4c05c8eac5 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 10:25:30 -0500 Subject: [PATCH 176/184] Add SetSubnetValidatorWeightTx serialization and SyntacticVerify tests --- .../set_subnet_validator_weight_tx_test.go | 365 ++++++++++++++++++ .../set_subnet_validator_weight_tx_test.json | 76 ++++ 2 files changed, 441 insertions(+) create mode 100644 vms/platformvm/txs/set_subnet_validator_weight_tx_test.go create mode 100644 vms/platformvm/txs/set_subnet_validator_weight_tx_test.json diff --git a/vms/platformvm/txs/set_subnet_validator_weight_tx_test.go b/vms/platformvm/txs/set_subnet_validator_weight_tx_test.go new file mode 100644 index 000000000000..13dd4afc7766 --- /dev/null +++ b/vms/platformvm/txs/set_subnet_validator_weight_tx_test.go @@ -0,0 +1,365 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + _ "embed" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" +) + +//go:embed set_subnet_validator_weight_tx_test.json +var setSubnetValidatorWeightTxJSON []byte + +func TestSetSubnetValidatorWeightTxSerialization(t *testing.T) { + require := require.New(t) + + var ( + message = []byte("message") + addr = ids.ShortID{ + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + } + avaxAssetID = ids.ID{ + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + } + customAssetID = ids.ID{ + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + } + txID = ids.ID{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + } + ) + + var unsignedTx UnsignedTx = &SetSubnetValidatorWeightTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.UnitTestID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 87654321, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 12345678, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + }, + { + Asset: avax.Asset{ + ID: customAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 876543210, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 0xffffffffffffffff, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + }, + }, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.Avax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{2, 5}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 2, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &stakeable.LockIn{ + Locktime: 876543210, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 0xefffffffffffffff, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 3, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 0x1000000000000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }, + }, + Memo: types.JSONByteSlice("😅\nwell that's\x01\x23\x45!"), + }, + }, + Message: message, + } + txBytes, err := Codec.Marshal(CodecVersion, &unsignedTx) + require.NoError(err) + + expectedBytes := []byte{ + // Codec version + 0x00, 0x00, + // SetSubnetValidatorWeightTx Type ID + 0x00, 0x00, 0x00, 0x25, + // Network ID + 0x00, 0x00, 0x00, 0x0a, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x02, + // Outputs[0] + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x05, 0x39, 0x7f, 0xb1, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x61, 0x4e, + // threshold + 0x00, 0x00, 0x00, 0x00, + // number of addresses + 0x00, 0x00, 0x00, 0x00, + // Outputs[1] + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // threshold + 0x00, 0x00, 0x00, 0x01, + // number of addresses + 0x00, 0x00, 0x00, 0x01, + // address[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + // number of inputs + 0x00, 0x00, 0x00, 0x03, + // inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 Avax + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x02, + // index of first signer + 0x00, 0x00, 0x00, 0x02, + // index of second signer + 0x00, 0x00, 0x00, 0x05, + // inputs[1] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x02, + // Custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked input type ID + 0x00, 0x00, 0x00, 0x15, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x00, + // inputs[2] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x03, + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x00, + // length of memo + 0x00, 0x00, 0x00, 0x14, + // memo + 0xf0, 0x9f, 0x98, 0x85, 0x0a, 0x77, 0x65, 0x6c, + 0x6c, 0x20, 0x74, 0x68, 0x61, 0x74, 0x27, 0x73, + 0x01, 0x23, 0x45, 0x21, + // length of message + 0x00, 0x00, 0x00, 0x07, + // message + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + } + require.Equal(expectedBytes, txBytes) + + ctx := snowtest.Context(t, constants.PlatformChainID) + unsignedTx.InitCtx(ctx) + + txJSON, err := json.MarshalIndent(unsignedTx, "", "\t") + require.NoError(err) + require.Equal( + // Normalize newlines for Windows + strings.ReplaceAll(string(setSubnetValidatorWeightTxJSON), "\r\n", "\n"), + string(txJSON), + ) +} + +func TestSetSubnetValidatorWeightTxSyntacticVerify(t *testing.T) { + ctx := snowtest.Context(t, ids.GenerateTestID()) + tests := []struct { + name string + tx *SetSubnetValidatorWeightTx + expectedErr error + }{ + { + name: "nil tx", + tx: nil, + expectedErr: ErrNilTx, + }, + { + name: "already verified", + // The tx includes invalid data to verify that a cached result is + // returned. + tx: &SetSubnetValidatorWeightTx{ + BaseTx: BaseTx{ + SyntacticallyVerified: true, + }, + }, + expectedErr: nil, + }, + { + name: "invalid BaseTx", + tx: &SetSubnetValidatorWeightTx{ + BaseTx: BaseTx{}, + }, + expectedErr: avax.ErrWrongNetworkID, + }, + { + name: "passes verification", + tx: &SetSubnetValidatorWeightTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }, + }, + }, + expectedErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + err := test.tx.SyntacticVerify(ctx) + require.ErrorIs(err, test.expectedErr) + if test.expectedErr != nil { + return + } + require.True(test.tx.SyntacticallyVerified) + }) + } +} diff --git a/vms/platformvm/txs/set_subnet_validator_weight_tx_test.json b/vms/platformvm/txs/set_subnet_validator_weight_tx_test.json new file mode 100644 index 000000000000..7f81afa155ac --- /dev/null +++ b/vms/platformvm/txs/set_subnet_validator_weight_tx_test.json @@ -0,0 +1,76 @@ +{ + "networkID": 10, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 87654321, + "output": { + "addresses": [], + "amount": 1, + "locktime": 12345678, + "threshold": 0 + } + } + }, + { + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 876543210, + "output": { + "addresses": [ + "P-testing1g32kvaugnx4tk3z4vemc3xd2hdz92enhgrdu9n" + ], + "amount": 18446744073709551615, + "locktime": 0, + "threshold": 1 + } + } + } + ], + "inputs": [ + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1000000000, + "signatureIndices": [ + 2, + 5 + ] + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 2, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 876543210, + "input": { + "amount": 17293822569102704639, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 3, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1152921504606846976, + "signatureIndices": [] + } + } + ], + "memo": "0xf09f98850a77656c6c2074686174277301234521", + "message": "0x6d657373616765" +} \ No newline at end of file From 6d04b1fba7804a4e415f27f12fb805b4e319457c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 10:32:05 -0500 Subject: [PATCH 177/184] Add SetSubnetValidatorWeightTx builder tests --- wallet/chain/p/builder_test.go | 82 ++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/wallet/chain/p/builder_test.go b/wallet/chain/p/builder_test.go index bc03f30814e9..9a3cb1001401 100644 --- a/wallet/chain/p/builder_test.go +++ b/wallet/chain/p/builder_test.go @@ -849,6 +849,88 @@ func TestRegisterSubnetValidatorTx(t *testing.T) { } } +func TestSetSubnetValidatorWeightTx(t *testing.T) { + const ( + nonce = 1 + weight = 7905001371 + ) + var ( + validationID = ids.GenerateTestID() + chainID = ids.GenerateTestID() + address = utils.RandomBytes(20) + ) + + addressedCallPayload, err := message.NewSubnetValidatorWeight( + validationID, + nonce, + weight, + ) + require.NoError(t, err) + + addressedCall, err := payload.NewAddressedCall( + address, + addressedCallPayload.Bytes(), + ) + require.NoError(t, err) + + unsignedWarp, err := warp.NewUnsignedMessage( + constants.UnitTestID, + chainID, + addressedCall.Bytes(), + ) + require.NoError(t, err) + + sk, err := bls.NewSecretKey() + require.NoError(t, err) + + warp, err := warp.NewMessage( + unsignedWarp, + &warp.BitSetSignature{ + Signers: set.NewBits(0).Bytes(), + Signature: ([bls.SignatureLen]byte)( + bls.SignatureToBytes( + bls.Sign( + sk, + unsignedWarp.Bytes(), + ), + ), + ), + }, + ) + require.NoError(t, err) + + warpMessageBytes := warp.Bytes() + for _, e := range testEnvironmentPostEtna { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = wallet.NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr), e.context, backend) + ) + + utx, err := builder.NewSetSubnetValidatorWeightTx( + warpMessageBytes, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(types.JSONByteSlice(warpMessageBytes), utx.Message) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + nil, + ) + }) + } +} + func makeTestUTXOs(utxosKey *secp256k1.PrivateKey) []*avax.UTXO { // Note: we avoid ids.GenerateTestNodeID here to make sure that UTXO IDs // won't change run by run. This simplifies checking what utxos are included From b71faf71d97a70ce4ccd69905338c11f273726c3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 10:44:06 -0500 Subject: [PATCH 178/184] execution test nits --- vms/platformvm/txs/executor/standard_tx_executor_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index ba4f5853b7b6..c392e2dfe030 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -3028,7 +3028,7 @@ func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { expectedErr: errWarpMessageNotYetAllowed, }, { - name: "duplicate SoV", + name: "SoV previously registered", balance: 1, updateExecutor: func(e *standardTxExecutor) error { e.state.PutExpiry(state.ExpiryEntry{ @@ -3090,8 +3090,7 @@ func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { expectedErr: safemath.ErrOverflow, }, { - name: "duplicate SoV", - balance: 1, + name: "duplicate subnetID + nodeID pair", updateExecutor: func(e *standardTxExecutor) error { return e.state.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ ValidationID: ids.GenerateTestID(), From fde96e7df4d9c7c6213043a246cc3ed3ac3e3c2d Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 13:02:52 -0500 Subject: [PATCH 179/184] Add SetSubnetValidatorWeightTx execution tests --- .../txs/executor/standard_tx_executor.go | 7 +- .../txs/executor/standard_tx_executor_test.go | 475 ++++++++++++++++++ 2 files changed, 479 insertions(+), 3 deletions(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 06d234353603..8be659f483c8 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -56,6 +56,7 @@ var ( errWarpMessageExpired = errors.New("warp message expired") errWarpMessageNotYetAllowed = errors.New("warp message not yet allowed") errWarpMessageAlreadyIssued = errors.New("warp message already issued") + errCouldNotLoadSoV = errors.New("could not load SoV") errWarpMessageContainsStaleNonce = errors.New("warp message contains stale nonce") errRemovingLastValidator = errors.New("attempting to remove the last SoV from a converted subnet") errStateCorruption = errors.New("state corruption") @@ -1012,7 +1013,7 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat // Verify that the message contains a valid nonce for a current validator. sov, err := e.state.GetSubnetOnlyValidator(msg.ValidationID) if err != nil { - return err + return fmt.Errorf("%w: %w", errCouldNotLoadSoV, err) } if msg.Nonce < sov.MinNonce { return fmt.Errorf("%w %d must be at least %d", errWarpMessageContainsStaleNonce, msg.Nonce, sov.MinNonce) @@ -1031,7 +1032,7 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat // Verify that we are not removing the last validator. weight, err := e.state.WeightOfSubnetOnlyValidators(sov.SubnetID) if err != nil { - return err + return fmt.Errorf("could not load SoV weights: %w", err) } if weight == sov.Weight { return errRemovingLastValidator @@ -1042,7 +1043,7 @@ func (e *standardTxExecutor) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidat if sov.EndAccumulatedFee != 0 { var remainingBalanceOwner message.PChainOwner if _, err := txs.Codec.Unmarshal(sov.RemainingBalanceOwner, &remainingBalanceOwner); err != nil { - return err + return fmt.Errorf("%w: remaining balance owner is malformed", errStateCorruption) } accruedFees := e.state.GetAccruedFees() diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index c392e2dfe030..569231d7e2c9 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -3208,6 +3208,481 @@ func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { } } +func TestStandardExecutorSetSubnetValidatorWeightTx(t *testing.T) { + var ( + fx = &secp256k1fx.Fx{} + vm = &secp256k1fx.TestVM{ + Log: logging.NoLog{}, + } + ) + require.NoError(t, fx.InitializeVM(vm)) + + var ( + ctx = snowtest.Context(t, constants.PlatformChainID) + defaultConfig = &config.Config{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + baseState = statetest.New(t, statetest.Config{ + Upgrades: defaultConfig.UpgradeConfig, + Context: ctx, + }) + wallet = txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // chainIDs + ) + flowChecker = utxo.NewVerifier( + ctx, + &vm.Clk, + fx, + ) + + backend = &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + } + feeCalculator = state.PickFeeCalculator(defaultConfig, baseState) + ) + + // Create the initial state + diff, err := state.NewDiffOn(baseState) + require.NoError(t, err) + + // Create the subnet + createSubnetTx, err := wallet.IssueCreateSubnetTx( + &secp256k1fx.OutputOwners{}, + ) + require.NoError(t, err) + + // Execute the subnet creation + _, _, _, err = StandardTx( + backend, + feeCalculator, + createSubnetTx, + diff, + ) + require.NoError(t, err) + + // Create the subnet conversion + initialSK, err := bls.NewSecretKey() + require.NoError(t, err) + + const ( + initialWeight = 1 + initialBalance = units.Avax + ) + var ( + subnetID = createSubnetTx.ID() + chainID = ids.GenerateTestID() + address = utils.RandomBytes(32) + validator = &txs.ConvertSubnetValidator{ + NodeID: ids.GenerateTestNodeID().Bytes(), + Weight: initialWeight, + Balance: initialBalance, + Signer: *signer.NewProofOfPossession(initialSK), + // RemainingBalanceOwner and DeactivationOwner are initialized so + // that later reflect based equality checks pass. + RemainingBalanceOwner: message.PChainOwner{ + Threshold: 0, + Addresses: []ids.ShortID{}, + }, + DeactivationOwner: message.PChainOwner{ + Threshold: 0, + Addresses: []ids.ShortID{}, + }, + } + validationID = subnetID.Append(0) + ) + + convertSubnetTx, err := wallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + []*txs.ConvertSubnetValidator{ + validator, + }, + ) + require.NoError(t, err) + + // Execute the subnet conversion + _, _, _, err = StandardTx( + backend, + feeCalculator, + convertSubnetTx, + diff, + ) + require.NoError(t, err) + require.NoError(t, diff.Apply(baseState)) + require.NoError(t, baseState.Commit()) + + initialSoV, err := baseState.GetSubnetOnlyValidator(validationID) + require.NoError(t, err) + + // Create the Warp message + const ( + nonce = 1 + weight = initialWeight + 1 + ) + unsignedIncreaseWeightWarpMessage := must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetValidatorWeight](t)(message.NewSubnetValidatorWeight( + validationID, + nonce, + weight, + )).Bytes(), + )).Bytes(), + )) + warpSignature := &warp.BitSetSignature{ + Signers: set.NewBits(0).Bytes(), + Signature: ([bls.SignatureLen]byte)(bls.SignatureToBytes( + bls.Sign( + initialSK, + unsignedIncreaseWeightWarpMessage.Bytes(), + ), + )), + } + increaseWeightWarpMessage := must[*warp.Message](t)(warp.NewMessage( + unsignedIncreaseWeightWarpMessage, + warpSignature, + )) + removeValidatorWarpMessage := must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetValidatorWeight](t)(message.NewSubnetValidatorWeight( + validationID, + nonce, + 0, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )) + + increaseL1Weight := func(weight uint64) func(*standardTxExecutor) error { + return func(e *standardTxExecutor) error { + sov := initialSoV + sov.ValidationID = ids.GenerateTestID() + sov.NodeID = ids.GenerateTestNodeID() + sov.Weight = weight + return e.state.PutSubnetOnlyValidator(sov) + } + } + + tests := []struct { + name string + message []byte + builderOptions []common.Option + updateExecutor func(*standardTxExecutor) error + expectedNonce uint64 + expectedWeight uint64 + expectedRemainingFundsUTXO *avax.UTXO + expectedErr error + }{ + { + name: "invalid prior to E-Upgrade", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ + UpgradeConfig: upgradetest.GetConfig(upgradetest.Durango), + } + return nil + }, + expectedErr: errEtnaUpgradeNotActive, + }, + { + name: "tx fails syntactic verification", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) + return nil + }, + expectedErr: avax.ErrWrongChainID, + }, + { + name: "invalid memo length", + builderOptions: []common.Option{ + common.WithMemo([]byte("memo!")), + }, + expectedErr: avax.ErrMemoTooLarge, + }, + { + name: "invalid fee calculation", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewStaticCalculator(e.backend.Config.StaticFeeConfig) + return nil + }, + expectedErr: txfee.ErrUnsupportedTx, + }, + { + name: "insufficient fee", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewDynamicCalculator( + e.backend.Config.DynamicFeeConfig.Weights, + 100*genesis.LocalParams.DynamicFeeConfig.MinPrice, + ) + return nil + }, + expectedErr: utxo.ErrInsufficientUnlockedFunds, + }, + { + name: "invalid warp message", + message: []byte{}, + expectedErr: codec.ErrCantUnpackVersion, + }, + { + name: "invalid warp payload", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.Hash](t)(payload.NewHash(ids.Empty)).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: payload.ErrWrongType, + }, + { + name: "invalid addressed call", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetConversion](t)(message.NewSubnetConversion(ids.Empty)).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: message.ErrWrongType, + }, + { + name: "invalid addressed call payload", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetValidatorWeight](t)(message.NewSubnetValidatorWeight( + validationID, + math.MaxUint64, + 1, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: message.ErrNonceReservedForRemoval, + }, + { + name: "SoV not found", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetValidatorWeight](t)(message.NewSubnetValidatorWeight( + ids.GenerateTestID(), // invalid initialValidationID + nonce, + weight, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: errCouldNotLoadSoV, + }, + { + name: "nonce too low", + updateExecutor: func(e *standardTxExecutor) error { + sov := initialSoV + sov.MinNonce = nonce + 1 + return e.state.PutSubnetOnlyValidator(sov) + }, + expectedErr: errWarpMessageContainsStaleNonce, + }, + { + name: "invalid source chain", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion(subnetID, state.SubnetConversion{}) + return nil + }, + expectedErr: errWrongWarpMessageSourceChainID, + }, + { + name: "invalid source address", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion(subnetID, state.SubnetConversion{ + ChainID: chainID, + }) + return nil + }, + expectedErr: errWrongWarpMessageSourceAddress, + }, + { + name: "remove last validator", + message: removeValidatorWarpMessage.Bytes(), + expectedErr: errRemovingLastValidator, + }, + { + name: "remove deactivated validator", + message: removeValidatorWarpMessage.Bytes(), + updateExecutor: func(e *standardTxExecutor) error { + // Add another validator to allow removal + if err := increaseL1Weight(1)(e); err != nil { + return err + } + + sov := initialSoV + sov.EndAccumulatedFee = 0 // Deactivate the validator + return e.state.PutSubnetOnlyValidator(sov) + }, + }, + { + name: "should have been previously deactivated", + message: removeValidatorWarpMessage.Bytes(), + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetAccruedFees(initialSoV.EndAccumulatedFee) + return increaseL1Weight(1)(e) + }, + expectedErr: errStateCorruption, + }, + { + name: "remove active validator", + message: removeValidatorWarpMessage.Bytes(), + updateExecutor: increaseL1Weight(1), + expectedRemainingFundsUTXO: &avax.UTXO{ + Asset: avax.Asset{ + ID: ctx.AVAXAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: initialBalance, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: validator.RemainingBalanceOwner.Threshold, + Addrs: validator.RemainingBalanceOwner.Addresses, + }, + }, + }, + }, + { + name: "L1 weight overflow", + updateExecutor: increaseL1Weight(math.MaxUint64 - initialWeight), + expectedErr: safemath.ErrOverflow, + }, + { + name: "update validator", + expectedNonce: nonce + 1, + expectedWeight: weight, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + // Create the SetSubnetValidatorWeightTx + wallet := txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // chainIDs + ) + + message := test.message + if message == nil { + message = increaseWeightWarpMessage.Bytes() + } + setSubnetValidatorWeightTx, err := wallet.IssueSetSubnetValidatorWeightTx( + message, + test.builderOptions..., + ) + require.NoError(err) + + diff, err := state.NewDiffOn(baseState) + require.NoError(err) + + executor := &standardTxExecutor{ + backend: &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + }, + feeCalculator: state.PickFeeCalculator(defaultConfig, baseState), + tx: setSubnetValidatorWeightTx, + state: diff, + } + if test.updateExecutor != nil { + require.NoError(test.updateExecutor(executor)) + } + + err = setSubnetValidatorWeightTx.Unsigned.Visit(executor) + require.ErrorIs(err, test.expectedErr) + if err != nil { + return + } + + for utxoID := range setSubnetValidatorWeightTx.InputIDs() { + _, err := diff.GetUTXO(utxoID) + require.ErrorIs(err, database.ErrNotFound) + } + + baseTxOutputUTXOs := setSubnetValidatorWeightTx.UTXOs() + for _, expectedUTXO := range baseTxOutputUTXOs { + utxoID := expectedUTXO.InputID() + utxo, err := diff.GetUTXO(utxoID) + require.NoError(err) + require.Equal(expectedUTXO, utxo) + } + + sov, err := diff.GetSubnetOnlyValidator(validationID) + if test.expectedWeight != 0 { + require.NoError(err) + + expectedSoV := initialSoV + expectedSoV.MinNonce = test.expectedNonce + expectedSoV.Weight = test.expectedWeight + require.Equal(expectedSoV, sov) + return + } + require.ErrorIs(err, database.ErrNotFound) + + utxoID := avax.UTXOID{ + TxID: setSubnetValidatorWeightTx.ID(), + OutputIndex: uint32(len(baseTxOutputUTXOs)), + } + inputID := utxoID.InputID() + utxo, err := diff.GetUTXO(inputID) + if test.expectedRemainingFundsUTXO == nil { + require.ErrorIs(err, database.ErrNotFound) + return + } + require.NoError(err) + + test.expectedRemainingFundsUTXO.UTXOID = utxoID + require.Equal(test.expectedRemainingFundsUTXO, utxo) + }) + } +} + func must[T any](t require.TestingT) func(T, error) T { return func(val T, err error) T { require.NoError(t, err) From c975b679124e0c3e2bf9387788d4841ab837380c Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 13:04:40 -0500 Subject: [PATCH 180/184] nit --- vms/platformvm/txs/executor/standard_tx_executor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 569231d7e2c9..3acad24eb5e0 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -3327,7 +3327,7 @@ func TestStandardExecutorSetSubnetValidatorWeightTx(t *testing.T) { initialSoV, err := baseState.GetSubnetOnlyValidator(validationID) require.NoError(t, err) - // Create the Warp message + // Create the Warp messages const ( nonce = 1 weight = initialWeight + 1 From ddd97e3612cbaf2ccecca8d3b692cdb3faa86ed6 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 13:08:24 -0500 Subject: [PATCH 181/184] Add overflow case --- .../txs/executor/standard_tx_executor_test.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 3acad24eb5e0..970c97121f82 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -3553,6 +3553,34 @@ func TestStandardExecutorSetSubnetValidatorWeightTx(t *testing.T) { return e.state.PutSubnetOnlyValidator(sov) }, }, + { + name: "remove deactivated validator with nonce overflow", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetValidatorWeight](t)(message.NewSubnetValidatorWeight( + validationID, + math.MaxUint64, + 0, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + updateExecutor: func(e *standardTxExecutor) error { + // Add another validator to allow removal + if err := increaseL1Weight(1)(e); err != nil { + return err + } + + sov := initialSoV + sov.EndAccumulatedFee = 0 // Deactivate the validator + return e.state.PutSubnetOnlyValidator(sov) + }, + }, { name: "should have been previously deactivated", message: removeValidatorWarpMessage.Bytes(), From e92cf64cb2b0ffa50bcc731a76df0a6e0b111593 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 14:37:10 -0500 Subject: [PATCH 182/184] ACP-77: Implement `RegisterSubnetValidatorTx` (#3420) --- tests/e2e/p/l1.go | 239 +++++++- vms/platformvm/metrics/tx_metrics.go | 7 + vms/platformvm/signer/proof_of_possession.go | 4 +- .../signer/proof_of_possession_test.go | 2 +- vms/platformvm/txs/codec.go | 1 + .../txs/executor/atomic_tx_executor.go | 4 + .../txs/executor/proposal_tx_executor.go | 4 + .../txs/executor/standard_tx_executor.go | 191 +++++- .../txs/executor/standard_tx_executor_test.go | 544 ++++++++++++++++++ vms/platformvm/txs/fee/calculator_test.go | 12 + vms/platformvm/txs/fee/complexity.go | 35 ++ vms/platformvm/txs/fee/static_calculator.go | 4 + .../txs/register_subnet_validator_tx.go | 45 ++ .../txs/register_subnet_validator_tx_test.go | 393 +++++++++++++ .../register_subnet_validator_tx_test.json | 175 ++++++ vms/platformvm/txs/visitor.go | 1 + vms/platformvm/validators/manager.go | 22 +- vms/platformvm/warp/payload/addressed_call.go | 2 +- vms/platformvm/warp/payload/hash.go | 2 +- vms/platformvm/warp/payload/payload.go | 2 +- vms/platformvm/warp/payload/payload_test.go | 4 +- wallet/chain/p/builder/builder.go | 72 +++ wallet/chain/p/builder/with_options.go | 15 + wallet/chain/p/builder_test.go | 102 ++++ wallet/chain/p/signer/visitor.go | 8 + wallet/chain/p/wallet/backend_visitor.go | 4 + wallet/chain/p/wallet/wallet.go | 29 + wallet/chain/p/wallet/with_options.go | 15 + .../register-subnet-validator/main.go | 141 +++++ 29 files changed, 2056 insertions(+), 23 deletions(-) create mode 100644 vms/platformvm/txs/register_subnet_validator_tx.go create mode 100644 vms/platformvm/txs/register_subnet_validator_tx_test.go create mode 100644 vms/platformvm/txs/register_subnet_validator_tx_test.json create mode 100644 wallet/subnet/primary/examples/register-subnet-validator/main.go diff --git a/tests/e2e/p/l1.go b/tests/e2e/p/l1.go index e9c8ec89a6c3..73c1e08eed1e 100644 --- a/tests/e2e/p/l1.go +++ b/tests/e2e/p/l1.go @@ -4,33 +4,58 @@ package p import ( + "context" + "errors" "math" + "slices" "time" "github.com/onsi/ginkgo/v2" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" "github.com/ava-labs/avalanchego/api/info" "github.com/ava-labs/avalanchego/config" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/network/peer" + "github.com/ava-labs/avalanchego/proto/pb/sdk" + "github.com/ava-labs/avalanchego/snow/networking/router" "github.com/ava-labs/avalanchego/tests/fixture/e2e" "github.com/ava-labs/avalanchego/tests/fixture/tmpnet" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/buffer" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/example/xsvm/genesis" "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + p2pmessage "github.com/ava-labs/avalanchego/message" + p2psdk "github.com/ava-labs/avalanchego/network/p2p" + p2ppb "github.com/ava-labs/avalanchego/proto/pb/p2p" snowvalidators "github.com/ava-labs/avalanchego/snow/validators" + platformvmvalidators "github.com/ava-labs/avalanchego/vms/platformvm/validators" warpmessage "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" ) const ( - genesisWeight = units.Schmeckle - genesisBalance = units.Avax + genesisWeight = units.Schmeckle + genesisBalance = units.Avax + registerWeight = genesisWeight / 10 + registerBalance = 0 + + // Validator registration attempts expire 5 minutes after they are created + expiryDelay = 5 * time.Minute + // P2P message requests timeout after 10 seconds + p2pTimeout = 10 * time.Second ) var _ = e2e.DescribePChain("[L1]", func() { @@ -164,6 +189,22 @@ var _ = e2e.DescribePChain("[L1]", func() { genesisNodePK, err := bls.PublicKeyFromCompressedBytes(genesisNodePoP.PublicKey[:]) require.NoError(err) + tc.By("connecting to the genesis validator") + var ( + networkID = env.GetNetwork().GetNetworkID() + genesisPeerMessages = buffer.NewUnboundedBlockingDeque[p2pmessage.InboundMessage](1) + ) + genesisPeer, err := peer.StartTestPeer( + tc.DefaultContext(), + subnetGenesisNode.StakingAddress, + networkID, + router.InboundHandlerFunc(func(_ context.Context, m p2pmessage.InboundMessage) { + tc.Outf("received %s %s from %s\n", m.Op(), m.Message(), m.NodeID()) + genesisPeerMessages.PushRight(m) + }), + ) + require.NoError(err) + address := []byte{} tc.By("issuing a ConvertSubnetTx", func() { _, err := pWallet.IssueConvertSubnetTx( @@ -227,6 +268,200 @@ var _ = e2e.DescribePChain("[L1]", func() { }) }) + advanceProposerVMPChainHeight := func() { + // We must wait at least [RecentlyAcceptedWindowTTL] to ensure the + // next block will reference the last accepted P-chain height. + time.Sleep((5 * platformvmvalidators.RecentlyAcceptedWindowTTL) / 4) + } + tc.By("advancing the proposervm P-chain height", advanceProposerVMPChainHeight) + + tc.By("creating the validator to register") + subnetRegisterNode := e2e.AddEphemeralNode(tc, env.GetNetwork(), tmpnet.FlagsMap{ + config.TrackSubnetsKey: subnetID.String(), + }) + + registerNodePoP, err := subnetRegisterNode.GetProofOfPossession() + require.NoError(err) + + tc.By("ensuring the subnet nodes are healthy", func() { + e2e.WaitForHealthy(tc, subnetGenesisNode) + e2e.WaitForHealthy(tc, subnetRegisterNode) + }) + + tc.By("creating the RegisterSubnetValidatorMessage") + expiry := uint64(time.Now().Add(expiryDelay).Unix()) // This message will expire in 5 minutes + registerSubnetValidatorMessage, err := warpmessage.NewRegisterSubnetValidator( + subnetID, + subnetRegisterNode.NodeID, + registerNodePoP.PublicKey, + expiry, + warpmessage.PChainOwner{}, + warpmessage.PChainOwner{}, + registerWeight, + ) + require.NoError(err) + + tc.By("registering the validator", func() { + tc.By("creating the unsigned warp message") + unsignedRegisterSubnetValidator := must[*warp.UnsignedMessage](tc)(warp.NewUnsignedMessage( + networkID, + chainID, + must[*payload.AddressedCall](tc)(payload.NewAddressedCall( + address, + registerSubnetValidatorMessage.Bytes(), + )).Bytes(), + )) + + tc.By("sending the request to sign the warp message", func() { + registerSubnetValidatorRequest, err := wrapWarpSignatureRequest( + unsignedRegisterSubnetValidator, + nil, + ) + require.NoError(err) + + require.True(genesisPeer.Send(tc.DefaultContext(), registerSubnetValidatorRequest)) + }) + + tc.By("getting the signature response") + registerSubnetValidatorSignature, ok, err := findMessage(genesisPeerMessages, unwrapWarpSignature) + require.NoError(err) + require.True(ok) + + tc.By("creating the signed warp message to register the validator") + registerSubnetValidator, err := warp.NewMessage( + unsignedRegisterSubnetValidator, + &warp.BitSetSignature{ + Signers: set.NewBits(0).Bytes(), // [signers] has weight from the genesis peer + Signature: ([bls.SignatureLen]byte)( + bls.SignatureToBytes(registerSubnetValidatorSignature), + ), + }, + ) + require.NoError(err) + + tc.By("issuing a RegisterSubnetValidatorTx", func() { + _, err := pWallet.IssueRegisterSubnetValidatorTx( + registerBalance, + registerNodePoP.ProofOfPossession, + registerSubnetValidator.Bytes(), + ) + require.NoError(err) + }) + }) + + tc.By("verifying the validator was registered", func() { + tc.By("verifying the validator set was updated", func() { + verifyValidatorSet(map[ids.NodeID]*snowvalidators.GetValidatorOutput{ + subnetGenesisNode.NodeID: { + NodeID: subnetGenesisNode.NodeID, + PublicKey: genesisNodePK, + Weight: genesisWeight, + }, + ids.EmptyNodeID: { // The validator is not active + NodeID: ids.EmptyNodeID, + Weight: registerWeight, + }, + }) + }) + }) + + genesisPeerMessages.Close() + genesisPeer.StartClose() + require.NoError(genesisPeer.AwaitClosed(tc.DefaultContext())) + _ = e2e.CheckBootstrapIsPossible(tc, env.GetNetwork()) }) }) + +func wrapWarpSignatureRequest( + msg *warp.UnsignedMessage, + justification []byte, +) (p2pmessage.OutboundMessage, error) { + p2pMessageFactory, err := p2pmessage.NewCreator( + logging.NoLog{}, + prometheus.NewRegistry(), + constants.DefaultNetworkCompressionType, + p2pTimeout, + ) + if err != nil { + return nil, err + } + + request := sdk.SignatureRequest{ + Message: msg.Bytes(), + Justification: justification, + } + requestBytes, err := proto.Marshal(&request) + if err != nil { + return nil, err + } + + return p2pMessageFactory.AppRequest( + msg.SourceChainID, + 0, + time.Hour, + p2psdk.PrefixMessage( + p2psdk.ProtocolPrefix(p2psdk.SignatureRequestHandlerID), + requestBytes, + ), + ) +} + +func findMessage[T any]( + q buffer.BlockingDeque[p2pmessage.InboundMessage], + parser func(p2pmessage.InboundMessage) (T, bool, error), +) (T, bool, error) { + var messagesToReprocess []p2pmessage.InboundMessage + defer func() { + slices.Reverse(messagesToReprocess) + for _, msg := range messagesToReprocess { + q.PushLeft(msg) + } + }() + + for { + msg, ok := q.PopLeft() + if !ok { + return utils.Zero[T](), false, nil + } + + parsed, ok, err := parser(msg) + if err != nil { + return utils.Zero[T](), false, err + } + if ok { + return parsed, true, nil + } + + messagesToReprocess = append(messagesToReprocess, msg) + } +} + +// unwrapWarpSignature assumes the only type of AppResponses that will be +// received are ACP-118 compliant responses. +func unwrapWarpSignature(msg p2pmessage.InboundMessage) (*bls.Signature, bool, error) { + var appResponse *p2ppb.AppResponse + switch msg := msg.Message().(type) { + case *p2ppb.AppResponse: + appResponse = msg + case *p2ppb.AppError: + return nil, false, errors.New(msg.ErrorMessage) + default: + return nil, false, nil + } + + var response sdk.SignatureResponse + if err := proto.Unmarshal(appResponse.AppBytes, &response); err != nil { + return nil, false, err + } + + warpSignature, err := bls.SignatureFromBytes(response.Signature) + return warpSignature, true, err +} + +func must[T any](t require.TestingT) func(T, error) T { + return func(val T, err error) T { + require.NoError(t, err) + return val + } +} diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index 7957cb6ab2e9..1da84206cf09 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -145,3 +145,10 @@ func (m *txMetrics) ConvertSubnetTx(*txs.ConvertSubnetTx) error { }).Inc() return nil } + +func (m *txMetrics) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorTx) error { + m.numTxs.With(prometheus.Labels{ + txLabel: "register_subnet_validator", + }).Inc() + return nil +} diff --git a/vms/platformvm/signer/proof_of_possession.go b/vms/platformvm/signer/proof_of_possession.go index 245d2a96c6f0..f63365d985f4 100644 --- a/vms/platformvm/signer/proof_of_possession.go +++ b/vms/platformvm/signer/proof_of_possession.go @@ -14,7 +14,7 @@ import ( var ( _ Signer = (*ProofOfPossession)(nil) - errInvalidProofOfPossession = errors.New("invalid proof of possession") + ErrInvalidProofOfPossession = errors.New("invalid proof of possession") ) type ProofOfPossession struct { @@ -52,7 +52,7 @@ func (p *ProofOfPossession) Verify() error { return err } if !bls.VerifyProofOfPossession(publicKey, signature, p.PublicKey[:]) { - return errInvalidProofOfPossession + return ErrInvalidProofOfPossession } p.publicKey = publicKey diff --git a/vms/platformvm/signer/proof_of_possession_test.go b/vms/platformvm/signer/proof_of_possession_test.go index 9f4f3feefa3c..9674d63985fc 100644 --- a/vms/platformvm/signer/proof_of_possession_test.go +++ b/vms/platformvm/signer/proof_of_possession_test.go @@ -35,7 +35,7 @@ func TestProofOfPossession(t *testing.T) { require.NoError(err) newBLSPOP.ProofOfPossession = blsPOP.ProofOfPossession err = newBLSPOP.Verify() - require.ErrorIs(err, errInvalidProofOfPossession) + require.ErrorIs(err, ErrInvalidProofOfPossession) } func TestNewProofOfPossessionDeterministic(t *testing.T) { diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index 123da91b8dea..4c3892753555 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -123,5 +123,6 @@ func RegisterDurangoTypes(targetCodec linearcodec.Codec) error { func RegisterEtnaTypes(targetCodec linearcodec.Codec) error { return errors.Join( targetCodec.RegisterType(&ConvertSubnetTx{}), + targetCodec.RegisterType(&RegisterSubnetValidatorTx{}), ) } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 1977608d09c1..95c56b24c4e6 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -112,6 +112,10 @@ func (*atomicTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } +func (*atomicTxExecutor) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorTx) error { + return ErrWrongTxType +} + func (e *atomicTxExecutor) ImportTx(*txs.ImportTx) error { return e.atomicTx() } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index 03b03c889a23..aa2e949b312f 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -131,6 +131,10 @@ func (*proposalTxExecutor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrWrongTxType } +func (*proposalTxExecutor) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorTx) 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 diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 3ccc059bfa8e..b64972452eba 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -4,6 +4,7 @@ package executor import ( + "bytes" "context" "errors" "fmt" @@ -20,21 +21,40 @@ import ( "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/gas" "github.com/ava-labs/avalanchego/vms/components/verify" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" +) + +// TODO: Before Etna, ensure that the maximum number of expiries to track is +// limited to a reasonable number by this window. +const ( + second = 1 + minute = 60 * second + hour = 60 * minute + day = 24 * hour + RegisterSubnetValidatorTxExpiryWindow = day ) var ( _ txs.Visitor = (*standardTxExecutor)(nil) - errEmptyNodeID = errors.New("validator nodeID cannot be empty") - errMaxStakeDurationTooLarge = errors.New("max stake duration must be less than or equal to the global max stake duration") - errMissingStartTimePreDurango = errors.New("staker transactions must have a StartTime pre-Durango") - errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") - errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") - errMaxNumActiveValidators = errors.New("already at the max number of active validators") + errEmptyNodeID = errors.New("validator nodeID cannot be empty") + errMaxStakeDurationTooLarge = errors.New("max stake duration must be less than or equal to the global max stake duration") + errMissingStartTimePreDurango = errors.New("staker transactions must have a StartTime pre-Durango") + errEtnaUpgradeNotActive = errors.New("attempting to use an Etna-upgrade feature prior to activation") + errTransformSubnetTxPostEtna = errors.New("TransformSubnetTx is not permitted post-Etna") + errMaxNumActiveValidators = errors.New("already at the max number of active validators") + errCouldNotLoadSubnetConversion = errors.New("could not load subnet conversion") + errWrongWarpMessageSourceChainID = errors.New("wrong warp message source chain ID") + errWrongWarpMessageSourceAddress = errors.New("wrong warp message source address") + errWarpMessageExpired = errors.New("warp message expired") + errWarpMessageNotYetAllowed = errors.New("warp message not yet allowed") + errWarpMessageAlreadyIssued = errors.New("warp message already issued") ) // StandardTx executes the standard transaction [tx]. @@ -780,6 +800,165 @@ func (e *standardTxExecutor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return nil } +func (e *standardTxExecutor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { + var ( + currentTimestamp = e.state.GetTimestamp() + upgrades = e.backend.Config.UpgradeConfig + ) + if !upgrades.IsEtnaActivated(currentTimestamp) { + return errEtnaUpgradeNotActive + } + + if err := e.tx.SyntacticVerify(e.backend.Ctx); err != nil { + return err + } + + if err := avax.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil { + return err + } + + // Verify the flowcheck + fee, err := e.feeCalculator.CalculateFee(tx) + if err != nil { + return err + } + fee, err = math.Add(fee, tx.Balance) + if err != nil { + return err + } + + if err := e.backend.FlowChecker.VerifySpend( + tx, + e.state, + tx.Ins, + tx.Outs, + e.tx.Creds, + map[ids.ID]uint64{ + e.backend.Ctx.AVAXAssetID: fee, + }, + ); err != nil { + return err + } + + // Parse the warp message. + warpMessage, err := warp.ParseMessage(tx.Message) + if err != nil { + return err + } + addressedCall, err := payload.ParseAddressedCall(warpMessage.Payload) + if err != nil { + return err + } + msg, err := message.ParseRegisterSubnetValidator(addressedCall.Payload) + if err != nil { + return err + } + if err := msg.Verify(); err != nil { + return err + } + + // Verify that the warp message was sent from the expected chain and + // address. + subnetConversion, err := e.state.GetSubnetConversion(msg.SubnetID) + if err != nil { + return fmt.Errorf("%w for %s with: %w", errCouldNotLoadSubnetConversion, msg.SubnetID, err) + } + if warpMessage.SourceChainID != subnetConversion.ChainID { + return fmt.Errorf("%w expected %s but had %s", errWrongWarpMessageSourceChainID, subnetConversion.ChainID, warpMessage.SourceChainID) + } + if !bytes.Equal(addressedCall.SourceAddress, subnetConversion.Addr) { + return fmt.Errorf("%w expected 0x%x but got 0x%x", errWrongWarpMessageSourceAddress, subnetConversion.Addr, addressedCall.SourceAddress) + } + + // Verify that the message contains a valid expiry time. + currentTimestampUnix := uint64(currentTimestamp.Unix()) + if msg.Expiry <= currentTimestampUnix { + return fmt.Errorf("%w at %d and it is currently %d", errWarpMessageExpired, msg.Expiry, currentTimestampUnix) + } + if secondsUntilExpiry := msg.Expiry - currentTimestampUnix; secondsUntilExpiry > RegisterSubnetValidatorTxExpiryWindow { + return fmt.Errorf("%w because time is %d seconds in the future but the limit is %d", errWarpMessageNotYetAllowed, secondsUntilExpiry, RegisterSubnetValidatorTxExpiryWindow) + } + + // Verify that this warp message isn't being replayed. + validationID := msg.ValidationID() + expiry := state.ExpiryEntry{ + Timestamp: msg.Expiry, + ValidationID: validationID, + } + isDuplicate, err := e.state.HasExpiry(expiry) + if err != nil { + return err + } + if isDuplicate { + return fmt.Errorf("%w for validationID %s", errWarpMessageAlreadyIssued, validationID) + } + + // Verify proof of possession provided by the transaction against the public + // key provided by the warp message. + pop := signer.ProofOfPossession{ + PublicKey: msg.BLSPublicKey, + ProofOfPossession: tx.ProofOfPossession, + } + if err := pop.Verify(); err != nil { + return err + } + + // Create the SoV. + nodeID, err := ids.ToNodeID(msg.NodeID) + if err != nil { + return err + } + remainingBalanceOwner, err := txs.Codec.Marshal(txs.CodecVersion, &msg.RemainingBalanceOwner) + if err != nil { + return err + } + deactivationOwner, err := txs.Codec.Marshal(txs.CodecVersion, &msg.DisableOwner) + if err != nil { + return err + } + sov := state.SubnetOnlyValidator{ + ValidationID: validationID, + SubnetID: msg.SubnetID, + NodeID: nodeID, + PublicKey: bls.PublicKeyToUncompressedBytes(pop.Key()), + RemainingBalanceOwner: remainingBalanceOwner, + DeactivationOwner: deactivationOwner, + StartTime: currentTimestampUnix, + Weight: msg.Weight, + MinNonce: 0, + EndAccumulatedFee: 0, // If Balance is 0, this is will remain 0 + } + + // If the balance is non-zero, this validator should be initially active. + if tx.Balance != 0 { + // Verify that there is space for an active validator. + if gas.Gas(e.state.NumActiveSubnetOnlyValidators()) >= e.backend.Config.ValidatorFeeConfig.Capacity { + return errMaxNumActiveValidators + } + + // Mark the validator as active. + currentFees := e.state.GetAccruedFees() + sov.EndAccumulatedFee, err = math.Add(tx.Balance, currentFees) + if err != nil { + return err + } + } + + if err := e.state.PutSubnetOnlyValidator(sov); err != nil { + return err + } + + txID := e.tx.ID() + + // Consume the UTXOS + avax.Consume(e.state, tx.Ins) + // Produce the UTXOS + avax.Produce(e.state, txID, tx.Outs) + // Prevent this warp message from being replayed + e.state.PutExpiry(expiry) + return nil +} + // Creates the staker as defined in [stakerTx] and adds it to [e.State]. func (e *standardTxExecutor) putStaker(stakerTx txs.Staker) error { var ( diff --git a/vms/platformvm/txs/executor/standard_tx_executor_test.go b/vms/platformvm/txs/executor/standard_tx_executor_test.go index 8a2e574ccfc5..c392e2dfe030 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor_test.go +++ b/vms/platformvm/txs/executor/standard_tx_executor_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/database" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" @@ -25,6 +26,7 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/hashing" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/verify" @@ -40,10 +42,13 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/txs/txstest" "github.com/ava-labs/avalanchego/vms/platformvm/utxo" "github.com/ava-labs/avalanchego/vms/platformvm/utxo/utxomock" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + safemath "github.com/ava-labs/avalanchego/utils/math" txfee "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" validatorfee "github.com/ava-labs/avalanchego/vms/platformvm/validators/fee" ) @@ -2670,3 +2675,542 @@ func TestStandardExecutorConvertSubnetTx(t *testing.T) { }) } } + +func TestStandardExecutorRegisterSubnetValidatorTx(t *testing.T) { + var ( + fx = &secp256k1fx.Fx{} + vm = &secp256k1fx.TestVM{ + Log: logging.NoLog{}, + } + ) + require.NoError(t, fx.InitializeVM(vm)) + + var ( + ctx = snowtest.Context(t, constants.PlatformChainID) + defaultConfig = &config.Config{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: genesis.LocalParams.ValidatorFeeConfig, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + baseState = statetest.New(t, statetest.Config{ + Upgrades: defaultConfig.UpgradeConfig, + Context: ctx, + }) + wallet = txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // chainIDs + ) + flowChecker = utxo.NewVerifier( + ctx, + &vm.Clk, + fx, + ) + + backend = &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + } + feeCalculator = state.PickFeeCalculator(defaultConfig, baseState) + ) + + // Create the initial state + diff, err := state.NewDiffOn(baseState) + require.NoError(t, err) + + // Create the subnet + createSubnetTx, err := wallet.IssueCreateSubnetTx( + &secp256k1fx.OutputOwners{}, + ) + require.NoError(t, err) + + // Execute the subnet creation + _, _, _, err = StandardTx( + backend, + feeCalculator, + createSubnetTx, + diff, + ) + require.NoError(t, err) + + // Create the subnet conversion + initialSK, err := bls.NewSecretKey() + require.NoError(t, err) + + const ( + initialWeight = 1 + initialBalance = units.Avax + ) + var ( + subnetID = createSubnetTx.ID() + chainID = ids.GenerateTestID() + address = utils.RandomBytes(32) + initialNodeID = ids.GenerateTestNodeID() + initialPoP = signer.NewProofOfPossession(initialSK) + validator = &txs.ConvertSubnetValidator{ + NodeID: initialNodeID.Bytes(), + Weight: initialWeight, + Balance: initialBalance, + Signer: *initialPoP, + RemainingBalanceOwner: message.PChainOwner{}, + DeactivationOwner: message.PChainOwner{}, + } + ) + convertSubnetTx, err := wallet.IssueConvertSubnetTx( + subnetID, + chainID, + address, + []*txs.ConvertSubnetValidator{ + validator, + }, + ) + require.NoError(t, err) + + // Execute the subnet conversion + _, _, _, err = StandardTx( + backend, + feeCalculator, + convertSubnetTx, + diff, + ) + require.NoError(t, err) + require.NoError(t, diff.Apply(baseState)) + require.NoError(t, baseState.Commit()) + + var ( + nodeID = ids.GenerateTestNodeID() + lastAcceptedTime = baseState.GetTimestamp() + expiryTime = lastAcceptedTime.Add(5 * time.Minute) + expiry = uint64(expiryTime.Unix()) // The warp message will expire in 5 minutes + ) + + const weight = 1 + + // Create the Warp message + sk, err := bls.NewSecretKey() + require.NoError(t, err) + pop := signer.NewProofOfPossession(sk) + pk := bls.PublicFromSecretKey(sk) + pkBytes := bls.PublicKeyToUncompressedBytes(pk) + + remainingBalanceOwner := message.PChainOwner{} + remainingBalanceOwnerBytes, err := txs.Codec.Marshal(txs.CodecVersion, &remainingBalanceOwner) + require.NoError(t, err) + + deactivationOwner := message.PChainOwner{} + deactivationOwnerBytes, err := txs.Codec.Marshal(txs.CodecVersion, &deactivationOwner) + require.NoError(t, err) + + addressedCallPayload := must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + expiry, + remainingBalanceOwner, + deactivationOwner, + weight, + )) + unsignedWarp := must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + addressedCallPayload.Bytes(), + )).Bytes(), + )) + warpSignature := &warp.BitSetSignature{ + Signers: set.NewBits(0).Bytes(), + Signature: ([bls.SignatureLen]byte)(bls.SignatureToBytes( + bls.Sign( + sk, + unsignedWarp.Bytes(), + ), + )), + } + warpMessage := must[*warp.Message](t)(warp.NewMessage( + unsignedWarp, + warpSignature, + )) + + validationID := addressedCallPayload.ValidationID() + tests := []struct { + name string + balance uint64 + message []byte + builderOptions []common.Option + updateTx func(*txs.RegisterSubnetValidatorTx) + updateExecutor func(*standardTxExecutor) error + expectedErr error + }{ + { + name: "invalid prior to E-Upgrade", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ + UpgradeConfig: upgradetest.GetConfig(upgradetest.Durango), + } + return nil + }, + expectedErr: errEtnaUpgradeNotActive, + }, + { + name: "tx fails syntactic verification", + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Ctx = snowtest.Context(t, ids.GenerateTestID()) + return nil + }, + expectedErr: avax.ErrWrongChainID, + }, + { + name: "invalid memo length", + builderOptions: []common.Option{ + common.WithMemo([]byte("memo!")), + }, + expectedErr: avax.ErrMemoTooLarge, + }, + { + name: "invalid fee calculation", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewStaticCalculator(e.backend.Config.StaticFeeConfig) + return nil + }, + expectedErr: txfee.ErrUnsupportedTx, + }, + { + name: "fee calculation overflow", + updateTx: func(tx *txs.RegisterSubnetValidatorTx) { + tx.Balance = math.MaxUint64 + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "insufficient fee", + updateExecutor: func(e *standardTxExecutor) error { + e.feeCalculator = txfee.NewDynamicCalculator( + e.backend.Config.DynamicFeeConfig.Weights, + 100*genesis.LocalParams.DynamicFeeConfig.MinPrice, + ) + return nil + }, + expectedErr: utxo.ErrInsufficientUnlockedFunds, + }, + { + name: "invalid warp message", + message: []byte{}, + expectedErr: codec.ErrCantUnpackVersion, + }, + { + name: "invalid warp payload", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.Hash](t)(payload.NewHash(ids.Empty)).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: payload.ErrWrongType, + }, + { + name: "invalid addressed call", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.SubnetConversion](t)(message.NewSubnetConversion(ids.Empty)).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: message.ErrWrongType, + }, + { + name: "invalid addressed call payload", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + expiry, + remainingBalanceOwner, + deactivationOwner, + 0, // weight = 0 is invalid + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: message.ErrInvalidWeight, + }, + { + name: "subnet conversion not found", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + ids.GenerateTestID(), // invalid subnetID + nodeID, + pop.PublicKey, + expiry, + remainingBalanceOwner, + deactivationOwner, + weight, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: errCouldNotLoadSubnetConversion, + }, + { + name: "invalid source chain", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion(subnetID, state.SubnetConversion{}) + return nil + }, + expectedErr: errWrongWarpMessageSourceChainID, + }, + { + name: "invalid source address", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetSubnetConversion(subnetID, state.SubnetConversion{ + ChainID: chainID, + }) + return nil + }, + expectedErr: errWrongWarpMessageSourceAddress, + }, + { + name: "message expired", + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetTimestamp(expiryTime) + return nil + }, + expectedErr: errWarpMessageExpired, + }, + { + name: "message expiry too far in the future", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + math.MaxUint64, // expiry too far in the future + remainingBalanceOwner, + deactivationOwner, + weight, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: errWarpMessageNotYetAllowed, + }, + { + name: "SoV previously registered", + balance: 1, + updateExecutor: func(e *standardTxExecutor) error { + e.state.PutExpiry(state.ExpiryEntry{ + Timestamp: expiry, + ValidationID: validationID, + }) + return nil + }, + expectedErr: errWarpMessageAlreadyIssued, + }, + { + name: "invalid PoP", + message: must[*warp.Message](t)(warp.NewMessage( + must[*warp.UnsignedMessage](t)(warp.NewUnsignedMessage( + ctx.NetworkID, + chainID, + must[*payload.AddressedCall](t)(payload.NewAddressedCall( + address, + must[*message.RegisterSubnetValidator](t)(message.NewRegisterSubnetValidator( + subnetID, + nodeID, + initialPoP.PublicKey, // Wrong public key + expiry, + remainingBalanceOwner, + deactivationOwner, + weight, + )).Bytes(), + )).Bytes(), + )), + warpSignature, + )).Bytes(), + expectedErr: signer.ErrInvalidProofOfPossession, + }, + { + name: "too many active validators", + balance: 1, + updateExecutor: func(e *standardTxExecutor) error { + e.backend.Config = &config.Config{ + DynamicFeeConfig: genesis.LocalParams.DynamicFeeConfig, + ValidatorFeeConfig: validatorfee.Config{ + Capacity: 0, + Target: genesis.LocalParams.ValidatorFeeConfig.Target, + MinPrice: genesis.LocalParams.ValidatorFeeConfig.MinPrice, + ExcessConversionConstant: genesis.LocalParams.ValidatorFeeConfig.ExcessConversionConstant, + }, + UpgradeConfig: upgradetest.GetConfig(upgradetest.Latest), + } + return nil + }, + expectedErr: errMaxNumActiveValidators, + }, + { + name: "accrued fees overflow", + balance: 1, + updateExecutor: func(e *standardTxExecutor) error { + e.state.SetAccruedFees(math.MaxUint64) + return nil + }, + expectedErr: safemath.ErrOverflow, + }, + { + name: "duplicate subnetID + nodeID pair", + updateExecutor: func(e *standardTxExecutor) error { + return e.state.PutSubnetOnlyValidator(state.SubnetOnlyValidator{ + ValidationID: ids.GenerateTestID(), + SubnetID: subnetID, + NodeID: nodeID, + PublicKey: bls.PublicKeyToUncompressedBytes(bls.PublicFromSecretKey(initialSK)), + Weight: 1, + }) + }, + expectedErr: state.ErrDuplicateSubnetOnlyValidator, + }, + { + name: "valid tx", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + // Create the RegisterSubnetValidatorTx + wallet := txstest.NewWallet( + t, + ctx, + defaultConfig, + baseState, + secp256k1fx.NewKeychain(genesistest.DefaultFundedKeys...), + nil, // subnetIDs + nil, // chainIDs + ) + + message := test.message + if message == nil { + message = warpMessage.Bytes() + } + registerSubnetValidatorTx, err := wallet.IssueRegisterSubnetValidatorTx( + test.balance, + pop.ProofOfPossession, + message, + test.builderOptions..., + ) + require.NoError(err) + + if test.updateTx != nil { + unsignedTx := registerSubnetValidatorTx.Unsigned.(*txs.RegisterSubnetValidatorTx) + test.updateTx(unsignedTx) + } + + diff, err := state.NewDiffOn(baseState) + require.NoError(err) + + executor := &standardTxExecutor{ + backend: &Backend{ + Config: defaultConfig, + Bootstrapped: utils.NewAtomic(true), + Fx: fx, + FlowChecker: flowChecker, + Ctx: ctx, + }, + feeCalculator: state.PickFeeCalculator(defaultConfig, baseState), + tx: registerSubnetValidatorTx, + state: diff, + } + if test.updateExecutor != nil { + require.NoError(test.updateExecutor(executor)) + } + + err = registerSubnetValidatorTx.Unsigned.Visit(executor) + require.ErrorIs(err, test.expectedErr) + if err != nil { + return + } + + for utxoID := range registerSubnetValidatorTx.InputIDs() { + _, err := diff.GetUTXO(utxoID) + require.ErrorIs(err, database.ErrNotFound) + } + + for _, expectedUTXO := range registerSubnetValidatorTx.UTXOs() { + utxoID := expectedUTXO.InputID() + utxo, err := diff.GetUTXO(utxoID) + require.NoError(err) + require.Equal(expectedUTXO, utxo) + } + + sov, err := diff.GetSubnetOnlyValidator(validationID) + require.NoError(err) + + var expectedEndAccumulatedFee uint64 + if test.balance != 0 { + expectedEndAccumulatedFee = test.balance + diff.GetAccruedFees() + } + require.Equal( + state.SubnetOnlyValidator{ + ValidationID: validationID, + SubnetID: subnetID, + NodeID: nodeID, + PublicKey: pkBytes, + RemainingBalanceOwner: remainingBalanceOwnerBytes, + DeactivationOwner: deactivationOwnerBytes, + StartTime: uint64(diff.GetTimestamp().Unix()), + Weight: weight, + MinNonce: 0, + EndAccumulatedFee: expectedEndAccumulatedFee, + }, + sov, + ) + + hasExpiry, err := diff.HasExpiry(state.ExpiryEntry{ + Timestamp: expiry, + ValidationID: validationID, + }) + require.NoError(err) + require.True(hasExpiry) + }) + } +} + +func must[T any](t require.TestingT) func(T, error) T { + return func(val T, err error) T { + require.NoError(t, err) + return val + } +} diff --git a/vms/platformvm/txs/fee/calculator_test.go b/vms/platformvm/txs/fee/calculator_test.go index 5039dc557345..dd71649fecd3 100644 --- a/vms/platformvm/txs/fee/calculator_test.go +++ b/vms/platformvm/txs/fee/calculator_test.go @@ -231,5 +231,17 @@ var ( }, expectedDynamicFee: 365_600, }, + { + name: "RegisterSubnetValidatorTx", + tx: "00000000002400003039000000000000000000000000000000000000000000000000000000000000000000000001dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000007002386f1f88b552a000000000000000000000001000000013cb7d3842e8cee6a0ebd09f1fe884f6861e1b29c00000001ca44ad45a63381b07074be7f82005c41550c989b967f40020f3bedc4b02191f300000000dbcf890f77f49b96857648b72b77f9f82937f28a68704af05da0dc12ba53f2db00000005002386f234262404000000010000000000000000000000003b9aca00ab5cb0516b7afdb13727f766185b2b8da44e2653eef63c85f196701083e649289cce1a23c39eb471b2473bc6872aa3ea190de0fe66296cbdd4132c92c3430ff22f28f0b341b15905a005bbd66cc0f4056bc4be5934e4f3a57151a60060f429190000012f000000003039705f3d4415f990225d3df5ce437d7af2aa324b1bbce854ee34ab6f39882250d20000009c000000000001000000000000008e000000000001a0673b4ee5ec44e57c8ab250dd7cd7b68d04421f64bd6559a4284a3ee358ff2b000000145efc86a11c5b12cc95b2cf527c023f9cf6e0e8f6b62034315c5d11cea4190f6ea8997821c02483d29adb5e4567843f7a44c39b2ffa20c8520dc358702fb1ec29f2746dcc000000006705af280000000000000000000000000000000000000000000000010000000000000001018e99dc6ed736089c03b9a1275e0cf801524ed341fb10111f29c0390fa2f96cf6aa78539ec767e5cd523c606c7ede50e60ba6065a3685e770d979b0df74e3541b61ed63f037463776098576e385767a695de59352b44e515831c5ee7a8cc728f9000000010000000900000001a0950b9e6e866130f0d09e2a7bfdd0246513295237258afa942b1850dab79824605c796bbfc9223cf91935fb29c66f8b927690220b9b1c24d6f078054a3e346201", + expectedStaticFeeErr: ErrUnsupportedTx, + expectedComplexity: gas.Dimensions{ + gas.Bandwidth: 710, // The length of the tx in bytes + gas.DBRead: IntrinsicRegisterSubnetValidatorTxComplexities[gas.DBRead] + intrinsicInputDBRead, + gas.DBWrite: IntrinsicRegisterSubnetValidatorTxComplexities[gas.DBWrite] + intrinsicInputDBWrite + intrinsicOutputDBWrite, + gas.Compute: 0, // TODO: implement + }, + expectedDynamicFee: 151_000, + }, } ) diff --git a/vms/platformvm/txs/fee/complexity.go b/vms/platformvm/txs/fee/complexity.go index e1891a3d51a1..5083296d2936 100644 --- a/vms/platformvm/txs/fee/complexity.go +++ b/vms/platformvm/txs/fee/complexity.go @@ -1,6 +1,8 @@ // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. // See the file LICENSE for licensing terms. +// TODO: Before Etna, address all TODOs in this package and ensure ACP-103 +// compliance. package fee import ( @@ -196,6 +198,15 @@ var ( gas.DBWrite: 2, // manager + weight gas.Compute: 0, } + IntrinsicRegisterSubnetValidatorTxComplexities = gas.Dimensions{ + gas.Bandwidth: IntrinsicBaseTxComplexities[gas.Bandwidth] + + wrappers.LongLen + // balance + bls.SignatureLen + // proof of possession + wrappers.IntLen, // message length + gas.DBRead: 0, // TODO + gas.DBWrite: 0, // TODO + gas.Compute: 0, // TODO: Include PoP verification time + } errUnsupportedOutput = errors.New("unsupported output type") errUnsupportedInput = errors.New("unsupported input type") @@ -436,6 +447,14 @@ func SignerComplexity(s signer.Signer) (gas.Dimensions, error) { } } +// WarpComplexity returns the complexity a warp message adds to a transaction. +func WarpComplexity(message []byte) (gas.Dimensions, error) { + // TODO: Implement me + return gas.Dimensions{ + gas.Bandwidth: uint64(len(message)), + }, nil +} + type complexityVisitor struct { output gas.Dimensions } @@ -686,6 +705,22 @@ func (c *complexityVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return err } +func (c *complexityVisitor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { + baseTxComplexity, err := baseTxComplexity(&tx.BaseTx) + if err != nil { + return err + } + warpComplexity, err := WarpComplexity(tx.Message) + if err != nil { + return err + } + c.output, err = IntrinsicRegisterSubnetValidatorTxComplexities.Add( + &baseTxComplexity, + &warpComplexity, + ) + return err +} + func baseTxComplexity(tx *txs.BaseTx) (gas.Dimensions, error) { outputsComplexity, err := OutputComplexity(tx.Outs...) if err != nil { diff --git a/vms/platformvm/txs/fee/static_calculator.go b/vms/platformvm/txs/fee/static_calculator.go index 1b97349ce2cb..be4651cbb911 100644 --- a/vms/platformvm/txs/fee/static_calculator.go +++ b/vms/platformvm/txs/fee/static_calculator.go @@ -51,6 +51,10 @@ func (*staticVisitor) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return ErrUnsupportedTx } +func (*staticVisitor) RegisterSubnetValidatorTx(*txs.RegisterSubnetValidatorTx) error { + return ErrUnsupportedTx +} + func (c *staticVisitor) AddValidatorTx(*txs.AddValidatorTx) error { c.fee = c.config.AddPrimaryNetworkValidatorFee return nil diff --git a/vms/platformvm/txs/register_subnet_validator_tx.go b/vms/platformvm/txs/register_subnet_validator_tx.go new file mode 100644 index 000000000000..a5a7a0192329 --- /dev/null +++ b/vms/platformvm/txs/register_subnet_validator_tx.go @@ -0,0 +1,45 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/vms/types" +) + +var _ UnsignedTx = (*RegisterSubnetValidatorTx)(nil) + +type RegisterSubnetValidatorTx struct { + // Metadata, inputs and outputs + BaseTx `serialize:"true"` + // Balance <= sum($AVAX inputs) - sum($AVAX outputs) - TxFee. + Balance uint64 `serialize:"true" json:"balance"` + // ProofOfPossession of the BLS key that is included in the Message. + ProofOfPossession [bls.SignatureLen]byte `serialize:"true" json:"proofOfPossession"` + // Message is expected to be a signed Warp message containing an + // AddressedCall payload with the RegisterSubnetValidator message. + Message types.JSONByteSlice `serialize:"true" json:"message"` +} + +func (tx *RegisterSubnetValidatorTx) 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 + } + + tx.SyntacticallyVerified = true + return nil +} + +func (tx *RegisterSubnetValidatorTx) Visit(visitor Visitor) error { + return visitor.RegisterSubnetValidatorTx(tx) +} diff --git a/vms/platformvm/txs/register_subnet_validator_tx_test.go b/vms/platformvm/txs/register_subnet_validator_tx_test.go new file mode 100644 index 000000000000..64055b6171e0 --- /dev/null +++ b/vms/platformvm/txs/register_subnet_validator_tx_test.go @@ -0,0 +1,393 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package txs + +import ( + "encoding/hex" + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + _ "embed" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/snowtest" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" +) + +//go:embed register_subnet_validator_tx_test.json +var registerSubnetValidatorTxJSON []byte + +func TestRegisterSubnetValidatorTxSerialization(t *testing.T) { + require := require.New(t) + + const balance = units.Avax + + skBytes, err := hex.DecodeString("6668fecd4595b81e4d568398c820bbf3f073cb222902279fa55ebb84764ed2e3") + require.NoError(err) + sk, err := bls.SecretKeyFromBytes(skBytes) + require.NoError(err) + + var ( + pop = signer.NewProofOfPossession(sk) + message = []byte("message") + addr = ids.ShortID{ + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + } + avaxAssetID = ids.ID{ + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + } + customAssetID = ids.ID{ + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + } + txID = ids.ID{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + } + ) + + var unsignedTx UnsignedTx = &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.UnitTestID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 87654321, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 12345678, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + }, + { + Asset: avax.Asset{ + ID: customAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 876543210, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 0xffffffffffffffff, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + }, + }, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.Avax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{2, 5}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 2, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &stakeable.LockIn{ + Locktime: 876543210, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 0xefffffffffffffff, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 3, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 0x1000000000000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }, + }, + Memo: types.JSONByteSlice("😅\nwell that's\x01\x23\x45!"), + }, + }, + Balance: balance, + ProofOfPossession: pop.ProofOfPossession, + Message: message, + } + txBytes, err := Codec.Marshal(CodecVersion, &unsignedTx) + require.NoError(err) + + expectedBytes := []byte{ + // Codec version + 0x00, 0x00, + // RegisterSubnetValidatorTx Type ID + 0x00, 0x00, 0x00, 0x24, + // Network ID + 0x00, 0x00, 0x00, 0x0a, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x02, + // Outputs[0] + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x05, 0x39, 0x7f, 0xb1, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x61, 0x4e, + // threshold + 0x00, 0x00, 0x00, 0x00, + // number of addresses + 0x00, 0x00, 0x00, 0x00, + // Outputs[1] + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // threshold + 0x00, 0x00, 0x00, 0x01, + // number of addresses + 0x00, 0x00, 0x00, 0x01, + // address[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + // number of inputs + 0x00, 0x00, 0x00, 0x03, + // inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 Avax + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x02, + // index of first signer + 0x00, 0x00, 0x00, 0x02, + // index of second signer + 0x00, 0x00, 0x00, 0x05, + // inputs[1] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x02, + // Custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked input type ID + 0x00, 0x00, 0x00, 0x15, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x00, + // inputs[2] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x03, + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x00, + // length of memo + 0x00, 0x00, 0x00, 0x14, + // memo + 0xf0, 0x9f, 0x98, 0x85, 0x0a, 0x77, 0x65, 0x6c, + 0x6c, 0x20, 0x74, 0x68, 0x61, 0x74, 0x27, 0x73, + 0x01, 0x23, 0x45, 0x21, + // balance + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // proof of possession + 0x8c, 0xfd, 0x79, 0x09, 0xd1, 0x53, 0xb9, 0x60, + 0x4b, 0x62, 0xb1, 0x43, 0xba, 0x36, 0x20, 0x7b, + 0xb7, 0xe6, 0x48, 0x67, 0x42, 0x44, 0x80, 0x20, + 0x2a, 0x67, 0xdc, 0x68, 0x76, 0x83, 0x46, 0xd9, + 0x5c, 0x90, 0x98, 0x3c, 0x2d, 0x27, 0x9c, 0x64, + 0xc4, 0x3c, 0x51, 0x13, 0x6b, 0x2a, 0x05, 0xe0, + 0x16, 0x02, 0xd5, 0x2a, 0xa6, 0x37, 0x6f, 0xda, + 0x17, 0xfa, 0x6e, 0x2a, 0x18, 0xa0, 0x83, 0xe4, + 0x9d, 0x9c, 0x45, 0x0e, 0xab, 0x7b, 0x89, 0xb1, + 0xd5, 0x55, 0x5d, 0xa5, 0xc4, 0x89, 0x87, 0x2e, + 0x02, 0xb7, 0xe5, 0x22, 0x7b, 0x77, 0x55, 0x0a, + 0xf1, 0x33, 0x0e, 0x5a, 0x71, 0xf8, 0xc3, 0x68, + // length of message + 0x00, 0x00, 0x00, 0x07, + // message + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + } + require.Equal(expectedBytes, txBytes) + + ctx := snowtest.Context(t, constants.PlatformChainID) + unsignedTx.InitCtx(ctx) + + txJSON, err := json.MarshalIndent(unsignedTx, "", "\t") + require.NoError(err) + require.Equal( + // Normalize newlines for Windows + strings.ReplaceAll(string(registerSubnetValidatorTxJSON), "\r\n", "\n"), + string(txJSON), + ) +} + +func TestRegisterSubnetValidatorTxSyntacticVerify(t *testing.T) { + ctx := snowtest.Context(t, ids.GenerateTestID()) + tests := []struct { + name string + tx *RegisterSubnetValidatorTx + expectedErr error + }{ + { + name: "nil tx", + tx: nil, + expectedErr: ErrNilTx, + }, + { + name: "already verified", + // The tx includes invalid data to verify that a cached result is + // returned. + tx: &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{ + SyntacticallyVerified: true, + }, + }, + expectedErr: nil, + }, + { + name: "invalid BaseTx", + tx: &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{}, + }, + expectedErr: avax.ErrWrongNetworkID, + }, + { + name: "passes verification", + tx: &RegisterSubnetValidatorTx{ + BaseTx: BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }, + }, + }, + expectedErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require := require.New(t) + + err := test.tx.SyntacticVerify(ctx) + require.ErrorIs(err, test.expectedErr) + if test.expectedErr != nil { + return + } + require.True(test.tx.SyntacticallyVerified) + }) + } +} diff --git a/vms/platformvm/txs/register_subnet_validator_tx_test.json b/vms/platformvm/txs/register_subnet_validator_tx_test.json new file mode 100644 index 000000000000..f741e2fdbb37 --- /dev/null +++ b/vms/platformvm/txs/register_subnet_validator_tx_test.json @@ -0,0 +1,175 @@ +{ + "networkID": 10, + "blockchainID": "11111111111111111111111111111111LpoYY", + "outputs": [ + { + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 87654321, + "output": { + "addresses": [], + "amount": 1, + "locktime": 12345678, + "threshold": 0 + } + } + }, + { + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 876543210, + "output": { + "addresses": [ + "P-testing1g32kvaugnx4tk3z4vemc3xd2hdz92enhgrdu9n" + ], + "amount": 18446744073709551615, + "locktime": 0, + "threshold": 1 + } + } + } + ], + "inputs": [ + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1000000000, + "signatureIndices": [ + 2, + 5 + ] + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 2, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 876543210, + "input": { + "amount": 17293822569102704639, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 3, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1152921504606846976, + "signatureIndices": [] + } + } + ], + "memo": "0xf09f98850a77656c6c2074686174277301234521", + "balance": 1000000000, + "proofOfPossession": [ + 140, + 253, + 121, + 9, + 209, + 83, + 185, + 96, + 75, + 98, + 177, + 67, + 186, + 54, + 32, + 123, + 183, + 230, + 72, + 103, + 66, + 68, + 128, + 32, + 42, + 103, + 220, + 104, + 118, + 131, + 70, + 217, + 92, + 144, + 152, + 60, + 45, + 39, + 156, + 100, + 196, + 60, + 81, + 19, + 107, + 42, + 5, + 224, + 22, + 2, + 213, + 42, + 166, + 55, + 111, + 218, + 23, + 250, + 110, + 42, + 24, + 160, + 131, + 228, + 157, + 156, + 69, + 14, + 171, + 123, + 137, + 177, + 213, + 85, + 93, + 165, + 196, + 137, + 135, + 46, + 2, + 183, + 229, + 34, + 123, + 119, + 85, + 10, + 241, + 51, + 14, + 90, + 113, + 248, + 195, + 104 + ], + "message": "0x6d657373616765" +} \ No newline at end of file diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 21c46476fa5f..02627c198f16 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -28,4 +28,5 @@ type Visitor interface { // Etna Transactions: ConvertSubnetTx(*ConvertSubnetTx) error + RegisterSubnetValidatorTx(*RegisterSubnetValidatorTx) error } diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index bd6fecce96ef..be37e6ee73e2 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -26,10 +26,18 @@ import ( ) const ( - validatorSetsCacheSize = 64 - maxRecentlyAcceptedWindowSize = 64 - minRecentlyAcceptedWindowSize = 0 - recentlyAcceptedWindowTTL = 30 * time.Second + // MaxRecentlyAcceptedWindowSize is the maximum number of blocks that the + // recommended minimum height will lag behind the last accepted block. + MaxRecentlyAcceptedWindowSize = 64 + // MinRecentlyAcceptedWindowSize is the minimum number of blocks that the + // recommended minimum height will lag behind the last accepted block. + MinRecentlyAcceptedWindowSize = 0 + // RecentlyAcceptedWindowTTL is the amount of time after a block is accepted + // to avoid recommending it as the minimum height. The size constraints take + // precedence over this time constraint. + RecentlyAcceptedWindowTTL = 30 * time.Second + + validatorSetsCacheSize = 64 ) var ( @@ -111,9 +119,9 @@ func NewManager( recentlyAccepted: window.New[ids.ID]( window.Config{ Clock: clk, - MaxSize: maxRecentlyAcceptedWindowSize, - MinSize: minRecentlyAcceptedWindowSize, - TTL: recentlyAcceptedWindowTTL, + MaxSize: MaxRecentlyAcceptedWindowSize, + MinSize: MinRecentlyAcceptedWindowSize, + TTL: RecentlyAcceptedWindowTTL, }, ), } diff --git a/vms/platformvm/warp/payload/addressed_call.go b/vms/platformvm/warp/payload/addressed_call.go index b3617ce487da..9950be42ec0d 100644 --- a/vms/platformvm/warp/payload/addressed_call.go +++ b/vms/platformvm/warp/payload/addressed_call.go @@ -37,7 +37,7 @@ func ParseAddressedCall(b []byte) (*AddressedCall, error) { } payload, ok := payloadIntf.(*AddressedCall) if !ok { - return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf) + return nil, fmt.Errorf("%w: %T", ErrWrongType, payloadIntf) } return payload, nil } diff --git a/vms/platformvm/warp/payload/hash.go b/vms/platformvm/warp/payload/hash.go index 330f74fd869d..73804d169bdf 100644 --- a/vms/platformvm/warp/payload/hash.go +++ b/vms/platformvm/warp/payload/hash.go @@ -33,7 +33,7 @@ func ParseHash(b []byte) (*Hash, error) { } payload, ok := payloadIntf.(*Hash) if !ok { - return nil, fmt.Errorf("%w: %T", errWrongType, payloadIntf) + return nil, fmt.Errorf("%w: %T", ErrWrongType, payloadIntf) } return payload, nil } diff --git a/vms/platformvm/warp/payload/payload.go b/vms/platformvm/warp/payload/payload.go index c5c09464803e..0f7831c6b343 100644 --- a/vms/platformvm/warp/payload/payload.go +++ b/vms/platformvm/warp/payload/payload.go @@ -8,7 +8,7 @@ import ( "fmt" ) -var errWrongType = errors.New("wrong payload type") +var ErrWrongType = errors.New("wrong payload type") // Payload provides a common interface for all payloads implemented by this // package. diff --git a/vms/platformvm/warp/payload/payload_test.go b/vms/platformvm/warp/payload/payload_test.go index 86b584ae33db..6ae1fcc09c3c 100644 --- a/vms/platformvm/warp/payload/payload_test.go +++ b/vms/platformvm/warp/payload/payload_test.go @@ -33,10 +33,10 @@ func TestParseWrongPayloadType(t *testing.T) { require.NoError(err) _, err = ParseAddressedCall(hashPayload.Bytes()) - require.ErrorIs(err, errWrongType) + require.ErrorIs(err, ErrWrongType) _, err = ParseHash(addressedPayload.Bytes()) - require.ErrorIs(err, errWrongType) + require.ErrorIs(err, ErrWrongType) } func TestParse(t *testing.T) { diff --git a/wallet/chain/p/builder/builder.go b/wallet/chain/p/builder/builder.go index e8762d7b2a68..8a429fbe24c3 100644 --- a/wallet/chain/p/builder/builder.go +++ b/wallet/chain/p/builder/builder.go @@ -12,6 +12,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/math" "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -164,6 +165,20 @@ type Builder interface { options ...common.Option, ) (*txs.ConvertSubnetTx, error) + // RegisterSubnetValidatorTx adds a validator to an L1. + // + // - [balance] that the validator should allocate to continuous fees + // - [proofOfPossession] is the BLS PoP for the key included in the Warp + // message + // - [message] is the Warp message that authorizes this validator to be + // added + NewRegisterSubnetValidatorTx( + balance uint64, + proofOfPossession [bls.SignatureLen]byte, + message []byte, + options ...common.Option, + ) (*txs.RegisterSubnetValidatorTx, error) + // NewImportTx creates an import transaction that attempts to consume all // the available UTXOs and import the funds to [to]. // @@ -863,6 +878,63 @@ func (b *builder) NewConvertSubnetTx( return tx, b.initCtx(tx) } +func (b *builder) NewRegisterSubnetValidatorTx( + balance uint64, + proofOfPossession [bls.SignatureLen]byte, + message []byte, + options ...common.Option, +) (*txs.RegisterSubnetValidatorTx, error) { + var ( + toBurn = map[ids.ID]uint64{ + b.context.AVAXAssetID: balance, + } + toStake = map[ids.ID]uint64{} + + ops = common.NewOptions(options) + memo = ops.Memo() + memoComplexity = gas.Dimensions{ + gas.Bandwidth: uint64(len(memo)), + } + ) + warpComplexity, err := fee.WarpComplexity(message) + if err != nil { + return nil, err + } + complexity, err := fee.IntrinsicRegisterSubnetValidatorTxComplexities.Add( + &memoComplexity, + &warpComplexity, + ) + if err != nil { + return nil, err + } + + inputs, outputs, _, err := b.spend( + toBurn, + toStake, + 0, + complexity, + nil, + ops, + ) + if err != nil { + return nil, err + } + + tx := &txs.RegisterSubnetValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: b.context.NetworkID, + BlockchainID: constants.PlatformChainID, + Ins: inputs, + Outs: outputs, + Memo: memo, + }}, + Balance: balance, + ProofOfPossession: proofOfPossession, + Message: message, + } + return tx, b.initCtx(tx) +} + func (b *builder) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/builder/with_options.go b/wallet/chain/p/builder/with_options.go index e83683b89687..89fb35e9bd26 100644 --- a/wallet/chain/p/builder/with_options.go +++ b/wallet/chain/p/builder/with_options.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -170,6 +171,20 @@ func (w *withOptions) NewConvertSubnetTx( ) } +func (w *withOptions) NewRegisterSubnetValidatorTx( + balance uint64, + proofOfPossession [bls.SignatureLen]byte, + message []byte, + options ...common.Option, +) (*txs.RegisterSubnetValidatorTx, error) { + return w.builder.NewRegisterSubnetValidatorTx( + balance, + proofOfPossession, + message, + common.UnionOptions(w.options, options)..., + ) +} + func (w *withOptions) NewImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/builder_test.go b/wallet/chain/p/builder_test.go index f56a0eb0896b..bc03f30814e9 100644 --- a/wallet/chain/p/builder_test.go +++ b/wallet/chain/p/builder_test.go @@ -26,7 +26,9 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/platformvm/txs/fee" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/vms/types" "github.com/ava-labs/avalanchego/wallet/chain/p/builder" @@ -747,6 +749,106 @@ func TestConvertSubnetTx(t *testing.T) { } } +func TestRegisterSubnetValidatorTx(t *testing.T) { + const ( + expiry = 1731005097 + weight = 7905001371 + + balance = units.Avax + ) + + sk, err := bls.NewSecretKey() + require.NoError(t, err) + pop := signer.NewProofOfPossession(sk) + + addressedCallPayload, err := message.NewRegisterSubnetValidator( + subnetID, + nodeID, + pop.PublicKey, + expiry, + message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + message.PChainOwner{ + Threshold: 1, + Addresses: []ids.ShortID{ + ids.GenerateTestShortID(), + }, + }, + weight, + ) + require.NoError(t, err) + + addressedCall, err := payload.NewAddressedCall( + utils.RandomBytes(20), + addressedCallPayload.Bytes(), + ) + require.NoError(t, err) + + unsignedWarp, err := warp.NewUnsignedMessage( + constants.UnitTestID, + ids.GenerateTestID(), + addressedCall.Bytes(), + ) + require.NoError(t, err) + + signers := set.NewBits(0) + + unsignedBytes := unsignedWarp.Bytes() + sig := bls.Sign(sk, unsignedBytes) + sigBytes := [bls.SignatureLen]byte{} + copy(sigBytes[:], bls.SignatureToBytes(sig)) + + warp, err := warp.NewMessage( + unsignedWarp, + &warp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: sigBytes, + }, + ) + require.NoError(t, err) + warpMessageBytes := warp.Bytes() + + for _, e := range testEnvironmentPostEtna { + t.Run(e.name, func(t *testing.T) { + var ( + require = require.New(t) + chainUTXOs = utxotest.NewDeterministicChainUTXOs(t, map[ids.ID][]*avax.UTXO{ + constants.PlatformChainID: utxos, + }) + backend = wallet.NewBackend(e.context, chainUTXOs, nil) + builder = builder.New(set.Of(utxoAddr), e.context, backend) + ) + + utx, err := builder.NewRegisterSubnetValidatorTx( + balance, + pop.ProofOfPossession, + warpMessageBytes, + common.WithMemo(e.memo), + ) + require.NoError(err) + require.Equal(balance, utx.Balance) + require.Equal(pop.ProofOfPossession, utx.ProofOfPossession) + require.Equal(types.JSONByteSlice(warpMessageBytes), utx.Message) + require.Equal(types.JSONByteSlice(e.memo), utx.Memo) + requireFeeIsCorrect( + require, + e.feeCalculator, + utx, + &utx.BaseTx.BaseTx, + nil, + nil, + map[ids.ID]uint64{ + e.context.AVAXAssetID: balance, // Balance of the validator + }, + ) + }) + } +} + func makeTestUTXOs(utxosKey *secp256k1.PrivateKey) []*avax.UTXO { // Note: we avoid ids.GenerateTestNodeID here to make sure that UTXO IDs // won't change run by run. This simplifies checking what utxos are included diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index b358e1f5d5ea..bb4f6d37f87f 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -198,6 +198,14 @@ func (s *visitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return sign(s.tx, true, txSigners) } +func (s *visitor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + return sign(s.tx, true, txSigners) +} + func (s *visitor) getSigners(sourceChainID ids.ID, ins []*avax.TransferableInput) ([][]keychain.Signer, error) { txSigners := make([][]keychain.Signer, len(ins)) for credIndex, transferInput := range ins { diff --git a/wallet/chain/p/wallet/backend_visitor.go b/wallet/chain/p/wallet/backend_visitor.go index f2f9e646edf8..733eaa505259 100644 --- a/wallet/chain/p/wallet/backend_visitor.go +++ b/wallet/chain/p/wallet/backend_visitor.go @@ -123,6 +123,10 @@ func (b *backendVisitor) ConvertSubnetTx(tx *txs.ConvertSubnetTx) error { return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { + return b.baseTx(&tx.BaseTx) +} + func (b *backendVisitor) baseTx(tx *txs.BaseTx) error { return b.b.removeUTXOs( b.ctx, diff --git a/wallet/chain/p/wallet/wallet.go b/wallet/chain/p/wallet/wallet.go index a36522cc6b3a..fd6fd9a782e0 100644 --- a/wallet/chain/p/wallet/wallet.go +++ b/wallet/chain/p/wallet/wallet.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -150,6 +151,21 @@ type Wallet interface { options ...common.Option, ) (*txs.Tx, error) + // IssueRegisterSubnetValidatorTx creates, signs, and issues a transaction + // that adds a validator to an L1. + // + // - [balance] that the validator should allocate to continuous fees + // - [proofOfPossession] is the BLS PoP for the key included in the Warp + // message + // - [message] is the Warp message that authorizes this validator to be + // added + IssueRegisterSubnetValidatorTx( + balance uint64, + proofOfPossession [bls.SignatureLen]byte, + message []byte, + options ...common.Option, + ) (*txs.Tx, error) + // IssueImportTx creates, signs, and issues an import transaction that // attempts to consume all the available UTXOs and import the funds to [to]. // @@ -404,6 +420,19 @@ func (w *wallet) IssueConvertSubnetTx( return w.IssueUnsignedTx(utx, options...) } +func (w *wallet) IssueRegisterSubnetValidatorTx( + balance uint64, + proofOfPossession [bls.SignatureLen]byte, + message []byte, + options ...common.Option, +) (*txs.Tx, error) { + utx, err := w.builder.NewRegisterSubnetValidatorTx(balance, proofOfPossession, message, options...) + if err != nil { + return nil, err + } + return w.IssueUnsignedTx(utx, options...) +} + func (w *wallet) IssueImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/chain/p/wallet/with_options.go b/wallet/chain/p/wallet/with_options.go index e73d5a3215ee..c3e9527e9478 100644 --- a/wallet/chain/p/wallet/with_options.go +++ b/wallet/chain/p/wallet/with_options.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -159,6 +160,20 @@ func (w *withOptions) IssueConvertSubnetTx( ) } +func (w *withOptions) IssueRegisterSubnetValidatorTx( + balance uint64, + proofOfPossession [bls.SignatureLen]byte, + message []byte, + options ...common.Option, +) (*txs.Tx, error) { + return w.wallet.IssueRegisterSubnetValidatorTx( + balance, + proofOfPossession, + message, + common.UnionOptions(w.options, options)..., + ) +} + func (w *withOptions) IssueImportTx( sourceChainID ids.ID, to *secp256k1fx.OutputOwners, diff --git a/wallet/subnet/primary/examples/register-subnet-validator/main.go b/wallet/subnet/primary/examples/register-subnet-validator/main.go new file mode 100644 index 000000000000..68248a7e18eb --- /dev/null +++ b/wallet/subnet/primary/examples/register-subnet-validator/main.go @@ -0,0 +1,141 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "context" + "encoding/hex" + "encoding/json" + "log" + "time" + + "github.com/ava-labs/avalanchego/api/info" + "github.com/ava-labs/avalanchego/genesis" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/message" + "github.com/ava-labs/avalanchego/vms/platformvm/warp/payload" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" +) + +func main() { + key := genesis.EWOQKey + uri := primary.LocalAPIURI + kc := secp256k1fx.NewKeychain(key) + subnetID := ids.FromStringOrPanic("2DeHa7Qb6sufPkmQcFWG2uCd4pBPv9WB6dkzroiMQhd1NSRtof") + chainID := ids.FromStringOrPanic("2BMFrJ9xeh5JdwZEx6uuFcjfZC2SV2hdbMT8ee5HrvjtfJb5br") + address := []byte{} + weight := uint64(1) + blsSKHex := "3f783929b295f16cd1172396acb23b20eed057b9afb1caa419e9915f92860b35" + + blsSKBytes, err := hex.DecodeString(blsSKHex) + if err != nil { + log.Fatalf("failed to decode secret key: %s\n", err) + } + + sk, err := bls.SecretKeyFromBytes(blsSKBytes) + if err != nil { + log.Fatalf("failed to parse secret key: %s\n", err) + } + + ctx := context.Background() + infoClient := info.NewClient(uri) + + nodeInfoStartTime := time.Now() + nodeID, nodePoP, err := infoClient.GetNodeID(ctx) + if err != nil { + log.Fatalf("failed to fetch node IDs: %s\n", err) + } + log.Printf("fetched node ID %s in %s\n", nodeID, time.Since(nodeInfoStartTime)) + + // MakeWallet fetches the available UTXOs owned by [kc] on the network that + // [uri] is hosting. + walletSyncStartTime := time.Now() + wallet, err := primary.MakeWallet(ctx, &primary.WalletConfig{ + URI: uri, + AVAXKeychain: kc, + EthKeychain: kc, + }) + if err != nil { + log.Fatalf("failed to initialize wallet: %s\n", err) + } + log.Printf("synced wallet in %s\n", time.Since(walletSyncStartTime)) + + // Get the P-chain wallet + pWallet := wallet.P() + context := pWallet.Builder().Context() + + expiry := uint64(time.Now().Add(5 * time.Minute).Unix()) // This message will expire in 5 minutes + addressedCallPayload, err := message.NewRegisterSubnetValidator( + subnetID, + nodeID, + nodePoP.PublicKey, + expiry, + message.PChainOwner{}, + message.PChainOwner{}, + weight, + ) + if err != nil { + log.Fatalf("failed to create RegisterSubnetValidator message: %s\n", err) + } + addressedCallPayloadJSON, err := json.MarshalIndent(addressedCallPayload, "", "\t") + if err != nil { + log.Fatalf("failed to marshal RegisterSubnetValidator message: %s\n", err) + } + log.Println(string(addressedCallPayloadJSON)) + + addressedCall, err := payload.NewAddressedCall( + address, + addressedCallPayload.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create AddressedCall message: %s\n", err) + } + + unsignedWarp, err := warp.NewUnsignedMessage( + context.NetworkID, + chainID, + addressedCall.Bytes(), + ) + if err != nil { + log.Fatalf("failed to create unsigned Warp message: %s\n", err) + } + + // This example assumes that the hard-coded BLS key is for the first + // validator in the signature bit-set. + signers := set.NewBits(0) + + unsignedBytes := unsignedWarp.Bytes() + sig := bls.Sign(sk, unsignedBytes) + sigBytes := [bls.SignatureLen]byte{} + copy(sigBytes[:], bls.SignatureToBytes(sig)) + + warp, err := warp.NewMessage( + unsignedWarp, + &warp.BitSetSignature{ + Signers: signers.Bytes(), + Signature: sigBytes, + }, + ) + if err != nil { + log.Fatalf("failed to create Warp message: %s\n", err) + } + + registerSubnetValidatorStartTime := time.Now() + registerSubnetValidatorTx, err := pWallet.IssueRegisterSubnetValidatorTx( + units.Avax, + nodePoP.ProofOfPossession, + warp.Bytes(), + ) + if err != nil { + log.Fatalf("failed to issue register subnet validator transaction: %s\n", err) + } + + validationID := addressedCallPayload.ValidationID() + log.Printf("registered new subnet validator %s to subnet %s with txID %s as validationID %s in %s\n", nodeID, subnetID, registerSubnetValidatorTx.ID(), validationID, time.Since(registerSubnetValidatorStartTime)) +} From abae44142ea346faa0623f093fab27b8d6abddc4 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 15:37:49 -0500 Subject: [PATCH 183/184] nits from PR review --- vms/platformvm/block/builder/builder.go | 14 +++++++------- vms/platformvm/block/executor/block.go | 25 ++++++++++++------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/vms/platformvm/block/builder/builder.go b/vms/platformvm/block/builder/builder.go index 86fe2ab3beaa..410d3285a9e7 100644 --- a/vms/platformvm/block/builder/builder.go +++ b/vms/platformvm/block/builder/builder.go @@ -211,7 +211,12 @@ func (b *builder) ShutdownBlockTimer() { } func (b *builder) BuildBlock(ctx context.Context) (snowman.Block, error) { - return b.BuildBlockWithContext(ctx, nil) + return b.BuildBlockWithContext( + ctx, + &smblock.Context{ + PChainHeight: 0, + }, + ) } func (b *builder) BuildBlockWithContext( @@ -245,11 +250,6 @@ func (b *builder) BuildBlockWithContext( return nil, fmt.Errorf("could not calculate next staker change time: %w", err) } - var pChainHeight uint64 - if blockContext != nil { - pChainHeight = blockContext.PChainHeight - } - statelessBlk, err := buildBlock( ctx, b, @@ -258,7 +258,7 @@ func (b *builder) BuildBlockWithContext( timestamp, timeWasCapped, preferredState, - pChainHeight, + blockContext.PChainHeight, ) if err != nil { return nil, err diff --git a/vms/platformvm/block/executor/block.go b/vms/platformvm/block/executor/block.go index 3a8f459915fd..0c3cffc89954 100644 --- a/vms/platformvm/block/executor/block.go +++ b/vms/platformvm/block/executor/block.go @@ -30,29 +30,23 @@ func (*Block) ShouldVerifyWithContext(context.Context) (bool, error) { } func (b *Block) VerifyWithContext(ctx context.Context, blockContext *smblock.Context) error { - var pChainHeight uint64 - if blockContext != nil { - pChainHeight = blockContext.PChainHeight - } - blkID := b.ID() if blkState, ok := b.manager.blkIDToState[blkID]; ok { - if !blkState.verifiedHeights.Contains(pChainHeight) { + if !blkState.verifiedHeights.Contains(blockContext.PChainHeight) { // Only the validity of warp messages need to be verified because - // this block has already passed verification with a different - // height. + // this block was already executed with a different block context. err := VerifyWarpMessages( ctx, b.manager.ctx.NetworkID, b.manager.ctx.ValidatorState, - pChainHeight, + blockContext.PChainHeight, b, ) if err != nil { return err } - blkState.verifiedHeights.Add(pChainHeight) + blkState.verifiedHeights.Add(blockContext.PChainHeight) } return nil // This block has already been executed. @@ -63,7 +57,7 @@ func (b *Block) VerifyWithContext(ctx context.Context, blockContext *smblock.Con ctx, b.manager.ctx.NetworkID, b.manager.ctx.ValidatorState, - pChainHeight, + blockContext.PChainHeight, b, ) if err != nil { @@ -75,12 +69,17 @@ func (b *Block) VerifyWithContext(ctx context.Context, blockContext *smblock.Con return b.Visit(&verifier{ backend: b.manager.backend, txExecutorBackend: b.manager.txExecutorBackend, - pChainHeight: pChainHeight, + pChainHeight: blockContext.PChainHeight, }) } func (b *Block) Verify(ctx context.Context) error { - return b.VerifyWithContext(ctx, nil) + return b.VerifyWithContext( + ctx, + &smblock.Context{ + PChainHeight: 0, + }, + ) } func (b *Block) Accept(context.Context) error { From b3e1095d11d1b86084922671be997cdec5f203d3 Mon Sep 17 00:00:00 2001 From: Stephen Buttolph Date: Fri, 8 Nov 2024 15:43:42 -0500 Subject: [PATCH 184/184] unexport visitor --- vms/platformvm/txs/executor/warp_verifier.go | 68 ++++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/vms/platformvm/txs/executor/warp_verifier.go b/vms/platformvm/txs/executor/warp_verifier.go index 5c7430629f32..731cac7b3827 100644 --- a/vms/platformvm/txs/executor/warp_verifier.go +++ b/vms/platformvm/txs/executor/warp_verifier.go @@ -16,7 +16,7 @@ const ( WarpQuorumDenominator = 100 ) -var _ txs.Visitor = (*WarpVerifier)(nil) +var _ txs.Visitor = (*warpVerifier)(nil) // VerifyWarpMessages verifies all warp messages in the tx. If any of the warp // messages are invalid, an error is returned. @@ -27,105 +27,105 @@ func VerifyWarpMessages( pChainHeight uint64, tx txs.UnsignedTx, ) error { - return tx.Visit(&WarpVerifier{ - Context: ctx, - NetworkID: networkID, - ValidatorState: validatorState, - PChainHeight: pChainHeight, + return tx.Visit(&warpVerifier{ + context: ctx, + networkID: networkID, + validatorState: validatorState, + pChainHeight: pChainHeight, }) } -type WarpVerifier struct { - Context context.Context - NetworkID uint32 - ValidatorState validators.State - PChainHeight uint64 +type warpVerifier struct { + context context.Context + networkID uint32 + validatorState validators.State + pChainHeight uint64 } -func (*WarpVerifier) AddValidatorTx(*txs.AddValidatorTx) error { +func (*warpVerifier) AddValidatorTx(*txs.AddValidatorTx) error { return nil } -func (*WarpVerifier) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { +func (*warpVerifier) AddSubnetValidatorTx(*txs.AddSubnetValidatorTx) error { return nil } -func (*WarpVerifier) AddDelegatorTx(*txs.AddDelegatorTx) error { +func (*warpVerifier) AddDelegatorTx(*txs.AddDelegatorTx) error { return nil } -func (*WarpVerifier) CreateChainTx(*txs.CreateChainTx) error { +func (*warpVerifier) CreateChainTx(*txs.CreateChainTx) error { return nil } -func (*WarpVerifier) CreateSubnetTx(*txs.CreateSubnetTx) error { +func (*warpVerifier) CreateSubnetTx(*txs.CreateSubnetTx) error { return nil } -func (*WarpVerifier) ImportTx(*txs.ImportTx) error { +func (*warpVerifier) ImportTx(*txs.ImportTx) error { return nil } -func (*WarpVerifier) ExportTx(*txs.ExportTx) error { +func (*warpVerifier) ExportTx(*txs.ExportTx) error { return nil } -func (*WarpVerifier) AdvanceTimeTx(*txs.AdvanceTimeTx) error { +func (*warpVerifier) AdvanceTimeTx(*txs.AdvanceTimeTx) error { return nil } -func (*WarpVerifier) RewardValidatorTx(*txs.RewardValidatorTx) error { +func (*warpVerifier) RewardValidatorTx(*txs.RewardValidatorTx) error { return nil } -func (*WarpVerifier) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { +func (*warpVerifier) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error { return nil } -func (*WarpVerifier) TransformSubnetTx(*txs.TransformSubnetTx) error { +func (*warpVerifier) TransformSubnetTx(*txs.TransformSubnetTx) error { return nil } -func (*WarpVerifier) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { +func (*warpVerifier) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { return nil } -func (*WarpVerifier) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { +func (*warpVerifier) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error { return nil } -func (*WarpVerifier) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { +func (*warpVerifier) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error { return nil } -func (*WarpVerifier) BaseTx(*txs.BaseTx) error { +func (*warpVerifier) BaseTx(*txs.BaseTx) error { return nil } -func (*WarpVerifier) ConvertSubnetTx(*txs.ConvertSubnetTx) error { +func (*warpVerifier) ConvertSubnetTx(*txs.ConvertSubnetTx) error { return nil } -func (w *WarpVerifier) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { +func (w *warpVerifier) RegisterSubnetValidatorTx(tx *txs.RegisterSubnetValidatorTx) error { return w.verify(tx.Message) } -func (w *WarpVerifier) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWeightTx) error { +func (w *warpVerifier) SetSubnetValidatorWeightTx(tx *txs.SetSubnetValidatorWeightTx) error { return w.verify(tx.Message) } -func (w *WarpVerifier) verify(message []byte) error { +func (w *warpVerifier) verify(message []byte) error { msg, err := warp.ParseMessage(message) if err != nil { return err } return msg.Signature.Verify( - w.Context, + w.context, &msg.UnsignedMessage, - w.NetworkID, - w.ValidatorState, - w.PChainHeight, + w.networkID, + w.validatorState, + w.pChainHeight, WarpQuorumNumerator, WarpQuorumDenominator, )