mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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.
828 lines
21 KiB
Go
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
|
|
}
|