diff --git a/vms/platformvm/block/executor/proposal_block_test.go b/vms/platformvm/block/executor/proposal_block_test.go index f0037754d06a..1fe3b9e5bd1c 100644 --- a/vms/platformvm/block/executor/proposal_block_test.go +++ b/vms/platformvm/block/executor/proposal_block_test.go @@ -98,6 +98,7 @@ func TestApricotProposalBlockTimeVerification(t *testing.T) { StartTime: utx.StartTime(), NextTime: chainTime, EndTime: chainTime, + Priority: utx.CurrentPriority(), }).Times(2) currentStakersIt.EXPECT().Release() onParentAccept.EXPECT().GetCurrentStakerIterator().Return(currentStakersIt, nil) diff --git a/vms/platformvm/service.go b/vms/platformvm/service.go index d1bdf60a6529..d7ffa80bc29e 100644 --- a/vms/platformvm/service.go +++ b/vms/platformvm/service.go @@ -29,9 +29,7 @@ import ( "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/components/keystore" - "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/platformvm/reward" - "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/state" "github.com/ava-labs/avalanchego/vms/platformvm/status" @@ -69,16 +67,7 @@ var ( type Service struct { vm *VM addrManager avax.AddressManager - stakerAttributesCache *cache.LRU[ids.ID, *stakerAttributes] -} - -// All attributes are optional and may not be filled for each stakerTx. -type stakerAttributes struct { - shares uint32 - rewardsOwner fx.Owner - validationRewardsOwner fx.Owner - delegationRewardsOwner fx.Owner - proofOfPossession *signer.ProofOfPossession + stakerAttributesCache *cache.LRU[ids.ID, *state.StakerRewardAttributes] } // GetHeight returns the height of the last accepted block @@ -698,44 +687,19 @@ type GetCurrentValidatorsReply struct { Validators []interface{} `json:"validators"` } -func (s *Service) loadStakerTxAttributes(txID ids.ID) (*stakerAttributes, error) { - // Lookup tx from the cache first. +func (s *Service) loadStakerTxAttributes(txID ids.ID) (*state.StakerRewardAttributes, error) { + // Lookup attributes from the cache first. attr, found := s.stakerAttributesCache.Get(txID) if found { return attr, nil } - // Tx not available in cache; pull it from disk and populate the cache. - tx, _, err := s.vm.state.GetTx(txID) + // attributes not available in cache; pull them from disk and populate the cache. + attr, err := s.vm.state.GetStakerRewardAttributes(txID) if err != nil { return nil, err } - switch stakerTx := tx.Unsigned.(type) { - case txs.ValidatorTx: - var pop *signer.ProofOfPossession - if staker, ok := stakerTx.(*txs.AddPermissionlessValidatorTx); ok { - if s, ok := staker.Signer.(*signer.ProofOfPossession); ok { - pop = s - } - } - - attr = &stakerAttributes{ - shares: stakerTx.Shares(), - validationRewardsOwner: stakerTx.ValidationRewardsOwner(), - delegationRewardsOwner: stakerTx.DelegationRewardsOwner(), - proofOfPossession: pop, - } - - case txs.DelegatorTx: - attr = &stakerAttributes{ - rewardsOwner: stakerTx.RewardsOwner(), - } - - default: - return nil, fmt.Errorf("unexpected staker tx type %T", tx.Unsigned) - } - s.stakerAttributesCache.Put(txID, attr) return attr, nil } @@ -828,7 +792,7 @@ func (s *Service) GetCurrentValidators(_ *http.Request, args *GetCurrentValidato return err } - shares := attr.shares + shares := attr.Shares delegationFee := avajson.Float32(100 * float32(shares) / float32(reward.PercentDenominator)) uptime, err := s.getAPIUptime(currentStaker) @@ -841,14 +805,14 @@ func (s *Service) GetCurrentValidators(_ *http.Request, args *GetCurrentValidato validationRewardOwner *platformapi.Owner delegationRewardOwner *platformapi.Owner ) - validationOwner, ok := attr.validationRewardsOwner.(*secp256k1fx.OutputOwners) + validationOwner, ok := attr.ValidationRewardsOwner.(*secp256k1fx.OutputOwners) if ok { validationRewardOwner, err = s.getAPIOwner(validationOwner) if err != nil { return err } } - delegationOwner, ok := attr.delegationRewardsOwner.(*secp256k1fx.OutputOwners) + delegationOwner, ok := attr.DelegationRewardsOwner.(*secp256k1fx.OutputOwners) if ok { delegationRewardOwner, err = s.getAPIOwner(delegationOwner) if err != nil { @@ -866,7 +830,7 @@ func (s *Service) GetCurrentValidators(_ *http.Request, args *GetCurrentValidato ValidationRewardOwner: validationRewardOwner, DelegationRewardOwner: delegationRewardOwner, DelegationFee: delegationFee, - Signer: attr.proofOfPossession, + Signer: attr.ProofOfPossession, } reply.Validators = append(reply.Validators, vdr) @@ -879,7 +843,7 @@ func (s *Service) GetCurrentValidators(_ *http.Request, args *GetCurrentValidato if err != nil { return err } - owner, ok := attr.rewardsOwner.(*secp256k1fx.OutputOwners) + owner, ok := attr.RewardsOwner.(*secp256k1fx.OutputOwners) if ok { rewardOwner, err = s.getAPIOwner(owner) if err != nil { @@ -1088,17 +1052,12 @@ func (s *Service) GetBlockchainStatus(r *http.Request, args *GetBlockchainStatus } func (s *Service) nodeValidates(blockchainID ids.ID) bool { - chainTx, _, err := s.vm.state.GetTx(blockchainID) + subnetID, err := s.vm.state.GetChainSubnet(blockchainID) if err != nil { return false } - chain, ok := chainTx.Unsigned.(*txs.CreateChainTx) - if !ok { - return false - } - - _, isValidator := s.vm.Validators.GetValidator(chain.SubnetID, s.vm.ctx.NodeID) + _, isValidator := s.vm.Validators.GetValidator(subnetID, s.vm.ctx.NodeID) return isValidator } @@ -1115,15 +1074,14 @@ func (s *Service) chainExists(ctx context.Context, blockID ids.ID, chainID ids.I } } - tx, _, err := state.GetTx(chainID) - if err == database.ErrNotFound { + switch _, err := state.GetChainSubnet(chainID); { + case err == nil: + return true, nil + case errors.Is(err, database.ErrNotFound): return false, nil - } - if err != nil { + default: return false, err } - _, ok = tx.Unsigned.(*txs.CreateChainTx) - return ok, nil } // ValidatedByArgs is the arguments for calling ValidatedBy @@ -1467,12 +1425,12 @@ func (s *Service) GetStake(_ *http.Request, args *GetStakeArgs, response *GetSta continue } - tx, _, err := s.vm.state.GetTx(staker.TxID) + stakerRewardAttributes, err := s.vm.state.GetStakerRewardAttributes(staker.TxID) if err != nil { return err } - stakedOuts = append(stakedOuts, getStakeHelper(tx, addrs, totalAmountStaked)...) + stakedOuts = append(stakedOuts, getStakeHelper(stakerRewardAttributes.Stake, addrs, totalAmountStaked)...) } pendingStakerIterator, err := s.vm.state.GetPendingStakerIterator() @@ -1488,12 +1446,12 @@ func (s *Service) GetStake(_ *http.Request, args *GetStakeArgs, response *GetSta continue } - tx, _, err := s.vm.state.GetTx(staker.TxID) + stakerRewardAttributes, err := s.vm.state.GetStakerRewardAttributes(staker.TxID) if err != nil { return err } - stakedOuts = append(stakedOuts, getStakeHelper(tx, addrs, totalAmountStaked)...) + stakedOuts = append(stakedOuts, getStakeHelper(stakerRewardAttributes.Stake, addrs, totalAmountStaked)...) } response.Stakeds = newJSONBalanceMap(totalAmountStaked) @@ -1860,17 +1818,11 @@ func (s *Service) getAPIOwner(owner *secp256k1fx.OutputOwners) (*platformapi.Own return apiOwner, nil } -// Takes in a staker and a set of addresses +// Takes in a slice of reward attributes and a set of addresses // Returns: // 1) The total amount staked by addresses in [addrs] // 2) The staked outputs -func getStakeHelper(tx *txs.Tx, addrs set.Set[ids.ShortID], totalAmountStaked map[ids.ID]uint64) []avax.TransferableOutput { - staker, ok := tx.Unsigned.(txs.PermissionlessStaker) - if !ok { - return nil - } - - stake := staker.Stake() +func getStakeHelper(stake []*avax.TransferableOutput, addrs set.Set[ids.ShortID], totalAmountStaked map[ids.ID]uint64) []avax.TransferableOutput { stakedOuts := make([]avax.TransferableOutput, 0, len(stake)) // Go through all of the staked outputs for _, output := range stake { diff --git a/vms/platformvm/service_test.go b/vms/platformvm/service_test.go index 69e94d0dee3e..abcbc68cb779 100644 --- a/vms/platformvm/service_test.go +++ b/vms/platformvm/service_test.go @@ -82,7 +82,7 @@ func defaultService(t *testing.T) (*Service, *mutableSharedMemory, *txstest.Wall return &Service{ vm: vm, addrManager: avax.NewAddressManager(vm.ctx), - stakerAttributesCache: &cache.LRU[ids.ID, *stakerAttributes]{ + stakerAttributesCache: &cache.LRU[ids.ID, *state.StakerRewardAttributes]{ Size: stakerAttributesCacheSize, }, }, mutableSharedMemory, factory diff --git a/vms/platformvm/state/diff.go b/vms/platformvm/state/diff.go index 91fb01d08fc9..f7fa1e55d9dd 100644 --- a/vms/platformvm/state/diff.go +++ b/vms/platformvm/state/diff.go @@ -209,6 +209,10 @@ func (d *diff) GetCurrentStakerIterator() (StakerIterator, error) { return d.currentStakerDiffs.GetStakerIterator(parentIterator), nil } +func (d *diff) GetStakerRewardAttributes(stakerID ids.ID) (*StakerRewardAttributes, error) { + return getStakerRewardAttributes(d, stakerID) +} + func (d *diff) GetPendingValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, error) { // If the validator was modified in this diff, return the modified // validator. @@ -330,6 +334,10 @@ func (d *diff) AddChain(createChainTx *txs.Tx) { } } +func (d *diff) GetChainSubnet(chainID ids.ID) (ids.ID, error) { + return getChainSubnet(d, chainID) +} + func (d *diff) GetTx(txID ids.ID) (*txs.Tx, status.Status, error) { if tx, exists := d.addedTxs[txID]; exists { return tx.tx, tx.status, nil diff --git a/vms/platformvm/state/mock_state.go b/vms/platformvm/state/mock_state.go index c1321567e6a9..2264b40590ad 100644 --- a/vms/platformvm/state/mock_state.go +++ b/vms/platformvm/state/mock_state.go @@ -182,6 +182,21 @@ func (mr *MockChainMockRecorder) DeleteUTXO(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUTXO", reflect.TypeOf((*MockChain)(nil).DeleteUTXO), arg0) } +// GetChainSubnet mocks base method. +func (m *MockChain) GetChainSubnet(arg0 ids.ID) (ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChainSubnet", arg0) + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetChainSubnet indicates an expected call of GetChainSubnet. +func (mr *MockChainMockRecorder) GetChainSubnet(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChainSubnet", reflect.TypeOf((*MockChain)(nil).GetChainSubnet), arg0) +} + // GetCurrentDelegatorIterator mocks base method. func (m *MockChain) GetCurrentDelegatorIterator(arg0 ids.ID, arg1 ids.NodeID) (StakerIterator, error) { m.ctrl.T.Helper() @@ -302,6 +317,21 @@ func (mr *MockChainMockRecorder) GetPendingValidator(arg0, arg1 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPendingValidator", reflect.TypeOf((*MockChain)(nil).GetPendingValidator), arg0, arg1) } +// GetStakerRewardAttributes mocks base method. +func (m *MockChain) GetStakerRewardAttributes(arg0 ids.ID) (*StakerRewardAttributes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStakerRewardAttributes", arg0) + ret0, _ := ret[0].(*StakerRewardAttributes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStakerRewardAttributes indicates an expected call of GetStakerRewardAttributes. +func (mr *MockChainMockRecorder) GetStakerRewardAttributes(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStakerRewardAttributes", reflect.TypeOf((*MockChain)(nil).GetStakerRewardAttributes), arg0) +} + // GetSubnetOwner mocks base method. func (m *MockChain) GetSubnetOwner(arg0 ids.ID) (fx.Owner, error) { m.ctrl.T.Helper() @@ -644,6 +674,21 @@ func (mr *MockDiffMockRecorder) DeleteUTXO(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUTXO", reflect.TypeOf((*MockDiff)(nil).DeleteUTXO), arg0) } +// GetChainSubnet mocks base method. +func (m *MockDiff) GetChainSubnet(arg0 ids.ID) (ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChainSubnet", arg0) + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetChainSubnet indicates an expected call of GetChainSubnet. +func (mr *MockDiffMockRecorder) GetChainSubnet(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChainSubnet", reflect.TypeOf((*MockDiff)(nil).GetChainSubnet), arg0) +} + // GetCurrentDelegatorIterator mocks base method. func (m *MockDiff) GetCurrentDelegatorIterator(arg0 ids.ID, arg1 ids.NodeID) (StakerIterator, error) { m.ctrl.T.Helper() @@ -764,6 +809,21 @@ func (mr *MockDiffMockRecorder) GetPendingValidator(arg0, arg1 any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPendingValidator", reflect.TypeOf((*MockDiff)(nil).GetPendingValidator), arg0, arg1) } +// GetStakerRewardAttributes mocks base method. +func (m *MockDiff) GetStakerRewardAttributes(arg0 ids.ID) (*StakerRewardAttributes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStakerRewardAttributes", arg0) + ret0, _ := ret[0].(*StakerRewardAttributes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStakerRewardAttributes indicates an expected call of GetStakerRewardAttributes. +func (mr *MockDiffMockRecorder) GetStakerRewardAttributes(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStakerRewardAttributes", reflect.TypeOf((*MockDiff)(nil).GetStakerRewardAttributes), arg0) +} + // GetSubnetOwner mocks base method. func (m *MockDiff) GetSubnetOwner(arg0 ids.ID) (fx.Owner, error) { m.ctrl.T.Helper() @@ -1216,6 +1276,21 @@ func (mr *MockStateMockRecorder) GetBlockIDAtHeight(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockIDAtHeight", reflect.TypeOf((*MockState)(nil).GetBlockIDAtHeight), arg0) } +// GetChainSubnet mocks base method. +func (m *MockState) GetChainSubnet(arg0 ids.ID) (ids.ID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetChainSubnet", arg0) + ret0, _ := ret[0].(ids.ID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetChainSubnet indicates an expected call of GetChainSubnet. +func (mr *MockStateMockRecorder) GetChainSubnet(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChainSubnet", reflect.TypeOf((*MockState)(nil).GetChainSubnet), arg0) +} + // GetChains mocks base method. func (m *MockState) GetChains(arg0 ids.ID) ([]*txs.Tx, error) { m.ctrl.T.Helper() @@ -1380,6 +1455,21 @@ func (mr *MockStateMockRecorder) GetRewardUTXOs(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRewardUTXOs", reflect.TypeOf((*MockState)(nil).GetRewardUTXOs), arg0) } +// GetStakerRewardAttributes mocks base method. +func (m *MockState) GetStakerRewardAttributes(arg0 ids.ID) (*StakerRewardAttributes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStakerRewardAttributes", arg0) + ret0, _ := ret[0].(*StakerRewardAttributes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStakerRewardAttributes indicates an expected call of GetStakerRewardAttributes. +func (mr *MockStateMockRecorder) GetStakerRewardAttributes(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStakerRewardAttributes", reflect.TypeOf((*MockState)(nil).GetStakerRewardAttributes), arg0) +} + // GetStartTime mocks base method. func (m *MockState) GetStartTime(arg0 ids.NodeID, arg1 ids.ID) (time.Time, error) { m.ctrl.T.Helper() diff --git a/vms/platformvm/state/staker.go b/vms/platformvm/state/staker.go index a9ba52595062..ab6a92bbe074 100644 --- a/vms/platformvm/state/staker.go +++ b/vms/platformvm/state/staker.go @@ -11,6 +11,9 @@ import ( "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/fx" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) @@ -126,3 +129,23 @@ func NewPendingStaker(txID ids.ID, staker txs.ScheduledStaker) (*Staker, error) Priority: staker.PendingPriority(), }, nil } + +// Staker object contains a staker's hot attributes, likely to be used often. +// StakerRewardAttributes contains a staker's cold attributes which are used less often. +// Note that both Staker and StakerAttribute content comes from the stakerTx creating the staker. +// In state.State we also have StakerMetadata, which contains data about the stakers +// generated during staker's activity (mostly uptimes). +type StakerRewardAttributes struct { + // common attributes + Stake []*avax.TransferableOutput + Outputs []*avax.TransferableOutput + + // validators specific attributes + Shares uint32 + ValidationRewardsOwner fx.Owner + DelegationRewardsOwner fx.Owner + ProofOfPossession *signer.ProofOfPossession + + // delegators specific attributes + RewardsOwner fx.Owner +} diff --git a/vms/platformvm/state/state.go b/vms/platformvm/state/state.go index 74b71dadf8a2..da30cd7fd9c7 100644 --- a/vms/platformvm/state/state.go +++ b/vms/platformvm/state/state.go @@ -112,6 +112,9 @@ type Chain interface { AddSubnetTransformation(transformSubnetTx *txs.Tx) AddChain(createChainTx *txs.Tx) + GetChainSubnet(chainID ids.ID) (ids.ID, error) + + GetStakerRewardAttributes(stakerID ids.ID) (*StakerRewardAttributes, error) GetTx(txID ids.ID) (*txs.Tx, status.Status, error) AddTx(tx *txs.Tx, status status.Status) @@ -689,6 +692,10 @@ func (s *state) GetCurrentStakerIterator() (StakerIterator, error) { return s.currentStakers.GetStakerIterator(), nil } +func (s *state) GetStakerRewardAttributes(stakerID ids.ID) (*StakerRewardAttributes, error) { + return getStakerRewardAttributes(s, stakerID) +} + func (s *state) GetPendingValidator(subnetID ids.ID, nodeID ids.NodeID) (*Staker, error) { return s.pendingStakers.GetValidator(subnetID, nodeID) } @@ -880,6 +887,10 @@ func (s *state) AddChain(createChainTxIntf *txs.Tx) { } } +func (s *state) GetChainSubnet(chainID ids.ID) (ids.ID, error) { + return getChainSubnet(s, chainID) +} + func (s *state) getChainDB(subnetID ids.ID) linkeddb.LinkedDB { if chainDB, cached := s.chainDBCache.Get(subnetID); cached { return chainDB diff --git a/vms/platformvm/state/state_helpers.go b/vms/platformvm/state/state_helpers.go new file mode 100644 index 000000000000..6726c0162966 --- /dev/null +++ b/vms/platformvm/state/state_helpers.go @@ -0,0 +1,67 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "errors" + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" +) + +var ( + ErrUnexpectedStakerType = errors.New("unexpected staker type ") + errNotABlockchain = errors.New("tx does not created a blockchain") +) + +func getStakerRewardAttributes(chain Chain, stakerID ids.ID) (*StakerRewardAttributes, error) { + stakerTx, _, err := chain.GetTx(stakerID) + if err != nil { + return nil, fmt.Errorf("failed to get next staker %s: %w", stakerID, err) + } + switch uStakerTx := stakerTx.Unsigned.(type) { + case txs.ValidatorTx: + var pop *signer.ProofOfPossession + if staker, ok := uStakerTx.(*txs.AddPermissionlessValidatorTx); ok { + if s, ok := staker.Signer.(*signer.ProofOfPossession); ok { + pop = s + } + } + + return &StakerRewardAttributes{ + Stake: uStakerTx.Stake(), + Outputs: uStakerTx.Outputs(), + Shares: uStakerTx.Shares(), + ValidationRewardsOwner: uStakerTx.ValidationRewardsOwner(), + DelegationRewardsOwner: uStakerTx.DelegationRewardsOwner(), + ProofOfPossession: pop, + }, nil + case txs.DelegatorTx: + return &StakerRewardAttributes{ + Stake: uStakerTx.Stake(), + Outputs: uStakerTx.Outputs(), + RewardsOwner: uStakerTx.RewardsOwner(), + }, nil + default: + return nil, fmt.Errorf("%w, txType %T", ErrUnexpectedStakerType, uStakerTx) + } +} + +func getChainSubnet(chain Chain, chainID ids.ID) (ids.ID, error) { + chainTx, _, err := chain.GetTx(chainID) + if err != nil { + return ids.Empty, fmt.Errorf( + "problem retrieving blockchain %q: %w", + chainID, + err, + ) + } + blockChain, ok := chainTx.Unsigned.(*txs.CreateChainTx) + if !ok { + return ids.Empty, fmt.Errorf("%w, txID %q", errNotABlockchain, chainID) + } + return blockChain.SubnetID, nil +} diff --git a/vms/platformvm/state/state_helpers_test.go b/vms/platformvm/state/state_helpers_test.go new file mode 100644 index 000000000000..6ad5eaff903e --- /dev/null +++ b/vms/platformvm/state/state_helpers_test.go @@ -0,0 +1,370 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package state + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/ids" + "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/platformvm/status" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +func TestGetStakerRewardAttributes(t *testing.T) { + type test struct { + name string + chainF func(*gomock.Controller) Chain + expectedAttributes *StakerRewardAttributes + expectedErr error + } + + var ( + stakerID = ids.GenerateTestID() + shares = uint32(2024) + avaxAssetID = ids.GenerateTestID() + addr = ids.GenerateTestShortID() + outputs = []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + { + 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{}, + }, + }, + }, + }, + } + stakeOutputs = []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &secp256k1fx.TransferOutput{ + Amt: 2 * units.KiloAvax, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + } + anOwner = &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + } + anotherOwner = &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 2, + Addrs: []ids.ShortID{ + addr, + }, + } + ) + sk, err := bls.NewSecretKey() + require.NoError(t, err) + pop := signer.NewProofOfPossession(sk) + + tests := []test{ + { + name: "permissionless validator tx type", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + validatorTx := &txs.Tx{ + Unsigned: &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + Outs: outputs, + }, + }, + StakeOuts: stakeOutputs, + ValidatorRewardsOwner: anOwner, + DelegatorRewardsOwner: anotherOwner, + DelegationShares: shares, + Signer: pop, + }, + } + chain.EXPECT().GetTx(stakerID).Return(validatorTx, status.Committed, nil) + return chain + }, + expectedAttributes: &StakerRewardAttributes{ + Stake: stakeOutputs, + Outputs: outputs, + Shares: shares, + ValidationRewardsOwner: anOwner, + DelegationRewardsOwner: anotherOwner, + ProofOfPossession: pop, + }, + expectedErr: nil, + }, + { + name: "non permissionless validator tx type", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + validatorTx := &txs.Tx{ + Unsigned: &txs.AddValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + Outs: outputs, + }, + }, + StakeOuts: stakeOutputs, + RewardsOwner: anOwner, + DelegationShares: shares, + }, + } + chain.EXPECT().GetTx(stakerID).Return(validatorTx, status.Committed, nil) + return chain + }, + expectedAttributes: &StakerRewardAttributes{ + Stake: stakeOutputs, + Outputs: outputs, + Shares: shares, + ValidationRewardsOwner: anOwner, + DelegationRewardsOwner: anOwner, + ProofOfPossession: nil, + }, + expectedErr: nil, + }, + { + name: "delegator tx type", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + delegatorTx := &txs.Tx{ + Unsigned: &txs.AddPermissionlessDelegatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + Outs: outputs, + }, + }, + StakeOuts: stakeOutputs, + DelegationRewardsOwner: anOwner, + }, + } + chain.EXPECT().GetTx(stakerID).Return(delegatorTx, status.Committed, nil) + return chain + }, + expectedAttributes: &StakerRewardAttributes{ + Stake: stakeOutputs, + Outputs: outputs, + RewardsOwner: anOwner, + }, + expectedErr: nil, + }, + { + name: "missing tx", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + chain.EXPECT().GetTx(stakerID).Return(nil, status.Unknown, database.ErrNotFound) + return chain + }, + expectedAttributes: nil, + expectedErr: database.ErrNotFound, + }, + { + name: "unexpected tx type", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + wrongTxType := &txs.Tx{ + Unsigned: &txs.CreateChainTx{}, + } + chain.EXPECT().GetTx(stakerID).Return(wrongTxType, status.Committed, nil) + return chain + }, + expectedAttributes: nil, + expectedErr: ErrUnexpectedStakerType, + }, + { + name: "getStakerRewardAttributes works across layers", + chainF: func(c *gomock.Controller) Chain { + // pile a diff on top of base state and let the target tx + // be included in base state. + state := NewMockState(c) + validatorTx := &txs.Tx{ + Unsigned: &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + Outs: outputs, + }, + }, + StakeOuts: stakeOutputs, + ValidatorRewardsOwner: anOwner, + DelegatorRewardsOwner: anotherOwner, + DelegationShares: shares, + Signer: pop, + }, + } + state.EXPECT().GetTx(stakerID).Return(validatorTx, status.Committed, nil) + state.EXPECT().GetTimestamp().Return(time.Now()) // needed to build diff + stateID := ids.GenerateTestID() + + versions := NewMockVersions(c) + versions.EXPECT().GetState(stateID).Return(state, true).Times(2) + + diff, err := NewDiff(stateID, versions) + require.NoError(t, err) + return diff + }, + expectedAttributes: &StakerRewardAttributes{ + Stake: stakeOutputs, + Outputs: outputs, + Shares: shares, + ValidationRewardsOwner: anOwner, + DelegationRewardsOwner: anotherOwner, + ProofOfPossession: pop, + }, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + chain := tt.chainF(ctrl) + attributes, err := getStakerRewardAttributes(chain, stakerID) + require.ErrorIs(err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + require.Equal(tt.expectedAttributes, attributes) + }) + } +} + +func TestGetChainSubnet(t *testing.T) { + type test struct { + name string + chainF func(*gomock.Controller) Chain + expectedSubnetID ids.ID + expectedErr error + } + + var ( + chainID = ids.GenerateTestID() + subnetID = ids.GenerateTestID() + ) + + tests := []test{ + { + name: "subnet from existing chain", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + createChainTx := &txs.Tx{ + Unsigned: &txs.CreateChainTx{ + SubnetID: subnetID, + }, + TxID: chainID, + } + chain.EXPECT().GetTx(chainID).Return(createChainTx, status.Committed, nil) + return chain + }, + expectedSubnetID: subnetID, + expectedErr: nil, + }, + { + name: "missing tx", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + chain.EXPECT().GetTx(chainID).Return(nil, status.Unknown, database.ErrNotFound) + return chain + }, + expectedSubnetID: ids.Empty, + expectedErr: database.ErrNotFound, + }, + { + name: "unexpected tx type", + chainF: func(c *gomock.Controller) Chain { + chain := NewMockChain(c) + wrongTxType := &txs.Tx{ + Unsigned: &txs.CreateSubnetTx{}, + } + chain.EXPECT().GetTx(chainID).Return(wrongTxType, status.Committed, nil) + return chain + }, + expectedSubnetID: ids.Empty, + expectedErr: errNotABlockchain, + }, + { + name: "getChainSubnet works across layers", + chainF: func(c *gomock.Controller) Chain { + // pile a diff on top of base state and let the target tx + // be included in base state. + state := NewMockState(c) + createChainTx := &txs.Tx{ + Unsigned: &txs.CreateChainTx{ + SubnetID: subnetID, + }, + TxID: chainID, + } + state.EXPECT().GetTx(chainID).Return(createChainTx, status.Committed, nil) + state.EXPECT().GetTimestamp().Return(time.Now()) // needed to build diff + stateID := ids.GenerateTestID() + + versions := NewMockVersions(c) + versions.EXPECT().GetState(stateID).Return(state, true).Times(2) + + diff, err := NewDiff(stateID, versions) + require.NoError(t, err) + return diff + }, + expectedSubnetID: subnetID, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + chain := tt.chainF(ctrl) + subnetID, err := getChainSubnet(chain, chainID) + require.ErrorIs(err, tt.expectedErr) + if tt.expectedErr != nil { + return + } + require.Equal(tt.expectedSubnetID, subnetID) + }) + } +} diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index c54b8207fb06..fa9dfad7cc27 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -329,24 +329,28 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error ) } - stakerTx, _, err := e.OnCommitState.GetTx(stakerToReward.TxID) + stakerAttributes, err := e.OnCommitState.GetStakerRewardAttributes(stakerToReward.TxID) if err != nil { - return fmt.Errorf("failed to get next removed staker tx: %w", err) + return fmt.Errorf("failed to get attributes for staker %s: %w", stakerToReward.TxID, err) } - // Invariant: A [txs.DelegatorTx] does not also implement the - // [txs.ValidatorTx] interface. - switch uStakerTx := stakerTx.Unsigned.(type) { - case txs.ValidatorTx: - if err := e.rewardValidatorTx(uStakerTx, stakerToReward); err != nil { + switch { + case stakerToReward.Priority.IsPermissionedValidator(): + // Invariant: Permissioned stakers are removed by the advancement of + // time and the current chain timestamp is == this staker's + // EndTime. This means only permissionless stakers should be + // left in the staker set. + return ErrShouldBePermissionlessStaker + case stakerToReward.Priority.IsCurrentValidator(): + if err := e.rewardValidatorTx(stakerToReward, stakerAttributes); err != nil { return err } // Handle staker lifecycle. e.OnCommitState.DeleteCurrentValidator(stakerToReward) e.OnAbortState.DeleteCurrentValidator(stakerToReward) - case txs.DelegatorTx: - if err := e.rewardDelegatorTx(uStakerTx, stakerToReward); err != nil { + case stakerToReward.Priority.IsCurrentDelegator(): + if err := e.rewardDelegatorTx(stakerToReward, stakerAttributes); err != nil { return err } @@ -354,11 +358,7 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error 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 - // EndTime. This means only permissionless stakers should be - // left in the staker set. - return ErrShouldBePermissionlessStaker + return state.ErrUnexpectedStakerType } // If the reward is aborted, then the current supply should be decreased. @@ -374,11 +374,11 @@ func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error return nil } -func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, validator *state.Staker) error { +func (e *ProposalTxExecutor) rewardValidatorTx(validator *state.Staker, valAttributes *state.StakerRewardAttributes) error { var ( txID = validator.TxID - stake = uValidatorTx.Stake() - outputs = uValidatorTx.Outputs() + stake = valAttributes.Stake + outputs = valAttributes.Outputs // Invariant: The staked asset must be equal to the reward asset. stakeAsset = stake[0].Asset ) @@ -403,7 +403,7 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val // Provide the reward here reward := validator.PotentialReward if reward > 0 { - validationRewardsOwner := uValidatorTx.ValidationRewardsOwner() + validationRewardsOwner := valAttributes.ValidationRewardsOwner outIntf, err := e.Fx.CreateOutput(reward, validationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) @@ -440,7 +440,7 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val return nil } - delegationRewardsOwner := uValidatorTx.DelegationRewardsOwner() + delegationRewardsOwner := valAttributes.DelegationRewardsOwner outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) @@ -476,11 +476,11 @@ func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, val return nil } -func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, delegator *state.Staker) error { +func (e *ProposalTxExecutor) rewardDelegatorTx(delegator *state.Staker, delAttributes *state.StakerRewardAttributes) error { var ( txID = delegator.TxID - stake = uDelegatorTx.Stake() - outputs = uDelegatorTx.Outputs() + stake = delAttributes.Stake + outputs = delAttributes.Outputs // Invariant: The staked asset must be equal to the reward asset. stakeAsset = stake[0].Asset ) @@ -507,29 +507,20 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err) } - vdrTxIntf, _, err := e.OnCommitState.GetTx(validator.TxID) + valAttributes, err := e.OnCommitState.GetStakerRewardAttributes(validator.TxID) if err != nil { return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err) } - // Invariant: Delegators must only be able to reference validator - // transactions that implement [txs.ValidatorTx]. All - // validator transactions implement this interface except the - // AddSubnetValidatorTx. - vdrTx, ok := vdrTxIntf.Unsigned.(txs.ValidatorTx) - if !ok { - return ErrWrongTxType - } - // Calculate split of reward between delegator/delegatee - delegateeReward, delegatorReward := reward.Split(delegator.PotentialReward, vdrTx.Shares()) + delegateeReward, delegatorReward := reward.Split(delegator.PotentialReward, valAttributes.Shares) utxosOffset := 0 // Reward the delegator here reward := delegatorReward if reward > 0 { - rewardsOwner := uDelegatorTx.RewardsOwner() + rewardsOwner := delAttributes.RewardsOwner outIntf, err := e.Fx.CreateOutput(reward, rewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) @@ -585,7 +576,7 @@ func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, del } else { // For any validators who started prior to [CortinaTime], we issue the // [delegateeReward] immediately. - delegationRewardsOwner := vdrTx.DelegationRewardsOwner() + delegationRewardsOwner := valAttributes.DelegationRewardsOwner outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner) if err != nil { return fmt.Errorf("failed to create output: %w", err) diff --git a/vms/platformvm/validators/manager.go b/vms/platformvm/validators/manager.go index 781d119e226b..50f97ca77e42 100644 --- a/vms/platformvm/validators/manager.go +++ b/vms/platformvm/validators/manager.go @@ -19,8 +19,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/block" "github.com/ava-labs/avalanchego/vms/platformvm/config" "github.com/ava-labs/avalanchego/vms/platformvm/metrics" - "github.com/ava-labs/avalanchego/vms/platformvm/status" - "github.com/ava-labs/avalanchego/vms/platformvm/txs" ) const ( @@ -47,7 +45,7 @@ type Manager interface { } type State interface { - GetTx(txID ids.ID) (*txs.Tx, status.Status, error) + GetChainSubnet(chainID ids.ID) (ids.ID, error) GetLastAccepted() ids.ID GetStatelessBlock(blockID ids.ID) (block.Block, error) @@ -367,19 +365,7 @@ func (m *manager) GetSubnetID(_ context.Context, chainID ids.ID) (ids.ID, error) return constants.PrimaryNetworkID, nil } - chainTx, _, err := m.state.GetTx(chainID) - if err != nil { - return ids.Empty, fmt.Errorf( - "problem retrieving blockchain %q: %w", - chainID, - err, - ) - } - chain, ok := chainTx.Unsigned.(*txs.CreateChainTx) - if !ok { - return ids.Empty, fmt.Errorf("%q is not a blockchain", chainID) - } - return chain.SubnetID, nil + return m.state.GetChainSubnet(chainID) } func (m *manager) OnAcceptedBlockID(blkID ids.ID) { diff --git a/vms/platformvm/vm.go b/vms/platformvm/vm.go index 203688d23136..59a744ac73d8 100644 --- a/vms/platformvm/vm.go +++ b/vms/platformvm/vm.go @@ -462,7 +462,7 @@ func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) { service := &Service{ vm: vm, addrManager: avax.NewAddressManager(vm.ctx), - stakerAttributesCache: &cache.LRU[ids.ID, *stakerAttributes]{ + stakerAttributesCache: &cache.LRU[ids.ID, *state.StakerRewardAttributes]{ Size: stakerAttributesCacheSize, }, }