Files
node/vms/xvm/block/executor/block.go
T
zeekay 14f638c918 xvm: remove execution_root activation gate — values not gates (active, not gated)
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).
2026-06-16 12:59:21 -07:00

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
}