Files
node/vms/platformvm/txs/executor/staker_tx_verification.go
T
Hanzo AI 8de1b78ed8 refactor: migrate UTXO components to standalone github.com/luxfi/utxo
One place, one way to define UTXO primitives. Delete the in-tree duplicate
at node/vms/components/lux/ and import the standalone luxfi/utxo package
with a 'lux' alias so existing call-sites (lux.UTXO, lux.TransferableInput,
etc.) remain unchanged in consumers.

Files changed: 167 imports rewritten, 2 directories deleted.

Build still green on the whole tree except pre-existing examples/multi-network
QChainMainnetID issue (unrelated).

Next: rename luxfi/utxo/luxmock → utxomock for full brand-neutrality; that
is a separate PR since it requires touching all consumer alias names.
2026-04-13 06:47:35 -07:00

828 lines
21 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"errors"
"fmt"
"math"
"time"
"github.com/luxfi/constants"
"github.com/luxfi/database"
"github.com/luxfi/ids"
lux "github.com/luxfi/utxo"
"github.com/luxfi/node/vms/platformvm/state"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/vms/platformvm/txs/fee"
safemath "github.com/luxfi/math"
)
var (
ErrWeightTooSmall = errors.New("weight of this validator is too low")
ErrWeightTooLarge = errors.New("weight of this validator is too large")
ErrInsufficientDelegationFee = errors.New("staker charges an insufficient delegation fee")
ErrStakeTooShort = errors.New("staking period is too short")
ErrStakeTooLong = errors.New("staking period is too long")
ErrFlowCheckFailed = errors.New("flow check failed")
ErrNotValidator = errors.New("isn't a current or pending validator")
ErrRemovePermissionlessValidator = errors.New("attempting to remove permissionless validator")
ErrStakeOverflow = errors.New("validator stake exceeds limit")
ErrPeriodMismatch = errors.New("proposed staking period is not inside dependent staking period")
ErrOverDelegated = errors.New("validator would be over delegated")
ErrIsNotTransformChainTx = errors.New("is not a transform net tx")
ErrTimestampNotBeforeStartTime = errors.New("chain timestamp not before start time")
ErrAlreadyValidator = errors.New("already a validator")
ErrDuplicateValidator = errors.New("duplicate validator")
ErrDelegateToPermissionedValidator = errors.New("delegation to permissioned validator")
ErrWrongStakedAssetID = errors.New("incorrect staked assetID")
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")
)
// verifyChainValidatorPrimaryNetworkRequirements verifies the primary
// network requirements for [chainValidator]. An error is returned if they
// are not fulfilled.
func verifyChainValidatorPrimaryNetworkRequirements(
isDurangoActive bool,
chainState state.Chain,
chainValidator txs.Validator,
) error {
primaryNetworkValidator, err := GetValidator(chainState, constants.PrimaryNetworkID, chainValidator.NodeID)
if err == database.ErrNotFound {
return fmt.Errorf(
"%s %w of the primary network",
chainValidator.NodeID,
ErrNotValidator,
)
}
if err != nil {
return fmt.Errorf(
"failed to fetch the primary network validator for %s: %w",
chainValidator.NodeID,
err,
)
}
// Ensure that the period this validator validates the specified chain
// is a subset of the time they validate the primary network.
startTime := chainState.GetTimestamp()
if !isDurangoActive {
startTime = chainValidator.StartTime()
}
if !txs.BoundedBy(
startTime,
chainValidator.EndTime(),
primaryNetworkValidator.StartTime,
primaryNetworkValidator.EndTime,
) {
return ErrPeriodMismatch
}
return nil
}
// verifyAddValidatorTx carries out the validation for an AddValidatorTx.
// It returns the tx outputs that should be returned if this validator is not
// added to the staking set.
func verifyAddValidatorTx(
backend *Backend,
feeCalculator fee.Calculator,
chainState state.Chain,
sTx *txs.Tx,
tx *txs.AddValidatorTx,
) (
[]*lux.TransferableOutput,
error,
) {
currentTimestamp := chainState.GetTimestamp()
if backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) {
return nil, ErrAddValidatorTxPostDurango
}
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return nil, err
}
if err := lux.VerifyMemoFieldLength(tx.Memo, false /*=isDurangoActive*/); err != nil {
return nil, err
}
startTime := tx.StartTime()
duration := tx.EndTime().Sub(startTime)
switch {
case tx.Validator.Wght < backend.Config.MinValidatorStake:
// Ensure validator is staking at least the minimum amount
return nil, ErrWeightTooSmall
case tx.Validator.Wght > backend.Config.MaxValidatorStake:
// Ensure validator isn't staking too much
return nil, ErrWeightTooLarge
case tx.DelegationShares < backend.Config.MinDelegationFee:
// Ensure the validator fee is at least the minimum amount
return nil, ErrInsufficientDelegationFee
case duration < backend.Config.MinStakeDuration:
// Ensure staking length is not too short
return nil, ErrStakeTooShort
case duration > backend.Config.MaxStakeDuration:
// Ensure staking length is not too long
return nil, ErrStakeTooLong
}
outs := make([]*lux.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts))
copy(outs, tx.Outs)
copy(outs[len(tx.Outs):], tx.StakeOuts)
if !backend.Bootstrapped.Get() {
return outs, nil
}
if err := verifyStakerStartTime(false /*=isDurangoActive*/, currentTimestamp, startTime); err != nil {
return nil, err
}
_, err := GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID)
if err == nil {
return nil, fmt.Errorf(
"%s is %w of the primary network",
tx.Validator.NodeID,
ErrAlreadyValidator,
)
}
if err != database.ErrNotFound {
return nil, fmt.Errorf(
"failed to find whether %s is a primary network validator: %w",
tx.Validator.NodeID,
err,
)
}
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return nil, err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Ins,
outs,
sTx.Creds,
map[ids.ID]uint64{
backend.Runtime.XAssetID: fee,
},
); err != nil {
return nil, fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return outs, nil
}
// verifyAddChainValidatorTx carries out the validation for an
// AddChainValidatorTx.
func verifyAddChainValidatorTx(
backend *Backend,
feeCalculator fee.Calculator,
chainState state.Chain,
sTx *txs.Tx,
tx *txs.AddChainValidatorTx,
) error {
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return err
}
var (
currentTimestamp = chainState.GetTimestamp()
isDurangoActive = backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp)
)
if err := lux.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil {
return err
}
startTime := currentTimestamp
if !isDurangoActive {
startTime = tx.StartTime()
}
duration := tx.EndTime().Sub(startTime)
switch {
case duration < backend.Config.MinStakeDuration:
// Ensure staking length is not too short
return ErrStakeTooShort
case duration > backend.Config.MaxStakeDuration:
// Ensure staking length is not too long
return ErrStakeTooLong
}
if !backend.Bootstrapped.Get() {
return nil
}
if err := verifyStakerStartTime(isDurangoActive, currentTimestamp, startTime); err != nil {
return err
}
_, err := GetValidator(chainState, tx.ChainValidator.Chain, tx.Validator.NodeID)
if err == nil {
return fmt.Errorf(
"attempted to issue %w for %s on net %s",
ErrDuplicateValidator,
tx.Validator.NodeID,
tx.ChainValidator.Chain,
)
}
if err != database.ErrNotFound {
return fmt.Errorf(
"failed to find whether %s is a net validator: %w",
tx.Validator.NodeID,
err,
)
}
if err := verifyChainValidatorPrimaryNetworkRequirements(isDurangoActive, chainState, tx.Validator); err != nil {
return err
}
baseTxCreds, err := verifyPoAChainAuthorization(backend.Fx, chainState, sTx, tx.ChainValidator.Chain, tx.ChainAuth)
if err != nil {
return err
}
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Ins,
tx.Outs,
baseTxCreds,
map[ids.ID]uint64{
backend.Runtime.XAssetID: fee,
},
); err != nil {
return fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return nil
}
// Returns the representation of [tx.NodeID] validating [tx.Chain].
// Returns true if [tx.NodeID] is a current validator of [tx.Chain].
// Returns an error if the given tx is invalid.
// The transaction is valid if:
// * [tx.NodeID] is a current/pending PoA validator of [tx.Chain].
// * [sTx]'s creds authorize it to spend the stated inputs.
// * [sTx]'s creds authorize it to remove a validator from [tx.Chain].
// * The flow checker passes.
func verifyRemoveChainValidatorTx(
backend *Backend,
feeCalculator fee.Calculator,
chainState state.Chain,
sTx *txs.Tx,
tx *txs.RemoveChainValidatorTx,
) (*state.Staker, bool, error) {
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return nil, false, err
}
var (
currentTimestamp = chainState.GetTimestamp()
isDurangoActive = backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp)
)
if err := lux.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil {
return nil, false, err
}
isCurrentValidator := true
vdr, err := chainState.GetCurrentValidator(tx.Chain, tx.NodeID)
if err == database.ErrNotFound {
vdr, err = chainState.GetPendingValidator(tx.Chain, tx.NodeID)
isCurrentValidator = false
}
if err != nil {
// It isn't a current or pending validator.
return nil, false, fmt.Errorf(
"%s %w of %s: %w",
tx.NodeID,
ErrNotValidator,
tx.Chain,
err,
)
}
if !vdr.Priority.IsPermissionedValidator() {
return nil, false, ErrRemovePermissionlessValidator
}
if !backend.Bootstrapped.Get() {
// Not bootstrapped yet -- don't need to do full verification.
return vdr, isCurrentValidator, nil
}
baseTxCreds, err := verifyChainAuthorization(backend.Fx, chainState, sTx, tx.Chain, tx.ChainAuth)
if err != nil {
return nil, false, err
}
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return nil, false, err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Ins,
tx.Outs,
baseTxCreds,
map[ids.ID]uint64{
backend.Runtime.XAssetID: fee,
},
); err != nil {
return nil, false, fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return vdr, isCurrentValidator, nil
}
// verifyAddDelegatorTx carries out the validation for an AddDelegatorTx.
// It returns the tx outputs that should be returned if this delegator is not
// added to the staking set.
func verifyAddDelegatorTx(
backend *Backend,
feeCalculator fee.Calculator,
chainState state.Chain,
sTx *txs.Tx,
tx *txs.AddDelegatorTx,
) (
[]*lux.TransferableOutput,
error,
) {
currentTimestamp := chainState.GetTimestamp()
if backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp) {
return nil, ErrAddDelegatorTxPostDurango
}
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return nil, err
}
if err := lux.VerifyMemoFieldLength(tx.Memo, false /*=isDurangoActive*/); err != nil {
return nil, err
}
var (
endTime = tx.EndTime()
startTime = tx.StartTime()
duration = endTime.Sub(startTime)
)
switch {
case duration < backend.Config.MinStakeDuration:
// Ensure staking length is not too short
return nil, ErrStakeTooShort
case duration > backend.Config.MaxStakeDuration:
// Ensure staking length is not too long
return nil, ErrStakeTooLong
case tx.Validator.Wght < backend.Config.MinDelegatorStake:
// Ensure validator is staking at least the minimum amount
return nil, ErrWeightTooSmall
}
outs := make([]*lux.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts))
copy(outs, tx.Outs)
copy(outs[len(tx.Outs):], tx.StakeOuts)
if !backend.Bootstrapped.Get() {
return outs, nil
}
if err := verifyStakerStartTime(false /*=isDurangoActive*/, currentTimestamp, startTime); err != nil {
return nil, err
}
primaryNetworkValidator, err := GetValidator(chainState, constants.PrimaryNetworkID, tx.Validator.NodeID)
if err != nil {
return nil, fmt.Errorf(
"failed to fetch the primary network validator for %s: %w",
tx.Validator.NodeID,
err,
)
}
maximumWeight, err := safemath.Mul64(MaxValidatorWeightFactor, primaryNetworkValidator.Weight)
if err != nil {
return nil, ErrStakeOverflow
}
if backend.Config.UpgradeConfig.IsApricotPhase3Activated(currentTimestamp) {
maximumWeight = min(maximumWeight, backend.Config.MaxValidatorStake)
}
if !txs.BoundedBy(
startTime,
endTime,
primaryNetworkValidator.StartTime,
primaryNetworkValidator.EndTime,
) {
return nil, ErrPeriodMismatch
}
overDelegated, err := overDelegated(
chainState,
primaryNetworkValidator,
maximumWeight,
tx.Validator.Wght,
startTime,
endTime,
)
if err != nil {
return nil, err
}
if overDelegated {
return nil, ErrOverDelegated
}
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return nil, err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Ins,
outs,
sTx.Creds,
map[ids.ID]uint64{
backend.Runtime.XAssetID: fee,
},
); err != nil {
return nil, fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return outs, nil
}
// verifyAddPermissionlessValidatorTx carries out the validation for an
// AddPermissionlessValidatorTx.
func verifyAddPermissionlessValidatorTx(
backend *Backend,
feeCalculator fee.Calculator,
chainState state.Chain,
sTx *txs.Tx,
tx *txs.AddPermissionlessValidatorTx,
) error {
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return err
}
var (
currentTimestamp = chainState.GetTimestamp()
isDurangoActive = backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp)
)
if err := lux.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil {
return err
}
if !backend.Bootstrapped.Get() {
return nil
}
startTime := currentTimestamp
if !isDurangoActive {
startTime = tx.StartTime()
}
duration := tx.EndTime().Sub(startTime)
if err := verifyStakerStartTime(isDurangoActive, currentTimestamp, startTime); err != nil {
return err
}
validatorRules, err := getValidatorRules(backend, chainState, tx.Chain)
if err != nil {
return err
}
stakedAssetID := tx.StakeOuts[0].AssetID()
switch {
case tx.Validator.Wght < validatorRules.minValidatorStake:
// Ensure validator is staking at least the minimum amount
return ErrWeightTooSmall
case tx.Validator.Wght > validatorRules.maxValidatorStake:
// Ensure validator isn't staking too much
return ErrWeightTooLarge
case tx.DelegationShares < validatorRules.minDelegationFee:
// Ensure the validator fee is at least the minimum amount
return ErrInsufficientDelegationFee
case duration < validatorRules.minStakeDuration:
// Ensure staking length is not too short
return ErrStakeTooShort
case duration > validatorRules.maxStakeDuration:
// Ensure staking length is not too long
return ErrStakeTooLong
case stakedAssetID != validatorRules.assetID:
// Wrong assetID used
return fmt.Errorf(
"%w: %s != %s",
ErrWrongStakedAssetID,
validatorRules.assetID,
stakedAssetID,
)
}
_, err = GetValidator(chainState, tx.Chain, tx.Validator.NodeID)
if err == nil {
return fmt.Errorf(
"%w: %s on %s",
ErrDuplicateValidator,
tx.Validator.NodeID,
tx.Chain,
)
}
if err != database.ErrNotFound {
return fmt.Errorf(
"failed to find whether %s is a validator on %s: %w",
tx.Validator.NodeID,
tx.Chain,
err,
)
}
if tx.Chain != constants.PrimaryNetworkID {
if err := verifyChainValidatorPrimaryNetworkRequirements(isDurangoActive, chainState, tx.Validator); err != nil {
return err
}
}
outs := make([]*lux.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts))
copy(outs, tx.Outs)
copy(outs[len(tx.Outs):], tx.StakeOuts)
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Ins,
outs,
sTx.Creds,
map[ids.ID]uint64{
backend.Runtime.XAssetID: fee,
},
); err != nil {
return fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return nil
}
// verifyAddPermissionlessDelegatorTx carries out the validation for an
// AddPermissionlessDelegatorTx.
func verifyAddPermissionlessDelegatorTx(
backend *Backend,
feeCalculator fee.Calculator,
chainState state.Chain,
sTx *txs.Tx,
tx *txs.AddPermissionlessDelegatorTx,
) error {
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return err
}
var (
currentTimestamp = chainState.GetTimestamp()
isDurangoActive = backend.Config.UpgradeConfig.IsDurangoActivated(currentTimestamp)
)
if err := lux.VerifyMemoFieldLength(tx.Memo, isDurangoActive); err != nil {
return err
}
if !backend.Bootstrapped.Get() {
return nil
}
var (
endTime = tx.EndTime()
startTime = currentTimestamp
)
if !isDurangoActive {
startTime = tx.StartTime()
}
duration := endTime.Sub(startTime)
if err := verifyStakerStartTime(isDurangoActive, currentTimestamp, startTime); err != nil {
return err
}
delegatorRules, err := getDelegatorRules(backend, chainState, tx.Chain)
if err != nil {
return err
}
stakedAssetID := tx.StakeOuts[0].AssetID()
switch {
case tx.Validator.Wght < delegatorRules.minDelegatorStake:
// Ensure delegator is staking at least the minimum amount
return ErrWeightTooSmall
case duration < delegatorRules.minStakeDuration:
// Ensure staking length is not too short
return ErrStakeTooShort
case duration > delegatorRules.maxStakeDuration:
// Ensure staking length is not too long
return ErrStakeTooLong
case stakedAssetID != delegatorRules.assetID:
// Wrong assetID used
return fmt.Errorf(
"%w: %s != %s",
ErrWrongStakedAssetID,
delegatorRules.assetID,
stakedAssetID,
)
}
validator, err := GetValidator(chainState, tx.Chain, tx.Validator.NodeID)
if err != nil {
return fmt.Errorf(
"failed to fetch the validator for %s on %s: %w",
tx.Validator.NodeID,
tx.Chain,
err,
)
}
maximumWeight, err := safemath.Mul64(
uint64(delegatorRules.maxValidatorWeightFactor),
validator.Weight,
)
if err != nil {
maximumWeight = math.MaxUint64
}
maximumWeight = min(maximumWeight, delegatorRules.maxValidatorStake)
if !txs.BoundedBy(
startTime,
endTime,
validator.StartTime,
validator.EndTime,
) {
return ErrPeriodMismatch
}
overDelegated, err := overDelegated(
chainState,
validator,
maximumWeight,
tx.Validator.Wght,
startTime,
endTime,
)
if err != nil {
return err
}
if overDelegated {
return ErrOverDelegated
}
outs := make([]*lux.TransferableOutput, len(tx.Outs)+len(tx.StakeOuts))
copy(outs, tx.Outs)
copy(outs[len(tx.Outs):], tx.StakeOuts)
if tx.Chain != constants.PrimaryNetworkID {
// Invariant: Delegators must only be able to reference validator
// transactions that implement [txs.ValidatorTx]. All
// validator transactions implement this interface except the
// AddChainValidatorTx. AddChainValidatorTx is the only
// permissioned validator, so we verify this delegator is
// pointing to a permissionless validator.
if validator.Priority.IsPermissionedValidator() {
return ErrDelegateToPermissionedValidator
}
}
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Ins,
outs,
sTx.Creds,
map[ids.ID]uint64{
backend.Runtime.XAssetID: fee,
},
); err != nil {
return fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return nil
}
// Returns an error if the given tx is invalid.
// The transaction is valid if:
// * [sTx]'s creds authorize it to spend the stated inputs.
// * [sTx]'s creds authorize it to transfer ownership of [tx.Chain].
// * The flow checker passes.
func verifyTransferChainOwnershipTx(
backend *Backend,
feeCalculator fee.Calculator,
chainState state.Chain,
sTx *txs.Tx,
tx *txs.TransferChainOwnershipTx,
) error {
var (
currentTimestamp = chainState.GetTimestamp()
upgrades = backend.Config.UpgradeConfig
)
if !upgrades.IsDurangoActivated(currentTimestamp) {
return ErrDurangoUpgradeNotActive
}
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return err
}
if err := lux.VerifyMemoFieldLength(tx.Memo, true /*=isDurangoActive*/); err != nil {
return err
}
if !backend.Bootstrapped.Get() {
// Not bootstrapped yet -- don't need to do full verification.
return nil
}
baseTxCreds, err := verifyChainAuthorization(backend.Fx, chainState, sTx, tx.Chain, tx.ChainAuth)
if err != nil {
return err
}
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Ins,
tx.Outs,
baseTxCreds,
map[ids.ID]uint64{
backend.Runtime.XAssetID: fee,
},
); err != nil {
return fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return nil
}
// Ensure the proposed validator starts after the current time
func verifyStakerStartTime(isDurangoActive bool, chainTime, stakerTime time.Time) error {
// Pre Durango activation, start time must be after current chain time.
// Post Durango activation, start time is not validated
if isDurangoActive {
return nil
}
if !chainTime.Before(stakerTime) {
return fmt.Errorf(
"%w: %s >= %s",
ErrTimestampNotBeforeStartTime,
chainTime,
stakerTime,
)
}
return nil
}