mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
The prior seam added a bespoke MerkleRootActivationHeight gate (default
MerkleRootNeverActivate=MaxUint64) that violated upgrade/upgrade.go's stated
philosophy ('activate-all-implicitly; the fields encode values, not gates').
Removed it entirely (grep-clean): the builder ALWAYS stamps the real xvm
execution_root, the executor ALWAYS recomputes+verifies it, an empty root is
now rejected. Root computation byte-IDENTICAL (exec_root=4f144ef7…) — only the
gating is gone. Net -92 lines. Tests converted to always-active reality
(empty-root-rejected is the new gate test); ./vms/xvm/... + ./upgrade/... green
uncached + -race. asset_root stays keccak256("") (UTXO-only executor; assets
bound via each UTXO's AssetID).
384 lines
11 KiB
Go
384 lines
11 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package executor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/luxfi/log"
|
|
|
|
"github.com/luxfi/consensus/core/choices"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/node/vms/xvm/block"
|
|
"github.com/luxfi/node/vms/xvm/state"
|
|
"github.com/luxfi/node/vms/xvm/txs/executor"
|
|
chain "github.com/luxfi/vm/chain"
|
|
"github.com/luxfi/vm/chains/atomic"
|
|
)
|
|
|
|
const SyncBound = 10 * time.Second
|
|
|
|
var (
|
|
_ chain.Block = (*Block)(nil)
|
|
|
|
ErrUnexpectedMerkleRoot = errors.New("unexpected merkle root")
|
|
ErrTimestampBeyondSyncBound = errors.New("proposed timestamp is too far in the future relative to local time")
|
|
ErrEmptyBlock = errors.New("block contains no transactions")
|
|
ErrChildBlockEarlierThanParent = errors.New("proposed timestamp before current chain time")
|
|
ErrConflictingBlockTxs = errors.New("block contains conflicting transactions")
|
|
ErrIncorrectHeight = errors.New("block has incorrect height")
|
|
ErrBlockNotFound = errors.New("block not found")
|
|
)
|
|
|
|
// Exported for testing in xvm package.
|
|
type Block struct {
|
|
block.Block
|
|
manager *manager
|
|
}
|
|
|
|
// ParentID returns the parent block ID
|
|
func (b *Block) ParentID() ids.ID {
|
|
return b.Block.Parent()
|
|
}
|
|
|
|
// EpochBit returns the epoch bit for FPC
|
|
func (b *Block) EpochBit() bool {
|
|
return false // XVM blocks don't support epoch bits yet
|
|
}
|
|
|
|
// FPCVotes returns embedded fast-path vote references
|
|
func (b *Block) FPCVotes() [][]byte {
|
|
return nil // XVM blocks don't support FPC votes yet
|
|
}
|
|
|
|
// Status returns the status of this block
|
|
func (b *Block) Status() uint8 {
|
|
blkID := b.ID()
|
|
// If this block is the last accepted block, we don't need to go to disk
|
|
if b.manager.lastAccepted == blkID {
|
|
return uint8(choices.Accepted)
|
|
}
|
|
// Check if the block is in memory. If so, it's processing.
|
|
if _, ok := b.manager.blkIDToState[blkID]; ok {
|
|
return uint8(choices.Processing)
|
|
}
|
|
// Block isn't in memory. Check in the database.
|
|
_, err := b.manager.state.GetBlock(blkID)
|
|
switch err {
|
|
case nil:
|
|
return uint8(choices.Accepted)
|
|
case database.ErrNotFound:
|
|
return uint8(choices.Processing)
|
|
default:
|
|
return uint8(choices.Processing)
|
|
}
|
|
}
|
|
|
|
func (b *Block) Verify(ctx context.Context) error {
|
|
blkID := b.ID()
|
|
if _, ok := b.manager.blkIDToState[blkID]; ok {
|
|
// This block has already been verified.
|
|
return nil
|
|
}
|
|
|
|
// The block's MerkleRoot carries the xvm execution_root over the post-block
|
|
// state. It is verified unconditionally once the parent and the block's txs
|
|
// are available: the executor recomputes the canonical execution_root and
|
|
// rejects any block whose stamped root disagrees (see the recompute below,
|
|
// after the txs have been applied to the state diff).
|
|
merkleRoot := b.Block.MerkleRoot()
|
|
|
|
// Only allow timestamp to reasonably far forward
|
|
newChainTime := b.Timestamp()
|
|
now := b.manager.clk.Time()
|
|
maxNewChainTime := now.Add(SyncBound)
|
|
if newChainTime.After(maxNewChainTime) {
|
|
return fmt.Errorf(
|
|
"%w, proposed time (%s), local time (%s)",
|
|
ErrTimestampBeyondSyncBound,
|
|
newChainTime,
|
|
now,
|
|
)
|
|
}
|
|
|
|
txs := b.Txs()
|
|
if len(txs) == 0 {
|
|
return ErrEmptyBlock
|
|
}
|
|
|
|
// Syntactic verification is generally pretty fast, so we verify this first
|
|
// before performing any possible DB reads.
|
|
for _, tx := range txs {
|
|
err := tx.Unsigned.Visit(&executor.SyntacticVerifier{
|
|
Backend: b.manager.backend,
|
|
Tx: tx,
|
|
})
|
|
if err != nil {
|
|
txID := tx.ID()
|
|
b.manager.mempool.MarkDropped(txID, err)
|
|
return fmt.Errorf("failed to syntactically verify tx %s: %w", txID, err)
|
|
}
|
|
}
|
|
|
|
// GPU batch pre-verification of secp256k1 signatures.
|
|
// This warms the recovery cache and validates all sigs in parallel on GPU
|
|
// when available. Errors are non-fatal; sequential verification handles them.
|
|
_ = batchVerifyBlockSignatures(txs)
|
|
|
|
// Verify that the parent exists.
|
|
parentID := b.Parent()
|
|
parent, err := b.manager.GetStatelessBlock(parentID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get parent %s: %w", parentID, err)
|
|
}
|
|
|
|
// Verify that currentBlkHeight = parentBlkHeight + 1.
|
|
expectedHeight := parent.Height() + 1
|
|
height := b.Height()
|
|
if expectedHeight != height {
|
|
return fmt.Errorf(
|
|
"%w: expected height %d, got %d",
|
|
ErrIncorrectHeight,
|
|
expectedHeight,
|
|
height,
|
|
)
|
|
}
|
|
|
|
stateDiff, err := state.NewDiff(parentID, b.manager)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed to initialize state diff on state at %s: %w",
|
|
parentID,
|
|
err,
|
|
)
|
|
}
|
|
|
|
parentChainTime := stateDiff.GetTimestamp()
|
|
// The proposed timestamp must not be before the parent's timestamp.
|
|
if newChainTime.Before(parentChainTime) {
|
|
return fmt.Errorf(
|
|
"%w: proposed timestamp (%s), chain time (%s)",
|
|
ErrChildBlockEarlierThanParent,
|
|
newChainTime,
|
|
parentChainTime,
|
|
)
|
|
}
|
|
|
|
stateDiff.SetTimestamp(newChainTime)
|
|
|
|
blockState := &blockState{
|
|
statelessBlock: b.Block,
|
|
onAcceptState: stateDiff,
|
|
importedInputs: set.NewSet[ids.ID](0),
|
|
atomicRequests: make(map[ids.ID]*atomic.Requests),
|
|
}
|
|
|
|
for _, tx := range txs {
|
|
// Verify that the tx is valid according to the current state of the
|
|
// chain.
|
|
err := tx.Unsigned.Visit(&executor.SemanticVerifier{
|
|
Backend: b.manager.backend,
|
|
State: stateDiff,
|
|
Tx: tx,
|
|
})
|
|
if err != nil {
|
|
txID := tx.ID()
|
|
b.manager.mempool.MarkDropped(txID, err)
|
|
return fmt.Errorf("failed to semantically verify tx %s: %w", txID, err)
|
|
}
|
|
|
|
// Apply the txs state changes to the state.
|
|
//
|
|
// Note: This must be done inside the same loop as semantic verification
|
|
// to ensure that semantic verification correctly accounts for
|
|
// transactions that occurred earlier in the block.
|
|
executor := &executor.Executor{
|
|
Codec: b.manager.backend.Codec,
|
|
State: stateDiff,
|
|
Tx: tx,
|
|
Inputs: set.NewSet[ids.ID](0),
|
|
}
|
|
err = tx.Unsigned.Visit(executor)
|
|
if err != nil {
|
|
txID := tx.ID()
|
|
b.manager.mempool.MarkDropped(txID, err)
|
|
return fmt.Errorf("failed to execute tx %s: %w", txID, err)
|
|
}
|
|
|
|
// Verify that the transaction we just executed didn't consume inputs
|
|
// that were already imported in a previous transaction.
|
|
if blockState.importedInputs.Overlaps(executor.Inputs) {
|
|
txID := tx.ID()
|
|
b.manager.mempool.MarkDropped(txID, ErrConflictingBlockTxs)
|
|
return ErrConflictingBlockTxs
|
|
}
|
|
// Add the imported inputs from this transaction to the block's imported inputs
|
|
for inputID := range executor.Inputs {
|
|
blockState.importedInputs.Add(inputID)
|
|
}
|
|
|
|
// Now that the tx would be marked as accepted, we should add it to the
|
|
// state for the next transaction in the block.
|
|
stateDiff.AddTx(tx)
|
|
|
|
for chainID, txRequests := range executor.AtomicRequests {
|
|
// Add/merge in the atomic requests represented by [tx]
|
|
chainRequests, exists := blockState.atomicRequests[chainID]
|
|
if !exists {
|
|
blockState.atomicRequests[chainID] = txRequests
|
|
continue
|
|
}
|
|
|
|
chainRequests.PutRequests = append(chainRequests.PutRequests, txRequests.PutRequests...)
|
|
chainRequests.RemoveRequests = append(chainRequests.RemoveRequests, txRequests.RemoveRequests...)
|
|
}
|
|
}
|
|
|
|
// Verify that none of the transactions consumed any inputs that were
|
|
// already imported in a currently processing block.
|
|
err = b.manager.VerifyUniqueInputs(parentID, blockState.importedInputs)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed to verify unique inputs on state at %s: %w",
|
|
parent,
|
|
err,
|
|
)
|
|
}
|
|
|
|
// The block must carry the execution_root over the now-fully-applied
|
|
// post-block state. Recompute it from the same canonical projection the
|
|
// builder stamped (one shared code path — see BlockExecutionRoot) and reject
|
|
// any mismatch. [height] was already resolved (and validated against the
|
|
// parent) above.
|
|
expectedRoot, err := BlockExecutionRoot(parent.MerkleRoot(), txs, stateDiff, height)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to compute expected block execution root: %w", err)
|
|
}
|
|
if merkleRoot != expectedRoot {
|
|
return fmt.Errorf(
|
|
"%w: block root %s, expected %s",
|
|
ErrUnexpectedMerkleRoot,
|
|
merkleRoot,
|
|
expectedRoot,
|
|
)
|
|
}
|
|
|
|
// Now that the block has been executed, we can add the block data to the
|
|
// state diff.
|
|
stateDiff.SetLastAccepted(blkID)
|
|
stateDiff.AddBlock(b.Block)
|
|
|
|
b.manager.blkIDToState[blkID] = blockState
|
|
b.manager.mempool.Remove(txs...)
|
|
return nil
|
|
}
|
|
|
|
func (b *Block) Accept(ctx context.Context) error {
|
|
blkID := b.ID()
|
|
defer b.manager.free(blkID)
|
|
|
|
txs := b.Txs()
|
|
for _, tx := range txs {
|
|
if b.manager.onAccept != nil {
|
|
b.manager.onAccept(tx)
|
|
}
|
|
}
|
|
|
|
b.manager.lastAccepted = blkID
|
|
b.manager.mempool.Remove(txs...)
|
|
|
|
blkState, ok := b.manager.blkIDToState[blkID]
|
|
if !ok {
|
|
return fmt.Errorf("%w: %s", ErrBlockNotFound, blkID)
|
|
}
|
|
|
|
// Update the state to reflect the changes made in [onAcceptState].
|
|
blkState.onAcceptState.Apply(b.manager.state)
|
|
|
|
defer b.manager.state.Abort()
|
|
batch, err := b.manager.state.CommitBatch()
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed to stage state diff for block %s: %w",
|
|
blkID,
|
|
err,
|
|
)
|
|
}
|
|
|
|
// Note that this method writes [batch] to the database.
|
|
// Convert the atomicRequests to interface{} type for SharedMemory
|
|
requests := make(map[ids.ID]interface{}, len(blkState.atomicRequests))
|
|
for chainID, reqs := range blkState.atomicRequests {
|
|
requests[chainID] = reqs
|
|
}
|
|
|
|
// Note that this method writes [batch] to the database.
|
|
if b.manager.backend.SharedMemory != nil {
|
|
if err := b.manager.backend.SharedMemory.Apply(requests, batch); err != nil {
|
|
return fmt.Errorf("failed to apply state diff to shared memory: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := b.manager.metrics.MarkBlockAccepted(b); err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.manager.backend.Runtime != nil {
|
|
if logger, ok := b.manager.backend.Runtime.Log.(interface {
|
|
Trace(string, ...log.Field)
|
|
}); ok {
|
|
logger.Trace(
|
|
"accepted block",
|
|
log.Stringer("blkID", blkID),
|
|
log.Uint64("height", b.Height()),
|
|
log.Stringer("parentID", b.Parent()),
|
|
log.Stringer("checksum", b.manager.state.Checksum()),
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Block) Reject(ctx context.Context) error {
|
|
blkID := b.ID()
|
|
defer b.manager.free(blkID)
|
|
|
|
if b.manager.backend.Log != nil {
|
|
b.manager.backend.Log.Debug(
|
|
"rejecting block",
|
|
"blkID", blkID.String(),
|
|
"height", b.Height(),
|
|
"parentID", b.Parent().String(),
|
|
)
|
|
}
|
|
|
|
for _, tx := range b.Txs() {
|
|
if err := b.manager.VerifyTx(tx); err != nil {
|
|
if b.manager.backend.Log != nil {
|
|
b.manager.backend.Log.Debug("dropping invalidated tx",
|
|
"txID", tx.ID().String(),
|
|
"blkID", blkID.String(),
|
|
"error", err,
|
|
)
|
|
}
|
|
continue
|
|
}
|
|
if err := b.manager.mempool.Add(tx); err != nil {
|
|
if b.manager.backend.Log != nil {
|
|
b.manager.backend.Log.Debug("dropping valid tx",
|
|
"txID", tx.ID().String(),
|
|
"blkID", blkID.String(),
|
|
"error", err,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|