Files
node/vms/xvm/block/builder/builder.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

199 lines
4.9 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"context"
"errors"
"fmt"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/txs/mempool"
"github.com/luxfi/node/vms/xvm/block"
"github.com/luxfi/node/vms/xvm/state"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/timer/mockable"
vmcore "github.com/luxfi/vm"
chain "github.com/luxfi/vm/chain"
blockexecutor "github.com/luxfi/node/vms/xvm/block/executor"
txexecutor "github.com/luxfi/node/vms/xvm/txs/executor"
)
// targetBlockSize is the max block size we aim to produce
const targetBlockSize = 128 * constants.KiB
var (
_ Builder = (*builder)(nil)
ErrNoTransactions = errors.New("no transactions")
)
type Builder interface {
// WaitForEvent waits until there is at least one tx available to the
// builder.
WaitForEvent(ctx context.Context) (vmcore.Message, error)
// BuildBlock can be called to attempt to create a new block
BuildBlock(context.Context) (chain.Block, error)
}
// builder implements a simple builder to convert txs into valid blocks
type builder struct {
backend *txexecutor.Backend
manager blockexecutor.Manager
clk *mockable.Clock
// Pool of all txs that may be able to be added
mempool mempool.Mempool[*txs.Tx]
}
func New(
backend *txexecutor.Backend,
manager blockexecutor.Manager,
clk *mockable.Clock,
mempool mempool.Mempool[*txs.Tx],
) Builder {
return &builder{
backend: backend,
manager: manager,
clk: clk,
mempool: mempool,
}
}
func (b *builder) WaitForEvent(ctx context.Context) (vmcore.Message, error) {
return b.mempool.WaitForEvent(ctx)
}
// BuildBlock builds a block to be added to consensus.
func (b *builder) BuildBlock(context.Context) (chain.Block, error) {
if !b.backend.Log.IsZero() {
b.backend.Log.Debug("starting to attempt to build a block")
}
// Get the block to build on top of and retrieve the new block's context.
preferredID := b.manager.Preferred()
preferred, err := b.manager.GetStatelessBlock(preferredID)
if err != nil {
return nil, err
}
preferredHeight := preferred.Height()
preferredTimestamp := preferred.Timestamp()
nextHeight := preferredHeight + 1
nextTimestamp := b.clk.Time() // [timestamp] = max(now, parentTime)
if preferredTimestamp.After(nextTimestamp) {
nextTimestamp = preferredTimestamp
}
stateDiff, err := state.NewDiff(preferredID, b.manager)
if err != nil {
return nil, err
}
var (
blockTxs []*txs.Tx
inputs = set.NewSet[ids.ID]()
remainingSize = targetBlockSize
)
for {
tx, exists := b.mempool.Peek()
// Invariant: [mempool.MaxTxSize] < [targetBlockSize]. This guarantees
// that we will only stop building a block once there are no
// transactions in the mempool or the block is at least
// [targetBlockSize - mempool.MaxTxSize] bytes full.
if !exists || len(tx.Bytes()) > remainingSize {
break
}
b.mempool.Remove(tx)
// Invariant: [tx] has already been syntactically verified.
txDiff, err := state.NewDiffOn(stateDiff)
if err != nil {
return nil, err
}
err = tx.Unsigned.Visit(&txexecutor.SemanticVerifier{
Backend: b.backend,
State: txDiff,
Tx: tx,
})
if err != nil {
txID := tx.ID()
b.mempool.MarkDropped(txID, err)
continue
}
executor := &txexecutor.Executor{
Codec: b.backend.Codec,
State: txDiff,
Tx: tx,
Inputs: set.NewSet[ids.ID](0), // Initialize empty set for imported inputs
}
err = tx.Unsigned.Visit(executor)
if err != nil {
txID := tx.ID()
b.mempool.MarkDropped(txID, err)
continue
}
if inputs.Overlaps(executor.Inputs) {
txID := tx.ID()
b.mempool.MarkDropped(txID, blockexecutor.ErrConflictingBlockTxs)
continue
}
err = b.manager.VerifyUniqueInputs(preferredID, executor.Inputs)
if err != nil {
txID := tx.ID()
b.mempool.MarkDropped(txID, err)
continue
}
inputs = inputs.Union(executor.Inputs)
txDiff.AddTx(tx)
txDiff.Apply(stateDiff)
remainingSize -= len(tx.Bytes())
blockTxs = append(blockTxs, tx)
}
if len(blockTxs) == 0 {
return nil, ErrNoTransactions
}
// Stamp the xvm execution_root computed over the post-block state into the
// block so the verifier can recompute and check it. The root is the tagged
// Merkle-tree fold over the post-block state (utxo ‖ asset ‖ tx ‖ height);
// the builder and the executor share exactly one computation
// (BlockExecutionRoot) so the stamped root and the verified root can never
// disagree.
parentRoot := preferred.MerkleRoot()
root, err := blockexecutor.BlockExecutionRoot(
parentRoot,
blockTxs,
stateDiff,
nextHeight,
)
if err != nil {
return nil, fmt.Errorf("failed to compute block execution root: %w", err)
}
statelessBlk, err := block.NewStandardBlockWithRoot(
preferredID,
nextHeight,
nextTimestamp,
root,
blockTxs,
b.backend.Codec,
)
if err != nil {
return nil, err
}
return b.manager.NewBlock(statelessBlk), nil
}