Files
zeekay 7cf87b230b node-unified is the node
The module already called itself github.com/luxfi/node while containing no node
in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and
the Go column of the matrix measured a VM plugin. A repo that fetches the thing
it claims to be has not replaced it, so the Go sources move in — 1515 files,
the daemon and everything under it — and the build reaches outside this module
for nothing.

Rust and C++ sit beside it unchanged and are still measured against it by the
differential. That is the point of one repo: the reference implementation and
the two ports are the same checkout, so parity is a thing you can run rather
than a thing you assert.

Verified: builds, and the closure carries no lux-private — checked at build time
rather than believed. The daemon starts twelve chains on a local network
(P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2026-09-07 16:15:25 -07:00

342 lines
14 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"errors"
"fmt"
"runtime"
"sync"
"github.com/luxfi/crypto/merkle"
"github.com/luxfi/ids"
"github.com/luxfi/node/vms/xvm/state"
"github.com/luxfi/node/vms/xvm/state/xvmroot"
"github.com/luxfi/node/vms/xvm/txs"
lux "github.com/luxfi/utxo"
)
// xvmExecutionRoot computes the xvm execution_root over a block's post-block
// state, byte-identical to xvmroot.ExecutionRoot, with the three family subtrees
// (utxo / asset / tx) built concurrently and each family's leaf digests hashed
// in parallel across a GOMAXPROCS-bounded worker pool. It is the single root
// computation shared by the builder (which stamps the result into the block) and
// the executor (which recomputes it to verify the block's stamped root). Having
// exactly one implementation means the producer and the verifier can never
// disagree on the rule.
//
// Byte-identity to the serial xvmroot.ExecutionRoot is structural, not
// best-effort: the canonical leaf ORDER (occupied, ascending slot index) and the
// canonical COMBINE (RFC-6962 lone-right Merkle fold via merkle.Root, then the
// fixed-shape keccak compose) are unchanged — only the per-leaf keccak hashing
// and the three independent family folds run on separate goroutines. Each worker
// writes a disjoint, pre-indexed slot of its family's leaf slice, so the slice
// fed to merkle.Root is identical to the one the serial path builds. A property
// test (TestParallelExecutionRootMatchesSerial) pins this over random state
// sizes including odd leaf counts.
func xvmExecutionRoot(
parentExecutionRoot [xvmroot.Size]byte,
utxos []xvmroot.UTXOLeaf,
assets []xvmroot.AssetLeaf,
leafTxs []xvmroot.TxLeaf,
height uint64,
) (executionRoot, utxoRoot, assetRoot, txRoot [xvmroot.Size]byte) {
var wg sync.WaitGroup
wg.Add(3)
go func() { defer wg.Done(); utxoRoot = parallelUTXORoot(utxos) }()
go func() { defer wg.Done(); assetRoot = parallelAssetRoot(assets) }()
go func() { defer wg.Done(); txRoot = parallelTxRoot(leafTxs) }()
wg.Wait()
executionRoot = xvmroot.Compose(parentExecutionRoot, utxoRoot, assetRoot, txRoot, height)
return executionRoot, utxoRoot, assetRoot, txRoot
}
// parallelUTXORoot is the parallel peer of xvmroot.UTXORoot. It compacts the
// occupied slots to their canonical ascending leaf positions (serial, O(n) and
// cheap), then computes the family root with the parallel fold — byte-identical
// to xvmroot.UTXORoot's merkle.Root over the same compacted digests.
func parallelUTXORoot(utxos []xvmroot.UTXOLeaf) [xvmroot.Size]byte {
idx := make([]int, 0, len(utxos))
for i := range utxos {
if utxos[i].Status&xvmroot.UTXOOccupied == 0 {
continue
}
idx = append(idx, i)
}
return parallelMerkleRoot(len(idx), func(k int) [xvmroot.Size]byte {
return xvmroot.UTXOLeafDigest(utxos[idx[k]], uint32(idx[k]))
})
}
// parallelAssetRoot is the parallel peer of xvmroot.AssetRoot.
func parallelAssetRoot(assets []xvmroot.AssetLeaf) [xvmroot.Size]byte {
idx := make([]int, 0, len(assets))
for i := range assets {
if assets[i].Occupied == 0 {
continue
}
idx = append(idx, i)
}
return parallelMerkleRoot(len(idx), func(k int) [xvmroot.Size]byte {
return xvmroot.AssetLeafDigest(assets[idx[k]], uint32(idx[k]))
})
}
// parallelTxRoot is the parallel peer of xvmroot.TxRoot. Every tx is folded (no
// skip predicate), so leaf position equals slot index.
func parallelTxRoot(leafTxs []xvmroot.TxLeaf) [xvmroot.Size]byte {
return parallelMerkleRoot(len(leafTxs), func(i int) [xvmroot.Size]byte {
return xvmroot.TxLeafDigest(leafTxs[i], uint32(i))
})
}
// parallelMerkleRoot computes the RFC-6962 tagged binary Merkle root over n leaf
// element-digests, where digest(k) yields the k-th leaf's element digest. It is
// byte-identical to merkle.Root(allDigests): it composes merkle's own exported
// LeafHash / NodeHash primitives (so the keccak/tagging spec is not duplicated)
// and follows the same level-by-level reduction with lone-right promotion — only
// the per-level loops run across a GOMAXPROCS-bounded worker pool. Both the
// element-digest stage and every internal merkle level are independent across
// their index range, so parallelizing the loops cannot change the bytes; the
// reduction shape is fixed by the leaf count alone.
//
// n == 0 → merkle.EmptyRoot (keccak256(""))
// n == 1 → merkle.LeafHash(digest(0))
// n > 1 → parallel level reduction
func parallelMerkleRoot(n int, digest func(k int) [xvmroot.Size]byte) [xvmroot.Size]byte {
if n == 0 {
return merkle.EmptyRoot()
}
// Level 0: tag each element digest. merkle.Root tags leaves[i] via
// LeafHash; we compute the element digest and tag it in the same step.
level := make([][xvmroot.Size]byte, n)
parallelFor(n, func(i int) {
level[i] = merkle.LeafHash(digest(i))
})
// Reduce level-by-level. parents = ceil(cnt/2); a lone right node on an odd
// level is promoted unchanged — identical to merkle.Root.
for len(level) > 1 {
cnt := len(level)
parents := (cnt + 1) / 2
pairs := cnt / 2
next := make([][xvmroot.Size]byte, parents)
parallelFor(pairs, func(j int) {
next[j] = merkle.NodeHash(level[2*j], level[2*j+1])
})
if cnt&1 == 1 {
next[parents-1] = level[cnt-1]
}
level = next
}
return level[0]
}
// parallelFor invokes body(i) for every i in [0, n), spread over a
// GOMAXPROCS-bounded pool of worker goroutines partitioning the range into
// contiguous shards. Each i is handled exactly once by one worker, so a body
// that writes a disjoint slot per i needs no further synchronization. Below a
// keccak-tuned threshold it runs inline, where fork/join would cost more than
// the work.
func parallelFor(n int, body func(i int)) {
if n == 0 {
return
}
workers := runtime.GOMAXPROCS(0)
// Below this many keccak invocations the fork/join overhead outweighs the
// parallel hashing; run inline. Tuned to keccak cost, not block size.
const inlineThreshold = 1024
if workers <= 1 || n < inlineThreshold {
for i := 0; i < n; i++ {
body(i)
}
return
}
if workers > n {
workers = n
}
var wg sync.WaitGroup
wg.Add(workers)
// Contiguous shards: the first [rem] workers take one extra element so every
// i in [0,n) is covered exactly once.
base, rem := n/workers, n%workers
start := 0
for w := 0; w < workers; w++ {
size := base
if w < rem {
size++
}
lo, hi := start, start+size
start = hi
go func() {
defer wg.Done()
for i := lo; i < hi; i++ {
body(i)
}
}()
}
wg.Wait()
}
// BlockExecutionRoot resolves the canonical leaf projection for a block and
// returns the execution_root the block must carry at [height]. It is the bridge
// from a stateless block to the pure xvm root computation: the builder calls it
// to stamp the root and the executor calls it to verify the stamped root, so the
// produced root and the verified root are derived by exactly one code path and
// can never disagree.
//
// [parentExecutionRoot] is the parent block's MerkleRoot (the empty root for a
// genesis or pre-activation parent). [postState] is the post-block state (the
// applied state diff) the UTXO/asset families are projected from. The leaf
// projection is the canonical, deterministic, pure-function-of-the-block image
// of the post-block state the root commits to. The tx family is fully realized
// from the block's transactions (TxID = tx.ID(), every tx folded in block
// order, status = accepted); it is identical on the builder and the verifier
// because both observe the same ordered tx set. The UTXO family is enumerated
// from the post-block occupied UTXO set and projected by postBlockUTXOLeaves;
// the asset family is empty (see postBlockAssetLeaves for why xvm's UTXO-only
// executor state has no enumerable asset arena to project).
//
// It returns an error if the post-block state cannot be enumerated or an output
// carries an owner model with no canonical owner_root yet — a wrong root must
// never be stamped or accepted silently.
func BlockExecutionRoot(
parentExecutionRoot ids.ID,
blkTxs []*txs.Tx,
postState state.Chain,
height uint64,
) (ids.ID, error) {
leafTxs := txLeaves(blkTxs)
utxos, err := postBlockUTXOLeaves(postState)
if err != nil {
return ids.Empty, err
}
assets := postBlockAssetLeaves(postState)
executionRoot, _, _, _ := xvmExecutionRoot([xvmroot.Size]byte(parentExecutionRoot), utxos, assets, leafTxs, height)
return ids.ID(executionRoot), nil
}
// postBlockUTXOLeaves projects the post-block state's occupied UTXO set into the
// execution_root UTXO family leaves, in canonical ascending-UTXOID order — the
// deterministic full-set order state.Chain.UTXOs returns and the order xvmroot
// folds. Every enumerated UTXO is occupied (the iterator never returns a spent
// or removed UTXO), so each leaf carries Status = UTXOOccupied and its leaf
// index is its position in the ascending enumeration (0,1,2,…), matching the
// `index` the kernel binds into each occupied slot's leaf preimage.
//
// Each lux.UTXO is projected field-by-field onto the canonical UTXOLeaf the GPU
// state-snapshot producer must match (xvm_gpu_layout.hpp's UTXO struct):
//
// UTXOID = utxo.InputID() (keccak(tx_id ‖ output_index))
// AssetID = utxo.AssetID()
// AmountLo = output amount (uint64) (0 for a non-value output)
// AmountHi = 0 (no xvm output exceeds 2^64-1;
// Amt is a single uint64, so the
// high limb is always 0)
// OwnerRoot = OwnerRoot(extractOwners(out)) (the canonical owner commitment;
// see ownerroot.go — this is the
// Go definition the GPU matches)
// Locktime = owner locktime (0 when the owner has none)
// Threshold = owner threshold (the N-of-M spend parameter)
// Status = UTXOOccupied (the set is the occupied set)
//
// It is a pure, deterministic function of the post-block state: the builder and
// the verifier both call it over the same post-block state, so the stamped and
// verified UTXO roots always agree.
func postBlockUTXOLeaves(postState state.Chain) ([]xvmroot.UTXOLeaf, error) {
utxos, err := postState.UTXOs(ids.Empty, 0) // 0 = unbounded: the full occupied set
if err != nil {
return nil, fmt.Errorf("enumerating post-block UTXO set: %w", err)
}
leaves := make([]xvmroot.UTXOLeaf, len(utxos))
for i, utxo := range utxos {
leaf, err := utxoLeaf(utxo)
if err != nil {
return nil, fmt.Errorf("projecting UTXO %s: %w", utxo.InputID(), err)
}
leaves[i] = leaf
}
return leaves, nil
}
// utxoLeaf projects one lux.UTXO onto its canonical UTXOLeaf field values. See
// postBlockUTXOLeaves for the field-by-field mapping; the owner_root derivation
// is defined in ownerroot.go.
func utxoLeaf(utxo *lux.UTXO) (xvmroot.UTXOLeaf, error) {
o, ok := extractOwners(utxo.Out)
if !ok {
return xvmroot.UTXOLeaf{}, fmt.Errorf("%w: output type %T", errUnsupportedOwnerModel, utxo.Out)
}
var amount uint64
if a, ok := utxo.Out.(lux.Amounter); ok {
amount = a.Amount()
}
return xvmroot.UTXOLeaf{
UTXOID: ids.ID(utxo.InputID()),
AssetID: ids.ID(utxo.AssetID()),
AmountLo: amount,
AmountHi: 0, // xvm amounts are uint64; the high limb is always zero.
OwnerRoot: OwnerRoot(o),
Locktime: o.locktime,
Threshold: o.threshold,
Status: xvmroot.UTXOOccupied,
}, nil
}
// errUnsupportedOwnerModel is returned when a UTXO output carries an owner model
// the execution_root projection does not yet define a canonical owner_root for.
// It surfaces as a block-verification/build error rather than silently hashing
// an unknown output under an invented rule.
var errUnsupportedOwnerModel = errors.New("xvm execution_root: output owner model has no canonical owner_root")
// postBlockAssetLeaves projects the post-block asset family. It is intentionally
// EMPTY: xvm's executor state is UTXO-only and maintains no enumerable asset
// arena.
//
// The GPU substrate (xvm_gpu_layout.hpp) carries a dedicated Asset arena with
// total_supply / mint_authority_root / freeze_flag / denomination because it is
// a GPU-NATIVE state model. The xvm executor's authoritative state has no such
// structure: an asset exists only as the AssetID stamped on the UTXOs a
// CreateAssetTx produces (txs/executor/executor.go CreateAssetTx), and the
// asset's denomination/name live only in that creating transaction's history —
// there is no GetAsset, no asset index, and no per-asset total-supply or
// mint-authority record the state layer can enumerate.
//
// Projecting a faithful asset arena would therefore require INVENTING state the
// executor does not track (e.g. summing UTXO amounts per asset for
// total_supply, and deriving a mint_authority_root from the asset's mint
// outputs) — unverified semantics with no executor-side authority to validate
// against. That is deliberately NOT auto-invented here: it is a consensus-state
// decision flagged for human ratification (a future asset-arena state model
// would have to be added to the executor first, then projected). Until then the
// asset family is the empty set, so asset_root = keccak256("") and the
// execution_root commits to a faithful UTXO family, the tx family, the parent,
// and the height. The UTXOs themselves still carry their AssetID, so the asset
// binding is not lost — it is committed through the UTXO family.
func postBlockAssetLeaves(state.Chain) []xvmroot.AssetLeaf { return nil }
// txLeaves projects a block's transactions onto the canonical tx-family leaf
// layout, in block order (the consensus-fixed, deterministic ordering). Every
// tx is folded; a tx included in an accepted block has status = accepted, with
// the kernel-snapshot reject_reason / proof_digest / kind fields zero (an xvm tx
// in a built or verified block carries no rejection and no proof digest at this
// layer). This is a pure function of the block — builder and verifier produce
// the same leaves.
func txLeaves(blkTxs []*txs.Tx) []xvmroot.TxLeaf {
const statusAccepted uint32 = 1
leaves := make([]xvmroot.TxLeaf, len(blkTxs))
for i, tx := range blkTxs {
leaves[i] = xvmroot.TxLeaf{
TxID: ids.ID(tx.ID()),
Status: statusAccepted,
}
}
return leaves
}