Files
node/vms/platformvm/txs/executor/staker_tx_verification.go
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

586 lines
15 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"
"github.com/luxfi/node/vms/platformvm/state"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/vms/platformvm/txs/fee"
lux "github.com/luxfi/utxo"
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")
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")
ErrDuplicateValidator = errors.New("duplicate validator")
ErrDelegateToPermissionedValidator = errors.New("delegation to permissioned validator")
ErrWrongStakedAssetID = errors.New("incorrect staked assetID")
ErrAddValidatorTxNotPermitted = errors.New("AddValidatorTx is not permitted")
ErrAddDelegatorTxNotPermitted = errors.New("AddDelegatorTx is not permitted")
)
// verifyChainValidatorPrimaryNetworkRequirements verifies the primary
// network requirements for [chainValidator]. An error is returned if they
// are not fulfilled.
func verifyChainValidatorPrimaryNetworkRequirements(
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.
if !txs.BoundedBy(
chainState.GetTimestamp(),
chainValidator.EndTime(),
primaryNetworkValidator.StartTime,
primaryNetworkValidator.EndTime,
) {
return ErrPeriodMismatch
}
return nil
}
// verifyAddValidatorTx permanently rejects AddValidatorTx; the legacy
// scheduled-staker flow has no role under activate-all-implicitly.
func verifyAddValidatorTx(
*Backend,
fee.Calculator,
state.Chain,
*txs.Tx,
*txs.AddValidatorTx,
) (
[]*lux.TransferableOutput,
error,
) {
return nil, ErrAddValidatorTxNotPermitted
}
// 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
}
if err := lux.VerifyMemoFieldLength(tx.Memo(), true); err != nil {
return err
}
currentTimestamp := chainState.GetTimestamp()
startTime := currentTimestamp
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(currentTimestamp, startTime); err != nil {
return err
}
_, err := GetValidator(chainState, tx.Chain(), tx.Validator().NodeID)
if err == nil {
return fmt.Errorf(
"attempted to issue %w for %s on net %s",
ErrDuplicateValidator,
tx.Validator().NodeID,
tx.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(chainState, tx.Validator()); err != nil {
return err
}
baseTxCreds, err := verifyPoAChainAuthorization(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.Inputs(),
tx.Outputs(),
baseTxCreds,
map[ids.ID]uint64{
backend.Runtime.UTXOAssetID: 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
}
if err := lux.VerifyMemoFieldLength(tx.Memo(), true); 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.Inputs(),
tx.Outputs(),
baseTxCreds,
map[ids.ID]uint64{
backend.Runtime.UTXOAssetID: fee,
},
); err != nil {
return nil, false, fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return vdr, isCurrentValidator, nil
}
// verifyAddDelegatorTx permanently rejects AddDelegatorTx; the legacy
// scheduled-staker flow has no role under activate-all-implicitly.
func verifyAddDelegatorTx(
*Backend,
fee.Calculator,
state.Chain,
*txs.Tx,
*txs.AddDelegatorTx,
) (
[]*lux.TransferableOutput,
error,
) {
return nil, ErrAddDelegatorTxNotPermitted
}
// 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
}
if err := lux.VerifyMemoFieldLength(tx.Memo(), true); err != nil {
return err
}
if !backend.Bootstrapped.Get() {
return nil
}
currentTimestamp := chainState.GetTimestamp()
startTime := currentTimestamp
duration := tx.EndTime().Sub(startTime)
if err := verifyStakerStartTime(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(chainState, tx.Validator()); err != nil {
return err
}
}
outs := make([]*lux.TransferableOutput, len(tx.Outputs())+len(tx.StakeOuts()))
copy(outs, tx.Outputs())
copy(outs[len(tx.Outputs()):], tx.StakeOuts())
// Verify the flowcheck
fee, err := feeCalculator.CalculateFee(tx)
if err != nil {
return err
}
if err := backend.FlowChecker.VerifySpend(
tx,
chainState,
tx.Inputs(),
outs,
sTx.Creds,
map[ids.ID]uint64{
backend.Runtime.UTXOAssetID: 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
}
if err := lux.VerifyMemoFieldLength(tx.Memo(), true); err != nil {
return err
}
if !backend.Bootstrapped.Get() {
return nil
}
var (
currentTimestamp = chainState.GetTimestamp()
endTime = tx.EndTime()
startTime = currentTimestamp
)
duration := endTime.Sub(startTime)
if err := verifyStakerStartTime(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.Mul(
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.Outputs())+len(tx.StakeOuts()))
copy(outs, tx.Outputs())
copy(outs[len(tx.Outputs()):], 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.Inputs(),
outs,
sTx.Creds,
map[ids.ID]uint64{
backend.Runtime.UTXOAssetID: 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 {
// Verify the tx is well-formed
if err := sTx.SyntacticVerify(backend.Runtime); err != nil {
return err
}
if err := lux.VerifyMemoFieldLength(tx.Memo(), true); 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.Inputs(),
tx.Outputs(),
baseTxCreds,
map[ids.ID]uint64{
backend.Runtime.UTXOAssetID: fee,
},
); err != nil {
return fmt.Errorf("%w: %w", ErrFlowCheckFailed, err)
}
return nil
}
// verifyStakerStartTime accepts every pair: a staker's start time is not
// validated against the chain time, so both arguments are ignored.
func verifyStakerStartTime(_, _ time.Time) error {
return nil
}