mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
X-Chain fully off pcodecs (the last node consumer). kind.go (xkind 1-5), tx.go
(wire.SignedTx envelope), parser (reflect-map + dual LinearCodec DELETED, fx
dispatch by envelope TypeKind/ShapeKind), base/create_asset/operation/import/
export tx, initial_state, operation, block/{standard,parser,builder}, genesis
(native genesis_wire.go: marshalGenesis/parseGenesis + txs.ParseUnsignedTx),
vm/service/static_service/wallet_service/utxo-spender, metrics (pcodecs.Errs ->
errors.Join). ALL xvm test codec threading removed; xvm tests green.
Block-build invariant: writeTxList requires Initialized txs (mempool/parse hand
the builder canonical bytes) — errors on empty tx-bytes rather than a hidden
Initialize side-effect; state_test updated to Initialize its fixtures.
Transport foundation: rpcchainvm NewListener unix-socket fast path gated
LUXD_VM_UNIX_SOCKET=1 (default TCP, non-breaking; api/zap 159b27a infers unix
from socket-path addr). Consumes upstream utxo TransferableOut/In (4ce6a6e).
ZERO real node-side pcodecs consumers remain. vms/pcodecs kept only for 3
external luxfi/chains VMs (Wave B: zkvm/bridgevm/mpcvm) pending their migration.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
197 lines
4.9 KiB
Go
197 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{
|
|
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,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b.manager.NewBlock(statelessBlk), nil
|
|
}
|