mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM. Executor semantics (standard_tx_executor): - registerOwnSet(): shared primitive — per-validator state.L1Validator with native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion manager-authority recording (byte-for-byte legacy tail). - ConvertNetworkTx: promote endomorphism (owner-authorized, folds old ConvertNetworkToL1Tx), gated by security.Mode.Manager. - CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set. - Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx. - txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec. All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free; wallet/network/primary off txs.Codec. KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads tx.Chains() only to derive managerChainID — it does NOT create the genesis chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
575 lines
15 KiB
Go
575 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"
|
|
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/math"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/node/vms/platformvm/block"
|
|
"github.com/luxfi/node/vms/platformvm/config"
|
|
"github.com/luxfi/node/vms/platformvm/metrics"
|
|
"github.com/luxfi/node/vms/platformvm/state"
|
|
"github.com/luxfi/node/vms/platformvm/status"
|
|
"github.com/luxfi/node/vms/platformvm/txs"
|
|
txexecutor "github.com/luxfi/node/vms/platformvm/txs/executor"
|
|
"github.com/luxfi/vm/chains/atomic"
|
|
|
|
"github.com/luxfi/node/vms/components/gas"
|
|
txfee "github.com/luxfi/node/vms/platformvm/txs/fee"
|
|
validatorfee "github.com/luxfi/node/vms/platformvm/validators/fee"
|
|
)
|
|
|
|
var (
|
|
_ block.Visitor = (*verifier)(nil)
|
|
|
|
ErrConflictingBlockTxs = errors.New("block contains conflicting transactions")
|
|
ErrStandardBlockWithoutChanges = errors.New("StandardBlock performs no state changes")
|
|
|
|
errIncorrectBlockHeight = errors.New("incorrect block height")
|
|
errOptionBlockTimestampNotMatchingParent = errors.New("option block proposed timestamp not matching parent block one")
|
|
)
|
|
|
|
// verifier handles the logic for verifying a block.
|
|
type verifier struct {
|
|
*backend
|
|
txExecutorBackend *txexecutor.Backend
|
|
pChainHeight uint64
|
|
}
|
|
|
|
func (v *verifier) AbortBlock(b *block.AbortBlock) error {
|
|
if err := v.optionBlock(b); err != nil {
|
|
return err
|
|
}
|
|
return v.abortBlock(b) // Must be the last validity check on the block
|
|
}
|
|
|
|
func (v *verifier) CommitBlock(b *block.CommitBlock) error {
|
|
if err := v.optionBlock(b); err != nil {
|
|
return err
|
|
}
|
|
return v.commitBlock(b) // Must be the last validity check on the block
|
|
}
|
|
|
|
func (v *verifier) ProposalBlock(b *block.ProposalBlock) error {
|
|
if err := v.nonOptionBlock(b); err != nil {
|
|
return err
|
|
}
|
|
|
|
parentID := b.Parent()
|
|
onDecisionState, err := state.NewDiff(parentID, v.backend)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Advance the time to [nextChainTime].
|
|
nextChainTime := b.Timestamp()
|
|
if _, err := txexecutor.AdvanceTimeTo(v.txExecutorBackend, onDecisionState, nextChainTime); err != nil {
|
|
return err
|
|
}
|
|
|
|
feeCalculator := state.PickFeeCalculator(v.txExecutorBackend.Config, onDecisionState)
|
|
inputs, atomicRequests, onAcceptFunc, gasConsumed, _, err := v.processStandardTxs(
|
|
b.Txs(),
|
|
feeCalculator,
|
|
onDecisionState,
|
|
b.Parent(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
onCommitState, err := state.NewDiffOn(onDecisionState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
onAbortState, err := state.NewDiffOn(onDecisionState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return v.proposalBlock( // Must be the last validity check on the block
|
|
b,
|
|
b.Tx(),
|
|
onDecisionState,
|
|
gasConsumed,
|
|
onCommitState,
|
|
onAbortState,
|
|
feeCalculator,
|
|
inputs,
|
|
atomicRequests,
|
|
onAcceptFunc,
|
|
)
|
|
}
|
|
|
|
func (v *verifier) StandardBlock(b *block.StandardBlock) error {
|
|
if err := v.nonOptionBlock(b); err != nil {
|
|
return err
|
|
}
|
|
|
|
parentID := b.Parent()
|
|
onAcceptState, err := state.NewDiff(parentID, v.backend)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Advance the time to [b.Timestamp()].
|
|
changed, err := txexecutor.AdvanceTimeTo(
|
|
v.txExecutorBackend,
|
|
onAcceptState,
|
|
b.Timestamp(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
feeCalculator := state.PickFeeCalculator(v.txExecutorBackend.Config, onAcceptState)
|
|
return v.standardBlock( // Must be the last validity check on the block
|
|
b,
|
|
b.Txs(),
|
|
feeCalculator,
|
|
onAcceptState,
|
|
changed,
|
|
)
|
|
}
|
|
|
|
func (v *verifier) optionBlock(b block.TimestampedBlock) error {
|
|
if err := v.commonBlock(b); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Option blocks must be uniquely generated from the parent ProposalBlock.
|
|
// This means that the timestamp must be standardized to the parent's
|
|
// timestamp.
|
|
parentID := b.Parent()
|
|
parentBlkTime := v.getTimestamp(parentID)
|
|
blkTime := b.Timestamp()
|
|
if !blkTime.Equal(parentBlkTime) {
|
|
return fmt.Errorf(
|
|
"%w parent block timestamp (%s) option block timestamp (%s)",
|
|
errOptionBlockTimestampNotMatchingParent,
|
|
parentBlkTime,
|
|
blkTime,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *verifier) nonOptionBlock(b block.TimestampedBlock) error {
|
|
if err := v.commonBlock(b); err != nil {
|
|
return err
|
|
}
|
|
|
|
parentID := b.Parent()
|
|
parentState, ok := v.GetState(parentID)
|
|
if !ok {
|
|
return fmt.Errorf("%w: %s", state.ErrMissingParentState, parentID)
|
|
}
|
|
|
|
newChainTime := b.Timestamp()
|
|
now := v.txExecutorBackend.Clk.Time()
|
|
return txexecutor.VerifyNewChainTime(
|
|
v.txExecutorBackend.Config.ValidatorFeeConfig,
|
|
newChainTime,
|
|
now,
|
|
parentState,
|
|
)
|
|
}
|
|
|
|
func (v *verifier) commonBlock(b block.Block) error {
|
|
parentID := b.Parent()
|
|
parent, err := v.GetBlock(parentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
expectedHeight := parent.Height() + 1
|
|
height := b.Height()
|
|
if expectedHeight != height {
|
|
return fmt.Errorf(
|
|
"%w expected %d, but found %d",
|
|
errIncorrectBlockHeight,
|
|
expectedHeight,
|
|
height,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// abortBlock populates the state of this block if [nil] is returned.
|
|
//
|
|
// Invariant: The call to abortBlock must be the last validity check on the
|
|
// block. If this function returns [nil], the block is cached as valid.
|
|
func (v *verifier) abortBlock(b block.Block) error {
|
|
parentID := b.Parent()
|
|
onAbortState, ok := v.getOnAbortState(parentID)
|
|
if !ok {
|
|
return fmt.Errorf("%w: %s", state.ErrMissingParentState, parentID)
|
|
}
|
|
|
|
blkID := b.ID()
|
|
v.setBlockState(blkID, &blockState{
|
|
statelessBlock: b,
|
|
onAcceptState: onAbortState,
|
|
timestamp: onAbortState.GetTimestamp(),
|
|
verifiedHeights: set.Of(v.pChainHeight),
|
|
metrics: calculateBlockMetrics(
|
|
v.txExecutorBackend.Config,
|
|
b,
|
|
onAbortState,
|
|
0,
|
|
),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// commitBlock populates the state of this block if [nil] is returned.
|
|
//
|
|
// Invariant: The call to commitBlock must be the last validity check on the
|
|
// block. If this function returns [nil], the block is cached as valid.
|
|
func (v *verifier) commitBlock(b block.Block) error {
|
|
parentID := b.Parent()
|
|
onCommitState, ok := v.getOnCommitState(parentID)
|
|
if !ok {
|
|
return fmt.Errorf("%w: %s", state.ErrMissingParentState, parentID)
|
|
}
|
|
|
|
blkID := b.ID()
|
|
v.setBlockState(blkID, &blockState{
|
|
statelessBlock: b,
|
|
onAcceptState: onCommitState,
|
|
timestamp: onCommitState.GetTimestamp(),
|
|
verifiedHeights: set.Of(v.pChainHeight),
|
|
metrics: calculateBlockMetrics(
|
|
v.txExecutorBackend.Config,
|
|
b,
|
|
onCommitState,
|
|
0,
|
|
),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// proposalBlock populates the state of this block if [nil] is returned.
|
|
//
|
|
// Invariant: The call to proposalBlock must be the last validity check on the
|
|
// block. If this function returns [nil], the block is cached as valid.
|
|
func (v *verifier) proposalBlock(
|
|
b block.Block,
|
|
tx *txs.Tx,
|
|
onDecisionState state.Diff,
|
|
gasConsumed gas.Gas,
|
|
onCommitState state.Diff,
|
|
onAbortState state.Diff,
|
|
feeCalculator txfee.Calculator,
|
|
inputs set.Set[ids.ID],
|
|
atomicRequests map[ids.ID]*atomic.Requests,
|
|
onAcceptFunc func(),
|
|
) error {
|
|
err := txexecutor.ProposalTx(
|
|
v.txExecutorBackend,
|
|
feeCalculator,
|
|
tx,
|
|
onCommitState,
|
|
onAbortState,
|
|
)
|
|
if err != nil {
|
|
txID := tx.ID()
|
|
v.MarkDropped(txID, err) // cache tx as dropped
|
|
return err
|
|
}
|
|
|
|
onCommitState.AddTx(tx, status.Committed)
|
|
onAbortState.AddTx(tx, status.Aborted)
|
|
|
|
v.Mempool.Remove(tx)
|
|
|
|
blkID := b.ID()
|
|
v.setBlockState(blkID, &blockState{
|
|
proposalBlockState: proposalBlockState{
|
|
onDecisionState: onDecisionState,
|
|
onCommitState: onCommitState,
|
|
onAbortState: onAbortState,
|
|
},
|
|
|
|
statelessBlock: b,
|
|
|
|
onAcceptFunc: onAcceptFunc,
|
|
|
|
inputs: inputs,
|
|
// It is safe to use [b.onAbortState] here because the timestamp will
|
|
// never be modified by a legacy abort block and the timestamp will
|
|
timestamp: onAbortState.GetTimestamp(),
|
|
atomicRequests: atomicRequests,
|
|
verifiedHeights: set.Of(v.pChainHeight),
|
|
metrics: calculateBlockMetrics(
|
|
v.txExecutorBackend.Config,
|
|
b,
|
|
onCommitState,
|
|
gasConsumed,
|
|
),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// standardBlock populates the state of this block if [nil] is returned.
|
|
//
|
|
// Invariant: The call to standardBlock must be the last validity check on the
|
|
// block. If this function returns [nil], the block is cached as valid.
|
|
func (v *verifier) standardBlock(
|
|
b block.Block,
|
|
txs []*txs.Tx,
|
|
feeCalculator txfee.Calculator,
|
|
onAcceptState state.Diff,
|
|
changedDuringAdvanceTime bool,
|
|
) error {
|
|
inputs, atomicRequests, onAcceptFunc, gasConsumed, lowBalanceL1ValidatorsEvicted, err := v.processStandardTxs(
|
|
txs,
|
|
feeCalculator,
|
|
onAcceptState,
|
|
b.Parent(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify that the block performs changes. If it does not, it never should
|
|
// have been issued.
|
|
if hasChanges := changedDuringAdvanceTime || len(txs) > 0 || lowBalanceL1ValidatorsEvicted; !hasChanges {
|
|
return ErrStandardBlockWithoutChanges
|
|
}
|
|
|
|
v.Mempool.Remove(txs...)
|
|
|
|
blkID := b.ID()
|
|
v.setBlockState(blkID, &blockState{
|
|
statelessBlock: b,
|
|
|
|
onAcceptState: onAcceptState,
|
|
onAcceptFunc: onAcceptFunc,
|
|
|
|
timestamp: onAcceptState.GetTimestamp(),
|
|
inputs: inputs,
|
|
atomicRequests: atomicRequests,
|
|
verifiedHeights: set.Of(v.pChainHeight),
|
|
metrics: calculateBlockMetrics(
|
|
v.txExecutorBackend.Config,
|
|
b,
|
|
onAcceptState,
|
|
gasConsumed,
|
|
),
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (v *verifier) processStandardTxs(txs []*txs.Tx, feeCalculator txfee.Calculator, diff state.Diff, parentID ids.ID) (
|
|
set.Set[ids.ID],
|
|
map[ids.ID]*atomic.Requests,
|
|
func(),
|
|
gas.Gas,
|
|
bool,
|
|
error,
|
|
) {
|
|
// Complexity is limited first to avoid processing too large of a block.
|
|
var gasConsumed gas.Gas
|
|
{
|
|
var blockComplexity gas.Dimensions
|
|
for _, tx := range txs {
|
|
txComplexity, err := txfee.TxComplexity(tx.Unsigned)
|
|
if err != nil {
|
|
txID := tx.ID()
|
|
v.MarkDropped(txID, err)
|
|
return nil, nil, nil, 0, false, err
|
|
}
|
|
|
|
blockComplexity, err = blockComplexity.Add(&txComplexity)
|
|
if err != nil {
|
|
return nil, nil, nil, 0, false, fmt.Errorf("block complexity overflow: %w", err)
|
|
}
|
|
}
|
|
|
|
var err error
|
|
gasConsumed, err = blockComplexity.ToGas(v.txExecutorBackend.Config.DynamicFeeConfig.Weights)
|
|
if err != nil {
|
|
return nil, nil, nil, 0, false, fmt.Errorf("block gas overflow: %w", err)
|
|
}
|
|
|
|
// If this block exceeds the available capacity, ConsumeGas will return
|
|
// an error.
|
|
feeState := diff.GetFeeState()
|
|
feeState, err = feeState.ConsumeGas(gasConsumed)
|
|
if err != nil {
|
|
return nil, nil, nil, 0, false, err
|
|
}
|
|
|
|
// Updating the fee state prior to executing the transactions is fine
|
|
// because the fee calculator was already created.
|
|
diff.SetFeeState(feeState)
|
|
}
|
|
|
|
var (
|
|
onAcceptFunc func()
|
|
inputs set.Set[ids.ID]
|
|
funcs = make([]func(), 0, len(txs))
|
|
atomicRequests = make(map[ids.ID]*atomic.Requests)
|
|
)
|
|
for _, tx := range txs {
|
|
txInputs, txAtomicRequests, onAccept, err := txexecutor.StandardTx(
|
|
v.txExecutorBackend,
|
|
feeCalculator,
|
|
tx,
|
|
diff,
|
|
)
|
|
if err != nil {
|
|
txID := tx.ID()
|
|
v.MarkDropped(txID, err) // cache tx as dropped
|
|
return nil, nil, nil, 0, false, err
|
|
}
|
|
// ensure it doesn't overlap with current input batch
|
|
if inputs.Overlaps(txInputs) {
|
|
return nil, nil, nil, 0, false, ErrConflictingBlockTxs
|
|
}
|
|
// Add UTXOs to batch
|
|
inputs = inputs.Union(txInputs)
|
|
|
|
diff.AddTx(tx, status.Committed)
|
|
if onAccept != nil {
|
|
funcs = append(funcs, onAccept)
|
|
}
|
|
|
|
for chainID, txRequests := range txAtomicRequests {
|
|
// Add/merge in the atomic requests represented by [tx]
|
|
chainRequests, exists := atomicRequests[chainID]
|
|
if !exists {
|
|
atomicRequests[chainID] = txRequests
|
|
continue
|
|
}
|
|
|
|
chainRequests.PutRequests = append(chainRequests.PutRequests, txRequests.PutRequests...)
|
|
chainRequests.RemoveRequests = append(chainRequests.RemoveRequests, txRequests.RemoveRequests...)
|
|
}
|
|
}
|
|
|
|
if err := v.verifyUniqueInputs(parentID, inputs); err != nil {
|
|
return nil, nil, nil, 0, false, err
|
|
}
|
|
|
|
if numFuncs := len(funcs); numFuncs == 1 {
|
|
onAcceptFunc = funcs[0]
|
|
} else if numFuncs > 1 {
|
|
onAcceptFunc = func() {
|
|
for _, f := range funcs {
|
|
f()
|
|
}
|
|
}
|
|
}
|
|
|
|
// After processing all the transactions, deactivate any L1 validators that
|
|
// might not have sufficient fee to pay for the next second.
|
|
//
|
|
// This ensures that L1 validators are not undercharged for the next second.
|
|
lowBalanceL1ValidatorsEvicted, err := deactivateLowBalanceL1Validators(
|
|
v.txExecutorBackend.Config.ValidatorFeeConfig,
|
|
diff,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, nil, 0, false, fmt.Errorf("failed to deactivate low balance L1 validators: %w", err)
|
|
}
|
|
|
|
return inputs, atomicRequests, onAcceptFunc, gasConsumed, lowBalanceL1ValidatorsEvicted, nil
|
|
}
|
|
|
|
func calculateBlockMetrics(
|
|
config *config.Internal,
|
|
blk block.Block,
|
|
s state.Chain,
|
|
gasConsumed gas.Gas,
|
|
) metrics.Block {
|
|
var (
|
|
gasState = s.GetFeeState()
|
|
validatorExcess = s.GetL1ValidatorExcess()
|
|
)
|
|
return metrics.Block{
|
|
Block: blk,
|
|
|
|
GasConsumed: gasConsumed,
|
|
GasState: gasState,
|
|
GasPrice: gas.CalculatePrice(
|
|
config.DynamicFeeConfig.MinPrice,
|
|
gasState.Excess,
|
|
config.DynamicFeeConfig.ExcessConversionConstant,
|
|
),
|
|
|
|
ActiveL1Validators: s.NumActiveL1Validators(),
|
|
ValidatorExcess: validatorExcess,
|
|
ValidatorPrice: gas.CalculatePrice(
|
|
config.ValidatorFeeConfig.MinPrice,
|
|
validatorExcess,
|
|
config.ValidatorFeeConfig.ExcessConversionConstant,
|
|
),
|
|
AccruedValidatorFees: s.GetAccruedFees(),
|
|
}
|
|
}
|
|
|
|
// deactivateLowBalanceL1Validators deactivates any L1 validators that might not
|
|
// have sufficient fees to pay for the next second. The returned bool will be
|
|
// true if at least one L1 validator was deactivated.
|
|
func deactivateLowBalanceL1Validators(
|
|
config validatorfee.Config,
|
|
diff state.Diff,
|
|
) (bool, error) {
|
|
var (
|
|
accruedFees = diff.GetAccruedFees()
|
|
validatorFeeState = validatorfee.State{
|
|
Current: gas.Gas(diff.NumActiveL1Validators()),
|
|
Excess: diff.GetL1ValidatorExcess(),
|
|
}
|
|
potentialCost = validatorFeeState.CostOf(
|
|
config,
|
|
1, // 1 second
|
|
)
|
|
)
|
|
potentialAccruedFees, err := math.Add(accruedFees, potentialCost)
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not calculate potentially accrued fees: %w", err)
|
|
}
|
|
|
|
// Invariant: Proposal transactions do not impact L1 validator state.
|
|
l1ValidatorIterator, err := diff.GetActiveL1ValidatorsIterator()
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not iterate over active L1 validators: %w", err)
|
|
}
|
|
|
|
var l1ValidatorsToDeactivate []state.L1Validator
|
|
for l1ValidatorIterator.Next() {
|
|
l1Validator := l1ValidatorIterator.Value()
|
|
// If the validator has exactly the right amount of fee for the next
|
|
// second we should not remove them here.
|
|
//
|
|
// GetActiveL1ValidatorsIterator iterates in order of increasing
|
|
// EndAccumulatedFee, so we can break early.
|
|
if l1Validator.EndAccumulatedFee >= potentialAccruedFees {
|
|
break
|
|
}
|
|
|
|
l1ValidatorsToDeactivate = append(l1ValidatorsToDeactivate, l1Validator)
|
|
}
|
|
|
|
// The iterator must be released prior to attempting to write to the
|
|
// diff.
|
|
l1ValidatorIterator.Release()
|
|
|
|
for _, l1Validator := range l1ValidatorsToDeactivate {
|
|
l1Validator.EndAccumulatedFee = 0
|
|
if err := diff.PutL1Validator(l1Validator); err != nil {
|
|
return false, fmt.Errorf("could not deactivate L1 validator %s: %w", l1Validator.ValidationID, err)
|
|
}
|
|
}
|
|
return len(l1ValidatorsToDeactivate) > 0, nil
|
|
}
|