github.com/ava-labs/avalanchego@v1.11.11/vms/platformvm/txs/executor/proposal_tx_executor.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package executor
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"time"
    10  
    11  	"github.com/ava-labs/avalanchego/database"
    12  	"github.com/ava-labs/avalanchego/ids"
    13  	"github.com/ava-labs/avalanchego/utils/math"
    14  	"github.com/ava-labs/avalanchego/vms/components/avax"
    15  	"github.com/ava-labs/avalanchego/vms/components/verify"
    16  	"github.com/ava-labs/avalanchego/vms/platformvm/reward"
    17  	"github.com/ava-labs/avalanchego/vms/platformvm/state"
    18  	"github.com/ava-labs/avalanchego/vms/platformvm/txs"
    19  	"github.com/ava-labs/avalanchego/vms/platformvm/txs/fee"
    20  )
    21  
    22  const (
    23  	// Maximum future start time for staking/delegating
    24  	MaxFutureStartTime = 24 * 7 * 2 * time.Hour
    25  
    26  	// SyncBound is the synchrony bound used for safe decision making
    27  	SyncBound = 10 * time.Second
    28  
    29  	MaxValidatorWeightFactor = 5
    30  )
    31  
    32  var (
    33  	_ txs.Visitor = (*ProposalTxExecutor)(nil)
    34  
    35  	ErrRemoveStakerTooEarly          = errors.New("attempting to remove staker before their end time")
    36  	ErrRemoveWrongStaker             = errors.New("attempting to remove wrong staker")
    37  	ErrChildBlockNotAfterParent      = errors.New("proposed timestamp not after current chain time")
    38  	ErrInvalidState                  = errors.New("generated output isn't valid state")
    39  	ErrShouldBePermissionlessStaker  = errors.New("expected permissionless staker")
    40  	ErrWrongTxType                   = errors.New("wrong transaction type")
    41  	ErrInvalidID                     = errors.New("invalid ID")
    42  	ErrProposedAddStakerTxAfterBanff = errors.New("staker transaction proposed after Banff")
    43  	ErrAdvanceTimeTxIssuedAfterBanff = errors.New("AdvanceTimeTx issued after Banff")
    44  )
    45  
    46  type ProposalTxExecutor struct {
    47  	// inputs, to be filled before visitor methods are called
    48  	*Backend
    49  	FeeCalculator fee.Calculator
    50  	Tx            *txs.Tx
    51  	// [OnCommitState] is the state used for validation.
    52  	// [OnCommitState] is modified by this struct's methods to
    53  	// reflect changes made to the state if the proposal is committed.
    54  	//
    55  	// Invariant: Both [OnCommitState] and [OnAbortState] represent the same
    56  	//            state when provided to this struct.
    57  	OnCommitState state.Diff
    58  	// [OnAbortState] is modified by this struct's methods to
    59  	// reflect changes made to the state if the proposal is aborted.
    60  	OnAbortState state.Diff
    61  }
    62  
    63  func (*ProposalTxExecutor) CreateChainTx(*txs.CreateChainTx) error {
    64  	return ErrWrongTxType
    65  }
    66  
    67  func (*ProposalTxExecutor) CreateSubnetTx(*txs.CreateSubnetTx) error {
    68  	return ErrWrongTxType
    69  }
    70  
    71  func (*ProposalTxExecutor) ImportTx(*txs.ImportTx) error {
    72  	return ErrWrongTxType
    73  }
    74  
    75  func (*ProposalTxExecutor) ExportTx(*txs.ExportTx) error {
    76  	return ErrWrongTxType
    77  }
    78  
    79  func (*ProposalTxExecutor) RemoveSubnetValidatorTx(*txs.RemoveSubnetValidatorTx) error {
    80  	return ErrWrongTxType
    81  }
    82  
    83  func (*ProposalTxExecutor) TransformSubnetTx(*txs.TransformSubnetTx) error {
    84  	return ErrWrongTxType
    85  }
    86  
    87  func (*ProposalTxExecutor) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error {
    88  	return ErrWrongTxType
    89  }
    90  
    91  func (*ProposalTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDelegatorTx) error {
    92  	return ErrWrongTxType
    93  }
    94  
    95  func (*ProposalTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error {
    96  	return ErrWrongTxType
    97  }
    98  
    99  func (*ProposalTxExecutor) BaseTx(*txs.BaseTx) error {
   100  	return ErrWrongTxType
   101  }
   102  
   103  func (e *ProposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error {
   104  	// AddValidatorTx is a proposal transaction until the Banff fork
   105  	// activation. Following the activation, AddValidatorTxs must be issued into
   106  	// StandardBlocks.
   107  	currentTimestamp := e.OnCommitState.GetTimestamp()
   108  	if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) {
   109  		return fmt.Errorf(
   110  			"%w: timestamp (%s) >= Banff fork time (%s)",
   111  			ErrProposedAddStakerTxAfterBanff,
   112  			currentTimestamp,
   113  			e.Config.UpgradeConfig.BanffTime,
   114  		)
   115  	}
   116  
   117  	onAbortOuts, err := verifyAddValidatorTx(
   118  		e.Backend,
   119  		e.FeeCalculator,
   120  		e.OnCommitState,
   121  		e.Tx,
   122  		tx,
   123  	)
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	txID := e.Tx.ID()
   129  
   130  	// Set up the state if this tx is committed
   131  	// Consume the UTXOs
   132  	avax.Consume(e.OnCommitState, tx.Ins)
   133  	// Produce the UTXOs
   134  	avax.Produce(e.OnCommitState, txID, tx.Outs)
   135  
   136  	newStaker, err := state.NewPendingStaker(txID, tx)
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	if err := e.OnCommitState.PutPendingValidator(newStaker); err != nil {
   142  		return err
   143  	}
   144  
   145  	// Set up the state if this tx is aborted
   146  	// Consume the UTXOs
   147  	avax.Consume(e.OnAbortState, tx.Ins)
   148  	// Produce the UTXOs
   149  	avax.Produce(e.OnAbortState, txID, onAbortOuts)
   150  	return nil
   151  }
   152  
   153  func (e *ProposalTxExecutor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error {
   154  	// AddSubnetValidatorTx is a proposal transaction until the Banff fork
   155  	// activation. Following the activation, AddSubnetValidatorTxs must be
   156  	// issued into StandardBlocks.
   157  	currentTimestamp := e.OnCommitState.GetTimestamp()
   158  	if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) {
   159  		return fmt.Errorf(
   160  			"%w: timestamp (%s) >= Banff fork time (%s)",
   161  			ErrProposedAddStakerTxAfterBanff,
   162  			currentTimestamp,
   163  			e.Config.UpgradeConfig.BanffTime,
   164  		)
   165  	}
   166  
   167  	if err := verifyAddSubnetValidatorTx(
   168  		e.Backend,
   169  		e.FeeCalculator,
   170  		e.OnCommitState,
   171  		e.Tx,
   172  		tx,
   173  	); err != nil {
   174  		return err
   175  	}
   176  
   177  	txID := e.Tx.ID()
   178  
   179  	// Set up the state if this tx is committed
   180  	// Consume the UTXOs
   181  	avax.Consume(e.OnCommitState, tx.Ins)
   182  	// Produce the UTXOs
   183  	avax.Produce(e.OnCommitState, txID, tx.Outs)
   184  
   185  	newStaker, err := state.NewPendingStaker(txID, tx)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	if err := e.OnCommitState.PutPendingValidator(newStaker); err != nil {
   191  		return err
   192  	}
   193  
   194  	// Set up the state if this tx is aborted
   195  	// Consume the UTXOs
   196  	avax.Consume(e.OnAbortState, tx.Ins)
   197  	// Produce the UTXOs
   198  	avax.Produce(e.OnAbortState, txID, tx.Outs)
   199  	return nil
   200  }
   201  
   202  func (e *ProposalTxExecutor) AddDelegatorTx(tx *txs.AddDelegatorTx) error {
   203  	// AddDelegatorTx is a proposal transaction until the Banff fork
   204  	// activation. Following the activation, AddDelegatorTxs must be issued into
   205  	// StandardBlocks.
   206  	currentTimestamp := e.OnCommitState.GetTimestamp()
   207  	if e.Config.UpgradeConfig.IsBanffActivated(currentTimestamp) {
   208  		return fmt.Errorf(
   209  			"%w: timestamp (%s) >= Banff fork time (%s)",
   210  			ErrProposedAddStakerTxAfterBanff,
   211  			currentTimestamp,
   212  			e.Config.UpgradeConfig.BanffTime,
   213  		)
   214  	}
   215  
   216  	onAbortOuts, err := verifyAddDelegatorTx(
   217  		e.Backend,
   218  		e.FeeCalculator,
   219  		e.OnCommitState,
   220  		e.Tx,
   221  		tx,
   222  	)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	txID := e.Tx.ID()
   228  
   229  	// Set up the state if this tx is committed
   230  	// Consume the UTXOs
   231  	avax.Consume(e.OnCommitState, tx.Ins)
   232  	// Produce the UTXOs
   233  	avax.Produce(e.OnCommitState, txID, tx.Outs)
   234  
   235  	newStaker, err := state.NewPendingStaker(txID, tx)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	e.OnCommitState.PutPendingDelegator(newStaker)
   241  
   242  	// Set up the state if this tx is aborted
   243  	// Consume the UTXOs
   244  	avax.Consume(e.OnAbortState, tx.Ins)
   245  	// Produce the UTXOs
   246  	avax.Produce(e.OnAbortState, txID, onAbortOuts)
   247  	return nil
   248  }
   249  
   250  func (e *ProposalTxExecutor) AdvanceTimeTx(tx *txs.AdvanceTimeTx) error {
   251  	switch {
   252  	case tx == nil:
   253  		return txs.ErrNilTx
   254  	case len(e.Tx.Creds) != 0:
   255  		return errWrongNumberOfCredentials
   256  	}
   257  
   258  	// Validate [newChainTime]
   259  	newChainTime := tx.Timestamp()
   260  	if e.Config.UpgradeConfig.IsBanffActivated(newChainTime) {
   261  		return fmt.Errorf(
   262  			"%w: proposed timestamp (%s) >= Banff fork time (%s)",
   263  			ErrAdvanceTimeTxIssuedAfterBanff,
   264  			newChainTime,
   265  			e.Config.UpgradeConfig.BanffTime,
   266  		)
   267  	}
   268  
   269  	parentChainTime := e.OnCommitState.GetTimestamp()
   270  	if !newChainTime.After(parentChainTime) {
   271  		return fmt.Errorf(
   272  			"%w, proposed timestamp (%s), chain time (%s)",
   273  			ErrChildBlockNotAfterParent,
   274  			parentChainTime,
   275  			parentChainTime,
   276  		)
   277  	}
   278  
   279  	// Only allow timestamp to move forward as far as the time of next staker
   280  	// set change time
   281  	nextStakerChangeTime, err := state.GetNextStakerChangeTime(e.OnCommitState)
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	now := e.Clk.Time()
   287  	if err := VerifyNewChainTime(
   288  		newChainTime,
   289  		nextStakerChangeTime,
   290  		now,
   291  	); err != nil {
   292  		return err
   293  	}
   294  
   295  	// Note that state doesn't change if this proposal is aborted
   296  	_, err = AdvanceTimeTo(e.Backend, e.OnCommitState, newChainTime)
   297  	return err
   298  }
   299  
   300  func (e *ProposalTxExecutor) RewardValidatorTx(tx *txs.RewardValidatorTx) error {
   301  	switch {
   302  	case tx == nil:
   303  		return txs.ErrNilTx
   304  	case tx.TxID == ids.Empty:
   305  		return ErrInvalidID
   306  	case len(e.Tx.Creds) != 0:
   307  		return errWrongNumberOfCredentials
   308  	}
   309  
   310  	currentStakerIterator, err := e.OnCommitState.GetCurrentStakerIterator()
   311  	if err != nil {
   312  		return err
   313  	}
   314  	if !currentStakerIterator.Next() {
   315  		return fmt.Errorf("failed to get next staker to remove: %w", database.ErrNotFound)
   316  	}
   317  	stakerToReward := currentStakerIterator.Value()
   318  	currentStakerIterator.Release()
   319  
   320  	if stakerToReward.TxID != tx.TxID {
   321  		return fmt.Errorf(
   322  			"%w: %s != %s",
   323  			ErrRemoveWrongStaker,
   324  			stakerToReward.TxID,
   325  			tx.TxID,
   326  		)
   327  	}
   328  
   329  	// Verify that the chain's timestamp is the validator's end time
   330  	currentChainTime := e.OnCommitState.GetTimestamp()
   331  	if !stakerToReward.EndTime.Equal(currentChainTime) {
   332  		return fmt.Errorf(
   333  			"%w: TxID = %s with %s < %s",
   334  			ErrRemoveStakerTooEarly,
   335  			tx.TxID,
   336  			currentChainTime,
   337  			stakerToReward.EndTime,
   338  		)
   339  	}
   340  
   341  	stakerTx, _, err := e.OnCommitState.GetTx(stakerToReward.TxID)
   342  	if err != nil {
   343  		return fmt.Errorf("failed to get next removed staker tx: %w", err)
   344  	}
   345  
   346  	// Invariant: A [txs.DelegatorTx] does not also implement the
   347  	//            [txs.ValidatorTx] interface.
   348  	switch uStakerTx := stakerTx.Unsigned.(type) {
   349  	case txs.ValidatorTx:
   350  		if err := e.rewardValidatorTx(uStakerTx, stakerToReward); err != nil {
   351  			return err
   352  		}
   353  
   354  		// Handle staker lifecycle.
   355  		e.OnCommitState.DeleteCurrentValidator(stakerToReward)
   356  		e.OnAbortState.DeleteCurrentValidator(stakerToReward)
   357  	case txs.DelegatorTx:
   358  		if err := e.rewardDelegatorTx(uStakerTx, stakerToReward); err != nil {
   359  			return err
   360  		}
   361  
   362  		// Handle staker lifecycle.
   363  		e.OnCommitState.DeleteCurrentDelegator(stakerToReward)
   364  		e.OnAbortState.DeleteCurrentDelegator(stakerToReward)
   365  	default:
   366  		// Invariant: Permissioned stakers are removed by the advancement of
   367  		//            time and the current chain timestamp is == this staker's
   368  		//            EndTime. This means only permissionless stakers should be
   369  		//            left in the staker set.
   370  		return ErrShouldBePermissionlessStaker
   371  	}
   372  
   373  	// If the reward is aborted, then the current supply should be decreased.
   374  	currentSupply, err := e.OnAbortState.GetCurrentSupply(stakerToReward.SubnetID)
   375  	if err != nil {
   376  		return err
   377  	}
   378  	newSupply, err := math.Sub(currentSupply, stakerToReward.PotentialReward)
   379  	if err != nil {
   380  		return err
   381  	}
   382  	e.OnAbortState.SetCurrentSupply(stakerToReward.SubnetID, newSupply)
   383  	return nil
   384  }
   385  
   386  func (e *ProposalTxExecutor) rewardValidatorTx(uValidatorTx txs.ValidatorTx, validator *state.Staker) error {
   387  	var (
   388  		txID    = validator.TxID
   389  		stake   = uValidatorTx.Stake()
   390  		outputs = uValidatorTx.Outputs()
   391  		// Invariant: The staked asset must be equal to the reward asset.
   392  		stakeAsset = stake[0].Asset
   393  	)
   394  
   395  	// Refund the stake only when validator is about to leave
   396  	// the staking set
   397  	for i, out := range stake {
   398  		utxo := &avax.UTXO{
   399  			UTXOID: avax.UTXOID{
   400  				TxID:        txID,
   401  				OutputIndex: uint32(len(outputs) + i),
   402  			},
   403  			Asset: out.Asset,
   404  			Out:   out.Output(),
   405  		}
   406  		e.OnCommitState.AddUTXO(utxo)
   407  		e.OnAbortState.AddUTXO(utxo)
   408  	}
   409  
   410  	utxosOffset := 0
   411  
   412  	// Provide the reward here
   413  	reward := validator.PotentialReward
   414  	if reward > 0 {
   415  		validationRewardsOwner := uValidatorTx.ValidationRewardsOwner()
   416  		outIntf, err := e.Fx.CreateOutput(reward, validationRewardsOwner)
   417  		if err != nil {
   418  			return fmt.Errorf("failed to create output: %w", err)
   419  		}
   420  		out, ok := outIntf.(verify.State)
   421  		if !ok {
   422  			return ErrInvalidState
   423  		}
   424  
   425  		utxo := &avax.UTXO{
   426  			UTXOID: avax.UTXOID{
   427  				TxID:        txID,
   428  				OutputIndex: uint32(len(outputs) + len(stake)),
   429  			},
   430  			Asset: stakeAsset,
   431  			Out:   out,
   432  		}
   433  		e.OnCommitState.AddUTXO(utxo)
   434  		e.OnCommitState.AddRewardUTXO(txID, utxo)
   435  
   436  		utxosOffset++
   437  	}
   438  
   439  	// Provide the accrued delegatee rewards from successful delegations here.
   440  	delegateeReward, err := e.OnCommitState.GetDelegateeReward(
   441  		validator.SubnetID,
   442  		validator.NodeID,
   443  	)
   444  	if err != nil {
   445  		return fmt.Errorf("failed to fetch accrued delegatee rewards: %w", err)
   446  	}
   447  
   448  	if delegateeReward == 0 {
   449  		return nil
   450  	}
   451  
   452  	delegationRewardsOwner := uValidatorTx.DelegationRewardsOwner()
   453  	outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner)
   454  	if err != nil {
   455  		return fmt.Errorf("failed to create output: %w", err)
   456  	}
   457  	out, ok := outIntf.(verify.State)
   458  	if !ok {
   459  		return ErrInvalidState
   460  	}
   461  
   462  	onCommitUtxo := &avax.UTXO{
   463  		UTXOID: avax.UTXOID{
   464  			TxID:        txID,
   465  			OutputIndex: uint32(len(outputs) + len(stake) + utxosOffset),
   466  		},
   467  		Asset: stakeAsset,
   468  		Out:   out,
   469  	}
   470  	e.OnCommitState.AddUTXO(onCommitUtxo)
   471  	e.OnCommitState.AddRewardUTXO(txID, onCommitUtxo)
   472  
   473  	// Note: There is no [offset] if the RewardValidatorTx is
   474  	// aborted, because the validator reward is not awarded.
   475  	onAbortUtxo := &avax.UTXO{
   476  		UTXOID: avax.UTXOID{
   477  			TxID:        txID,
   478  			OutputIndex: uint32(len(outputs) + len(stake)),
   479  		},
   480  		Asset: stakeAsset,
   481  		Out:   out,
   482  	}
   483  	e.OnAbortState.AddUTXO(onAbortUtxo)
   484  	e.OnAbortState.AddRewardUTXO(txID, onAbortUtxo)
   485  	return nil
   486  }
   487  
   488  func (e *ProposalTxExecutor) rewardDelegatorTx(uDelegatorTx txs.DelegatorTx, delegator *state.Staker) error {
   489  	var (
   490  		txID    = delegator.TxID
   491  		stake   = uDelegatorTx.Stake()
   492  		outputs = uDelegatorTx.Outputs()
   493  		// Invariant: The staked asset must be equal to the reward asset.
   494  		stakeAsset = stake[0].Asset
   495  	)
   496  
   497  	// Refund the stake only when delegator is about to leave
   498  	// the staking set
   499  	for i, out := range stake {
   500  		utxo := &avax.UTXO{
   501  			UTXOID: avax.UTXOID{
   502  				TxID:        txID,
   503  				OutputIndex: uint32(len(outputs) + i),
   504  			},
   505  			Asset: out.Asset,
   506  			Out:   out.Output(),
   507  		}
   508  		e.OnCommitState.AddUTXO(utxo)
   509  		e.OnAbortState.AddUTXO(utxo)
   510  	}
   511  
   512  	// We're (possibly) rewarding a delegator, so we need to fetch
   513  	// the validator they are delegated to.
   514  	validator, err := e.OnCommitState.GetCurrentValidator(delegator.SubnetID, delegator.NodeID)
   515  	if err != nil {
   516  		return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err)
   517  	}
   518  
   519  	vdrTxIntf, _, err := e.OnCommitState.GetTx(validator.TxID)
   520  	if err != nil {
   521  		return fmt.Errorf("failed to get whether %s is a validator: %w", delegator.NodeID, err)
   522  	}
   523  
   524  	// Invariant: Delegators must only be able to reference validator
   525  	//            transactions that implement [txs.ValidatorTx]. All
   526  	//            validator transactions implement this interface except the
   527  	//            AddSubnetValidatorTx.
   528  	vdrTx, ok := vdrTxIntf.Unsigned.(txs.ValidatorTx)
   529  	if !ok {
   530  		return ErrWrongTxType
   531  	}
   532  
   533  	// Calculate split of reward between delegator/delegatee
   534  	delegateeReward, delegatorReward := reward.Split(delegator.PotentialReward, vdrTx.Shares())
   535  
   536  	utxosOffset := 0
   537  
   538  	// Reward the delegator here
   539  	reward := delegatorReward
   540  	if reward > 0 {
   541  		rewardsOwner := uDelegatorTx.RewardsOwner()
   542  		outIntf, err := e.Fx.CreateOutput(reward, rewardsOwner)
   543  		if err != nil {
   544  			return fmt.Errorf("failed to create output: %w", err)
   545  		}
   546  		out, ok := outIntf.(verify.State)
   547  		if !ok {
   548  			return ErrInvalidState
   549  		}
   550  		utxo := &avax.UTXO{
   551  			UTXOID: avax.UTXOID{
   552  				TxID:        txID,
   553  				OutputIndex: uint32(len(outputs) + len(stake)),
   554  			},
   555  			Asset: stakeAsset,
   556  			Out:   out,
   557  		}
   558  
   559  		e.OnCommitState.AddUTXO(utxo)
   560  		e.OnCommitState.AddRewardUTXO(txID, utxo)
   561  
   562  		utxosOffset++
   563  	}
   564  
   565  	if delegateeReward == 0 {
   566  		return nil
   567  	}
   568  
   569  	// Reward the delegatee here
   570  	if e.Config.UpgradeConfig.IsCortinaActivated(validator.StartTime) {
   571  		previousDelegateeReward, err := e.OnCommitState.GetDelegateeReward(
   572  			validator.SubnetID,
   573  			validator.NodeID,
   574  		)
   575  		if err != nil {
   576  			return fmt.Errorf("failed to get delegatee reward: %w", err)
   577  		}
   578  
   579  		// Invariant: The rewards calculator can never return a
   580  		//            [potentialReward] that would overflow the
   581  		//            accumulated rewards.
   582  		newDelegateeReward := previousDelegateeReward + delegateeReward
   583  
   584  		// For any validators starting after [CortinaTime], we defer rewarding the
   585  		// [reward] until their staking period is over.
   586  		err = e.OnCommitState.SetDelegateeReward(
   587  			validator.SubnetID,
   588  			validator.NodeID,
   589  			newDelegateeReward,
   590  		)
   591  		if err != nil {
   592  			return fmt.Errorf("failed to update delegatee reward: %w", err)
   593  		}
   594  	} else {
   595  		// For any validators who started prior to [CortinaTime], we issue the
   596  		// [delegateeReward] immediately.
   597  		delegationRewardsOwner := vdrTx.DelegationRewardsOwner()
   598  		outIntf, err := e.Fx.CreateOutput(delegateeReward, delegationRewardsOwner)
   599  		if err != nil {
   600  			return fmt.Errorf("failed to create output: %w", err)
   601  		}
   602  		out, ok := outIntf.(verify.State)
   603  		if !ok {
   604  			return ErrInvalidState
   605  		}
   606  		utxo := &avax.UTXO{
   607  			UTXOID: avax.UTXOID{
   608  				TxID:        txID,
   609  				OutputIndex: uint32(len(outputs) + len(stake) + utxosOffset),
   610  			},
   611  			Asset: stakeAsset,
   612  			Out:   out,
   613  		}
   614  
   615  		e.OnCommitState.AddUTXO(utxo)
   616  		e.OnCommitState.AddRewardUTXO(txID, utxo)
   617  	}
   618  	return nil
   619  }