xvm: parallel intra-block execution_root + activation-gated merkleRoot wiring (DEFAULT OFF)

Core goal — build the xvm block state root from independently-built
subtrees in parallel, and wire it into the block path behind an
off-by-default activation gate (zero live-consensus change).

Parallel build (vms/xvm/block/executor/executionroot.go): the three
family folds (utxo/asset/tx) run concurrently; each folds via a
GOMAXPROCS worker-pool RFC-6962 level reduction composed from
crypto/merkle's exported LeafHash/NodeHash (no duplicated keccak/tag —
DRY). Byte-IDENTICAL to the serial xvmroot.ExecutionRoot: canonical leaf
order + count-determined combine shape are unchanged; only the per-index
loops parallelize (disjoint-slot writes). Proven by 200-random +
sizes-0..4097 byte-identity tests, race-clean (go test -race).
Benchmark (M1 Max, 65536 UTXOs): execution_root 87.4ms -> 19.1ms (4.58x).

Gated wiring: upgrade.Config.MerkleRootActivationHeight defaults to
MerkleRootNeverActivate (math.MaxUint64) on Mainnet/Testnet/Default AND
xvm.DefaultConfig (safety-critical — VM.initialize discards init.Upgrade,
so without the default the zero value would activate from genesis).
Below activation: builder leaves Root=ids.Empty + executor keeps the
verbatim reject-if-non-empty rule (byte-for-byte current behavior, tested).
At/above: builder stamps BlockExecutionRoot; executor recomputes + verifies
(rejects mismatch). nil-safe (nil config -> OFF).

HONEST SEAM — NOT ACTIVATION-READY: postBlockUTXOLeaves/postBlockAssetLeaves
return nil. A real projection needs (1) a committed mapping from the
executor codec lux.UTXO/asset types to the GPU-snapshot leaf fields
(OwnerRoot/AmountHi/Threshold/TotalSupply/...), and (2) a full-occupied-set
state enumeration API (only GetUTXO/per-address UTXOIDs exist today). Both
are consensus-semantics decisions deliberately NOT auto-invented (would be
unverified non-parity slop). So today the root commits to parent + EMPTY
utxo/asset roots + the (correct) tx family + height. Consensus-inert
because the gate is OFF; builder and verifier call the identical projection
so stamped==verified always. A human must ratify the projection + wire a
state iterator + GPU snapshot producer before any activation height is set.

Additive constructor NewStandardBlockWithRoot (NewStandardBlock = its
empty-root case). xvmroot gained 3 byte-transparent exported leaf-digest
accessors (KAT 4f144ef7 unchanged) so the parallel fold reuses the one
canonical preimage. CGO_ENABLED=0 clean; go test ./vms/xvm/... ./upgrade/
PASS. Local commit; not pushed (consensus repo — awaiting human review of
the activation seam).
This commit is contained in:
zeekay
2026-06-15 18:07:50 -07:00
parent ea34ffae2f
commit 845d990d5c
13 changed files with 913 additions and 42 deletions
+37 -8
View File
@@ -8,12 +8,23 @@
package upgrade
import (
"math"
"time"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
)
// MerkleRootNeverActivate is the sentinel activation height meaning "never": no
// real block can reach math.MaxUint64, so a Config carrying this value keeps the
// xvm execution_root path OFF for the entire life of the chain. It is the
// default on Mainnet, Testnet, and Default until a concrete network upgrade
// picks a finite activation height. Below activation the xvm block path keeps
// the historical rule byte-for-byte (builder leaves the root empty; the executor
// rejects any non-empty root), so installing this sentinel is a no-op for live
// consensus.
const MerkleRootNeverActivate uint64 = math.MaxUint64
// InitiallyActiveTime is the canonical Lux genesis-activation timestamp. It is
// referenced by code paths that historically demanded "post-fork timestamp" as
// an input and now want a single deterministic value.
@@ -25,9 +36,24 @@ var InitiallyActiveTime = time.Date(2020, time.December, 5, 5, 0, 0, 0, time.UTC
// X-Chain genesis state at boot. It is a value, not a gate.
// - EpochDuration is the LP-181 epoch duration; per-network value
// (5m on mainnet, 30s on test/dev) tunes consensus pacing.
// - MerkleRootActivationHeight is the xvm block height at and above which the
// block's MerkleRoot must carry the xvm execution_root (computed over the
// post-block state) instead of staying empty. It DEFAULTS to
// MerkleRootNeverActivate (never) on every published network, so the
// historical empty-root rule is preserved until a real upgrade sets a finite
// height. It is a value (an activation point), not a feature flag.
type Config struct {
XChainStopVertexID ids.ID `json:"xChainStopVertexID"`
EpochDuration time.Duration `json:"epochDuration"`
XChainStopVertexID ids.ID `json:"xChainStopVertexID"`
EpochDuration time.Duration `json:"epochDuration"`
MerkleRootActivationHeight uint64 `json:"merkleRootActivationHeight"`
}
// IsMerkleRootActivated reports whether the xvm execution_root must be stamped
// into (and verified on) a block at [height]. Below the activation height — and
// always, while the height is the never sentinel — it returns false and the
// historical empty-root rule applies.
func (c *Config) IsMerkleRootActivated(height uint64) bool {
return height >= c.MerkleRootActivationHeight
}
// Validate is retained for callers that still invoke it; under activate-all-
@@ -37,16 +63,19 @@ func (*Config) Validate() error { return nil }
var (
Mainnet = Config{
XChainStopVertexID: ids.FromStringOrPanic("jrGWDh5Po9FMj54depyunNixpia5PN4aAYxfmNzU8n752Rjga"),
EpochDuration: 5 * time.Minute,
XChainStopVertexID: ids.FromStringOrPanic("jrGWDh5Po9FMj54depyunNixpia5PN4aAYxfmNzU8n752Rjga"),
EpochDuration: 5 * time.Minute,
MerkleRootActivationHeight: MerkleRootNeverActivate, // unset until a real upgrade picks a height
}
Testnet = Config{
XChainStopVertexID: ids.FromStringOrPanic("2D1cmbiG36BqQMRyHt4kFhWarmatA1ighSpND3FeFgz3vFVtCZ"),
EpochDuration: 30 * time.Second,
XChainStopVertexID: ids.FromStringOrPanic("2D1cmbiG36BqQMRyHt4kFhWarmatA1ighSpND3FeFgz3vFVtCZ"),
EpochDuration: 30 * time.Second,
MerkleRootActivationHeight: MerkleRootNeverActivate, // unset until a real upgrade picks a height
}
Default = Config{
XChainStopVertexID: ids.Empty,
EpochDuration: 30 * time.Second,
XChainStopVertexID: ids.Empty,
EpochDuration: 30 * time.Second,
MerkleRootActivationHeight: MerkleRootNeverActivate, // unset until a real upgrade picks a height
}
)
+30 -3
View File
@@ -7,16 +7,16 @@ import (
"context"
"errors"
chain "github.com/luxfi/vm/chain"
"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/node/vms/txs/mempool"
"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"
@@ -165,10 +165,37 @@ func (b *builder) BuildBlock(context.Context) (chain.Block, error) {
return nil, ErrNoTransactions
}
statelessBlk, err := block.NewStandardBlock(
// Below the xvm execution_root activation height (the default on every
// published network, where MerkleRootActivationHeight is the never
// sentinel), the block carries an empty root — byte-for-byte the historical
// shape. At and above activation, stamp the execution_root computed over the
// post-block state so the verifier can recompute and check it.
if !b.backend.Config.IsMerkleRootActivated(nextHeight) {
statelessBlk, err := block.NewStandardBlock(
preferredID,
nextHeight,
nextTimestamp,
blockTxs,
b.backend.Codec,
)
if err != nil {
return nil, err
}
return b.manager.NewBlock(statelessBlk), nil
}
parentRoot := preferred.MerkleRoot()
root := blockexecutor.BlockExecutionRoot(
parentRoot,
blockTxs,
stateDiff,
nextHeight,
)
statelessBlk, err := block.NewStandardBlockWithRoot(
preferredID,
nextHeight,
nextTimestamp,
root,
blockTxs,
b.backend.Codec,
)
@@ -0,0 +1,169 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package builder
import (
"context"
"testing"
"time"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/luxfi/mock/gomock"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms/pcodecs/pcodecsmock"
"github.com/luxfi/node/vms/xvm/block"
blkexecutor "github.com/luxfi/node/vms/xvm/block/executor"
"github.com/luxfi/node/vms/xvm/block/executor/executormock"
"github.com/luxfi/node/vms/xvm/config"
"github.com/luxfi/node/vms/xvm/state/statemock"
"github.com/luxfi/node/vms/xvm/txs"
txexecutor "github.com/luxfi/node/vms/xvm/txs/executor"
"github.com/luxfi/node/vms/xvm/txs/mempool"
"github.com/luxfi/node/vms/xvm/txs/txsmock"
"github.com/luxfi/timer/mockable"
chain "github.com/luxfi/vm/chain"
)
// buildOneTxBlock drives BuildBlock with a single always-valid mocked tx over a
// mocked manager/state, parent at [parentHeight] with [parentRoot], and the
// given xvm config (which carries the execution_root activation height). It
// returns the *StandardBlock the builder handed to NewBlock so the test can
// inspect the stamped Root.
func buildOneTxBlock(
t *testing.T,
ctrl *gomock.Controller,
cfg *config.Config,
parentRoot ids.ID,
parentHeight uint64,
) *block.StandardBlock {
t.Helper()
require := require.New(t)
preferredID := ids.GenerateTestID()
preferredTimestamp := time.Now()
preferredBlock := block.NewMockBlock(ctrl)
preferredBlock.EXPECT().Height().Return(parentHeight).AnyTimes()
preferredBlock.EXPECT().Timestamp().Return(preferredTimestamp).AnyTimes()
preferredBlock.EXPECT().MerkleRoot().Return(parentRoot).AnyTimes()
preferredState := statemock.NewChain(ctrl)
preferredState.EXPECT().GetLastAccepted().Return(preferredID).AnyTimes()
preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp).AnyTimes()
// One tx that passes semantic verification and execution and adds no inputs.
unsignedTx := txsmock.NewUnsignedTx(ctrl)
unsignedTx.EXPECT().Visit(gomock.Any()).Return(nil) // semantic verification
unsignedTx.EXPECT().Visit(gomock.Any()).DoAndReturn( // execution
func(visitor txs.Visitor) error {
require.IsType(&txexecutor.Executor{}, visitor)
ex := visitor.(*txexecutor.Executor)
if ex.Inputs == nil {
ex.Inputs = set.NewSet[ids.ID](0)
}
return nil
},
)
unsignedTx.EXPECT().SetBytes(gomock.Any()).AnyTimes()
unsignedTx.EXPECT().InputIDs().Return(nil).AnyTimes()
tx := &txs.Tx{Unsigned: unsignedTx}
// Give the tx a real, stable ID (ID = hash of signed bytes). The
// execution_root's tx family binds tx.ID(), so a deterministic ID lets the
// test recompute the expected root.
tx.SetBytes(nil, []byte{0x0A, 0x0B, 0x0C, 0x0D})
manager := executormock.NewManager(ctrl)
manager.EXPECT().Preferred().Return(preferredID)
manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil)
manager.EXPECT().GetState(preferredID).Return(preferredState, true)
manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil).AnyTimes()
var built *block.StandardBlock
manager.EXPECT().NewBlock(gomock.Any()).DoAndReturn(
func(b *block.StandardBlock) chain.Block {
built = b
return nil
},
)
memPool, err := mempool.New("", metric.NewRegistry())
require.NoError(err)
require.NoError(memPool.Add(tx))
// Mock codec: the builder serializes the block; the test reads block.Root
// (set before marshal), so fixed marshal output is fine.
codec := pcodecsmock.NewManager(ctrl)
codec.EXPECT().Marshal(gomock.Any(), gomock.Any()).Return([]byte{1, 2, 3}, nil).AnyTimes()
codec.EXPECT().Size(gomock.Any(), gomock.Any()).Return(2, nil).AnyTimes()
builder := New(
&txexecutor.Backend{
Ctx: context.Background(),
Runtime: testRuntime(),
Codec: codec,
Config: cfg,
Log: log.NewNoOpLogger(),
},
manager,
&mockable.Clock{},
memPool,
)
_, err = builder.BuildBlock(context.Background())
require.NoError(err)
require.NotNil(built)
return built
}
// TestBuildBlockMerkleRootGateOff is deliverable case (a), builder side: with
// the gate OFF (the default, never-sentinel height) the builder leaves the
// block's merkle root empty — byte-for-byte the historical behavior.
func TestBuildBlockMerkleRootGateOff(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
cfg := &config.Config{MerkleRootActivationHeight: upgrade.MerkleRootNeverActivate}
built := buildOneTxBlock(t, ctrl, cfg, ids.Empty, 1337)
require.Equal(ids.Empty, built.MerkleRoot(), "gate off must leave the root empty")
}
// TestBuildBlockMerkleRootGateOffNilConfig confirms the fail-safe: a backend
// with no config at all is treated as OFF, so the root stays empty.
func TestBuildBlockMerkleRootGateOffNilConfig(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
built := buildOneTxBlock(t, ctrl, nil, ids.Empty, 1337)
require.Equal(ids.Empty, built.MerkleRoot(), "nil config must fail safe to off (empty root)")
}
// TestBuildBlockMerkleRootGateOn is deliverable case (b), builder side: with the
// gate ON (activation height 0) the builder stamps the xvm execution_root
// computed over the post-block state. The stamped root must be non-empty and
// must equal the canonical BlockExecutionRoot over the same inputs.
func TestBuildBlockMerkleRootGateOn(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
parentRoot := ids.GenerateTestID()
const parentHeight = uint64(41)
cfg := &config.Config{MerkleRootActivationHeight: 0} // activate from genesis
built := buildOneTxBlock(t, ctrl, cfg, parentRoot, parentHeight)
require.NotEqual(ids.Empty, built.MerkleRoot(), "gate on must stamp a non-empty root")
// Recompute the expected root from the canonical projection: parent root +
// the block's tx family (UTXO/asset families empty per the snapshot seam) at
// the built height (parentHeight + 1).
want := blkexecutor.BlockExecutionRoot(parentRoot, built.Txs(), nil, parentHeight+1)
require.Equal(want, built.MerkleRoot())
}
+29 -3
View File
@@ -12,13 +12,13 @@ import (
"github.com/luxfi/log"
"github.com/luxfi/consensus/core/choices"
chain "github.com/luxfi/vm/chain"
"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"
)
@@ -87,9 +87,17 @@ func (b *Block) Verify(ctx context.Context) error {
return nil
}
// Currently we don't populate the blocks merkle root.
// Gate the merkle-root rule on the xvm execution_root activation height.
// Below activation (the default on every published network, where
// MerkleRootActivationHeight is the never sentinel) the historical rule
// holds verbatim: the block must carry an empty root, and a non-empty root
// is rejected here. At and above activation the block must carry the
// execution_root over the post-block state; that is recomputed and checked
// below, once the parent and the block's txs are available. A non-empty root
// is the only case the activation height affects at this point, so the gate
// is consulted only then.
merkleRoot := b.Block.MerkleRoot()
if merkleRoot != ids.Empty {
if merkleRoot != ids.Empty && !b.manager.backend.Config.IsMerkleRootActivated(b.Height()) {
return fmt.Errorf("%w: %s", ErrUnexpectedMerkleRoot, merkleRoot)
}
@@ -250,6 +258,24 @@ func (b *Block) Verify(ctx context.Context) error {
)
}
// At and above the xvm execution_root activation height, 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. Below
// activation this is skipped and the empty-root rule checked above stands.
// [height] was already resolved (and validated against the parent) above.
if b.manager.backend.Config.IsMerkleRootActivated(height) {
expectedRoot := BlockExecutionRoot(parent.MerkleRoot(), txs, stateDiff, height)
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)
+12 -3
View File
@@ -13,13 +13,11 @@ import (
"github.com/luxfi/metric"
"github.com/stretchr/testify/require"
"github.com/luxfi/runtime"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/mock/gomock"
"github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/vm/chains/atomic/atomicmock"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms/xvm/block"
"github.com/luxfi/node/vms/xvm/config"
"github.com/luxfi/node/vms/xvm/metrics/metricsmock"
@@ -28,8 +26,11 @@ import (
txexecutor "github.com/luxfi/node/vms/xvm/txs/executor"
"github.com/luxfi/node/vms/xvm/txs/mempool"
"github.com/luxfi/node/vms/xvm/txs/txsmock"
"github.com/luxfi/runtime"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/utils"
"github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/vm/chains/atomic/atomicmock"
)
func TestBlockVerify(t *testing.T) {
@@ -65,6 +66,10 @@ func TestBlockVerify(t *testing.T) {
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().MerkleRoot().Return(ids.GenerateTestID()).AnyTimes()
// A non-empty root makes the gate consult the block height to
// decide if the execution_root path is active (it is not, by
// default), so Height is now read on this path.
mockBlock.EXPECT().Height().Return(uint64(1)).AnyTimes()
return &Block{
Block: mockBlock,
manager: &manager{
@@ -971,6 +976,10 @@ func defaultTestBackend(bootstrapped bool, sharedMemory atomic.SharedMemory) *tx
Config: &config.Config{
TxFee: 0,
CreateAssetTxFee: 0,
// Model production: the xvm execution_root gate is OFF (never
// sentinel) unless a test overrides it. Without this, the Go zero
// value (0) would mean "activate from genesis".
MerkleRootActivationHeight: upgrade.MerkleRootNeverActivate,
},
Log: log.NoLog{},
}
+251
View File
@@ -0,0 +1,251 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"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"
)
// 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/asset families come
// from postBlockUTXOLeaves / postBlockAssetLeaves; see those for the
// snapshot-producer seam.
func BlockExecutionRoot(
parentExecutionRoot ids.ID,
blkTxs []*txs.Tx,
postState state.Chain,
height uint64,
) ids.ID {
leafTxs := txLeaves(blkTxs)
utxos := postBlockUTXOLeaves(postState)
assets := postBlockAssetLeaves(postState)
executionRoot, _, _, _ := xvmExecutionRoot([xvmroot.Size]byte(parentExecutionRoot), utxos, assets, leafTxs, height)
return ids.ID(executionRoot)
}
// postBlockUTXOLeaves and postBlockAssetLeaves project the post-block state into
// the xvm execution_root's UTXO and asset family leaves — the occupied set in
// canonical ascending slot order, as the kernel snapshot lays it out.
//
// SEAM (gated off; not yet wired): the canonical mapping from the xvm executor's
// codec/output-interface UTXO and asset types to the accelerator's flat
// state-snapshot leaf fields (UTXOLeaf.OwnerRoot / AmountHi / Threshold,
// AssetLeaf.TotalSupply / MintAuthorityRoot / FreezeFlag / Denomination) is owned
// by the GPU state-snapshot producer — the same component that feeds xvmroot's
// KAT — and enumerating the full occupied set needs a state iterator the xvm
// state layer does not yet expose. Until that producer lands, these families are
// empty, so the execution_root commits to the parent root, the empty UTXO/asset
// roots, the tx family (fully derived from the block), and the height. This is
// consensus-inert today: the gate defaults to the never sentinel on every
// published network (see upgrade.MerkleRootNeverActivate), so the path is never
// taken until a real upgrade both sets a height AND wires the snapshot producer.
// The builder and verifier call this identical projection, so the stamped and
// verified roots always agree.
func postBlockUTXOLeaves(state.Chain) []xvmroot.UTXOLeaf { return nil }
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
}
@@ -0,0 +1,189 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"math/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/node/vms/xvm/state/xvmroot"
)
// randUTXOLeaves builds n UTXO leaves with random fields and a random occupancy
// mix (roughly half unoccupied) so the parallel compaction is exercised against
// the serial one over a non-trivial occupied subset.
func randUTXOLeaves(rng *rand.Rand, n int) []xvmroot.UTXOLeaf {
us := make([]xvmroot.UTXOLeaf, n)
for i := range us {
rng.Read(us[i].UTXOID[:])
rng.Read(us[i].AssetID[:])
rng.Read(us[i].OwnerRoot[:])
us[i].AmountLo = rng.Uint64()
us[i].AmountHi = rng.Uint64()
us[i].Locktime = rng.Uint64()
us[i].Threshold = rng.Uint32()
if rng.Intn(2) == 0 {
us[i].Status = xvmroot.UTXOOccupied
}
}
return us
}
func randAssetLeaves(rng *rand.Rand, n int) []xvmroot.AssetLeaf {
as := make([]xvmroot.AssetLeaf, n)
for i := range as {
rng.Read(as[i].AssetID[:])
rng.Read(as[i].MintAuthorityRoot[:])
as[i].TotalSupplyLo = rng.Uint64()
as[i].TotalSupplyHi = rng.Uint64()
as[i].FreezeFlag = rng.Uint32()
as[i].Denomination = rng.Uint32()
if rng.Intn(2) == 0 {
as[i].Occupied = 1
}
}
return as
}
func randTxLeaves(rng *rand.Rand, n int) []xvmroot.TxLeaf {
ts := make([]xvmroot.TxLeaf, n)
for i := range ts {
rng.Read(ts[i].TxID[:])
rng.Read(ts[i].ProofDigest[:])
ts[i].Kind = rng.Uint32()
ts[i].Status = rng.Uint32()
ts[i].RejectReason = rng.Uint32()
}
return ts
}
// TestParallelFamilyRootsMatchSerial pins each parallel family fold byte-for-byte
// against the serial xvmroot family root, across a range of sizes that includes
// 0, 1, odd, even, and sizes straddling the inline/worker threshold — the leaf
// counts where lone-right Merkle promotion and worker sharding edges live.
func TestParallelFamilyRootsMatchSerial(t *testing.T) {
require := require.New(t)
rng := rand.New(rand.NewSource(0xC0FFEE))
sizes := []int{0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 33, 63, 64, 65,
127, 255, 256, 257, 511, 1000, 1023, 1024, 4097}
for _, n := range sizes {
us := randUTXOLeaves(rng, n)
require.Equalf(xvmroot.UTXORoot(us), parallelUTXORoot(us), "utxo root n=%d", n)
as := randAssetLeaves(rng, n)
require.Equalf(xvmroot.AssetRoot(as), parallelAssetRoot(as), "asset root n=%d", n)
ts := randTxLeaves(rng, n)
require.Equalf(xvmroot.TxRoot(ts), parallelTxRoot(ts), "tx root n=%d", n)
}
}
// TestParallelExecutionRootMatchesSerial is the property test the deliverable
// requires: the full parallel execution_root equals the serial
// xvmroot.ExecutionRoot over random state of random sizes (including odd
// per-family counts), so the parallel build is a drop-in for the serial oracle.
func TestParallelExecutionRootMatchesSerial(t *testing.T) {
require := require.New(t)
rng := rand.New(rand.NewSource(0x5EED))
for iter := 0; iter < 200; iter++ {
var parent [xvmroot.Size]byte
rng.Read(parent[:])
height := rng.Uint64()
// Independent, frequently-odd per-family sizes.
nU := rng.Intn(600)
nA := rng.Intn(600)
nT := rng.Intn(600)
us := randUTXOLeaves(rng, nU)
as := randAssetLeaves(rng, nA)
ts := randTxLeaves(rng, nT)
wantExec, wantU, wantA, wantT := xvmroot.ExecutionRoot(parent, us, as, ts, height)
gotExec, gotU, gotA, gotT := xvmExecutionRoot(parent, us, as, ts, height)
require.Equalf(wantU, gotU, "utxo iter=%d nU=%d", iter, nU)
require.Equalf(wantA, gotA, "asset iter=%d nA=%d", iter, nA)
require.Equalf(wantT, gotT, "tx iter=%d nT=%d", iter, nT)
require.Equalf(wantExec, gotExec, "exec iter=%d", iter)
}
}
// TestParallelExecutionRootEmpty confirms the all-empty state reduces to the
// same composed root the serial path yields (empty family roots are
// keccak256(""), not zero).
func TestParallelExecutionRootEmpty(t *testing.T) {
require := require.New(t)
var parent [xvmroot.Size]byte
wantExec, _, _, _ := xvmroot.ExecutionRoot(parent, nil, nil, nil, 0)
gotExec, _, _, _ := xvmExecutionRoot(parent, nil, nil, nil, 0)
require.Equal(wantExec, gotExec)
}
const benchUTXOCount = 65536
func benchUTXOSet() []xvmroot.UTXOLeaf {
rng := rand.New(rand.NewSource(1))
us := make([]xvmroot.UTXOLeaf, benchUTXOCount)
for i := range us {
rng.Read(us[i].UTXOID[:])
rng.Read(us[i].AssetID[:])
rng.Read(us[i].OwnerRoot[:])
us[i].AmountLo = rng.Uint64()
us[i].Threshold = 1
us[i].Status = xvmroot.UTXOOccupied // all occupied: worst-case fold width
}
return us
}
// BenchmarkUTXORootSerial folds a 65536-leaf UTXO set with the serial
// xvmroot.UTXORoot.
func BenchmarkUTXORootSerial(b *testing.B) {
us := benchUTXOSet()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = xvmroot.UTXORoot(us)
}
}
// BenchmarkUTXORootParallel folds the same 65536-leaf set with the parallel
// worker-pool fold. Compare against BenchmarkUTXORootSerial to see the
// leaf-hashing speedup.
func BenchmarkUTXORootParallel(b *testing.B) {
us := benchUTXOSet()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = parallelUTXORoot(us)
}
}
// BenchmarkExecutionRootSerial / Parallel compare the full execution_root build
// over a 65536-UTXO post-block state (with a modest asset + tx set), serial
// xvmroot.ExecutionRoot vs the concurrent xvmExecutionRoot.
func BenchmarkExecutionRootSerial(b *testing.B) {
var parent [xvmroot.Size]byte
us := benchUTXOSet()
rng := rand.New(rand.NewSource(2))
as := randAssetLeaves(rng, 4096)
ts := randTxLeaves(rng, 4096)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, _ = xvmroot.ExecutionRoot(parent, us, as, ts, 100)
}
}
func BenchmarkExecutionRootParallel(b *testing.B) {
var parent [xvmroot.Size]byte
us := benchUTXOSet()
rng := rand.New(rand.NewSource(2))
as := randAssetLeaves(rng, 4096)
ts := randTxLeaves(rng, 4096)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _, _ = xvmExecutionRoot(parent, us, as, ts, 100)
}
}
+18
View File
@@ -74,11 +74,29 @@ func NewStandardBlock(
timestamp time.Time,
txs []*txs.Tx,
cm pcodecs.Manager,
) (*StandardBlock, error) {
return NewStandardBlockWithRoot(parentID, height, timestamp, ids.Empty, txs, cm)
}
// NewStandardBlockWithRoot builds a StandardBlock carrying an explicit merkle
// root and serializes it. NewStandardBlock is the root == ids.Empty special case
// — the historical, pre-activation shape. Above the xvm execution_root
// activation height the builder passes the computed execution_root here so it is
// part of the serialized, hashed block bytes; below activation the empty-root
// path (NewStandardBlock) is used and the bytes are byte-for-byte unchanged.
func NewStandardBlockWithRoot(
parentID ids.ID,
height uint64,
timestamp time.Time,
root ids.ID,
txs []*txs.Tx,
cm pcodecs.Manager,
) (*StandardBlock, error) {
blk := &StandardBlock{
PrntID: parentID,
Hght: height,
Time: uint64(timestamp.Unix()),
Root: root,
Transactions: txs,
}
+7
View File
@@ -7,6 +7,7 @@ import (
"github.com/go-json-experiment/json"
jsonv1 "github.com/go-json-experiment/json/v1"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms/xvm/config"
"github.com/luxfi/node/vms/xvm/network"
)
@@ -17,6 +18,12 @@ var DefaultConfig = Config{
Config: config.Config{
TxFee: 1000, // 1000 nanoLux base transaction fee
CreateAssetTxFee: 10000, // 10000 nanoLux for asset creation
// xvm execution_root gate OFF by default: a VM with no explicit override
// keeps the historical empty-root rule. A real network upgrade sets a
// finite height via the JSON config to activate it. Without this default
// the Go zero value (0) would activate the path from genesis — see the
// safety rail in upgrade.MerkleRootNeverActivate.
MerkleRootActivationHeight: upgrade.MerkleRootNeverActivate,
},
}
+26
View File
@@ -13,4 +13,30 @@ type Config struct {
// IndexTransactions enables transaction indexing by address
IndexTransactions bool `json:"indexTransactions"`
// MerkleRootActivationHeight is the xvm block height at and above which the
// block's MerkleRoot must carry the xvm execution_root over the post-block
// state. It is sourced from upgrade.Config at VM init and defaults to
// upgrade.MerkleRootNeverActivate (math.MaxUint64) on every published
// network, so the historical empty-root rule holds until a real upgrade
// picks a finite height. A zero value (the Go struct zero) activates from
// genesis and is used only by tests that exercise the activated path.
MerkleRootActivationHeight uint64 `json:"merkleRootActivationHeight"`
}
// IsMerkleRootActivated reports whether the xvm execution_root must be stamped
// into (and verified on) a block at [height]. Mirrors
// upgrade.Config.IsMerkleRootActivated so the block path can gate on the value
// carried in the backend without reaching back into the upgrade package.
//
// It is nil-safe: a nil config (a backend constructed without one) is OFF, so
// the gate fails safe to the historical empty-root rule. Production always
// carries a config whose height is sourced from upgrade.Config — the never
// sentinel by default — so the activated path engages only when an operator
// deliberately sets a finite height.
func (c *Config) IsMerkleRootActivated(height uint64) bool {
if c == nil {
return false
}
return height >= c.MerkleRootActivationHeight
}
+15
View File
@@ -147,6 +147,21 @@ func txLeafDigest(t TxLeaf, i uint32) [Size]byte {
return hash.ComputeKeccak256Array(b)
}
// UTXOLeafDigest returns the canonical per-element keccak256 leaf digest d_i for
// the UTXO at slot index i — the same bytes UTXORoot folds. It is exported so a
// parallel builder can compute the family's leaf digests across worker
// goroutines from the one canonical preimage definition (no second copy of the
// layout), then merkle.Root-combine to a byte-identical root.
func UTXOLeafDigest(u UTXOLeaf, i uint32) [Size]byte { return utxoLeafDigest(u, i) }
// AssetLeafDigest returns the canonical per-element keccak256 leaf digest d_i for
// the asset at slot index i — the same bytes AssetRoot folds.
func AssetLeafDigest(a AssetLeaf, i uint32) [Size]byte { return assetLeafDigest(a, i) }
// TxLeafDigest returns the canonical per-element keccak256 leaf digest d_i for
// the tx at slot index i — the same bytes TxRoot folds.
func TxLeafDigest(t TxLeaf, i uint32) [Size]byte { return txLeafDigest(t, i) }
// UTXORoot is the tagged binary Merkle root over the occupied UTXOs' leaf
// digests, in ascending slot index. Slot index i is the position in utxos
// (bound into each leaf preimage); a UTXO with (Status & UTXOOccupied) == 0 is
+98
View File
@@ -0,0 +1,98 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package xvm
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/node/upgrade"
"github.com/luxfi/node/vms/xvm/block"
blkexecutor "github.com/luxfi/node/vms/xvm/block/executor"
)
func u64ptr(v uint64) *uint64 { return &v }
// TestMerkleRootGateOffDefaultBlockEmpty is deliverable case (a), end to end: a
// VM with the default config (gate OFF, never sentinel) builds and accepts a
// real block whose merkle root is empty — the historical behavior, unchanged.
func TestMerkleRootGateOffDefaultBlockEmpty(t *testing.T) {
require := require.New(t)
env := setup(t, &envConfig{fork: upgrade.Default})
env.vm.Lock.Unlock()
tx := newTx(t, env.genesisBytes, env.consensusRuntime.ChainID, env.vm.parser, "LUX")
require.NoError(env.vm.network.IssueTxFromRPC(tx))
blkIntf, err := env.vm.BuildBlock(context.Background())
require.NoError(err)
require.Equal(ids.Empty, blkIntf.(*blkexecutor.Block).MerkleRoot(),
"gate off: built block must carry an empty root")
// And it verifies and accepts under the historical empty-root rule.
require.NoError(blkIntf.Verify(context.Background()))
require.NoError(blkIntf.Accept(context.Background()))
}
// TestMerkleRootGateOnAcceptsAndRejects is deliverable case (b), end to end: a
// VM with the gate ON (activation height 0) builds a real block that carries a
// non-empty xvm execution_root, that block verifies and accepts, and a sibling
// block whose root is tampered is rejected with ErrUnexpectedMerkleRoot.
func TestMerkleRootGateOnAcceptsAndRejects(t *testing.T) {
require := require.New(t)
env := setup(t, &envConfig{
fork: upgrade.Default,
merkleRootActivationHeight: u64ptr(0), // activate from genesis
})
env.vm.Lock.Unlock()
tx := newTx(t, env.genesisBytes, env.consensusRuntime.ChainID, env.vm.parser, "LUX")
require.NoError(env.vm.network.IssueTxFromRPC(tx))
blkIntf, err := env.vm.BuildBlock(context.Background())
require.NoError(err)
// The builder stamped the execution_root: non-empty, and equal to the
// canonical projection over the block's parent root + tx family.
built := blkIntf.(*blkexecutor.Block)
root := built.MerkleRoot()
require.NotEqual(ids.Empty, root, "gate on: built block must carry a non-empty root")
parentID := built.Parent()
parentBlk, err := env.vm.chainManager.GetStatelessBlock(parentID)
require.NoError(err)
wantRoot := blkexecutor.BlockExecutionRoot(parentBlk.MerkleRoot(), built.Txs(), nil, built.Height())
require.Equal(wantRoot, root, "stamped root must equal the canonical execution_root")
// Build a sibling block identical in every way EXCEPT a deliberately wrong
// root, parse it through the VM, and verify it: the executor recomputes the
// expected root and rejects the mismatch.
cm := env.vm.parser.Codec()
wrongRoot := ids.GenerateTestID()
require.NotEqual(wrongRoot, root)
tampered, err := block.NewStandardBlockWithRoot(
parentID,
built.Height(),
time.Unix(int64(built.Timestamp().Unix()), 0),
wrongRoot,
built.Txs(),
cm,
)
require.NoError(err)
tamperedParsed, err := env.vm.ParseBlock(context.Background(), tampered.Bytes())
require.NoError(err)
err = tamperedParsed.Verify(context.Background())
require.ErrorIs(err, blkexecutor.ErrUnexpectedMerkleRoot)
// The correctly-stamped block still verifies and accepts.
require.NoError(blkIntf.Verify(context.Background()))
require.NoError(blkIntf.Accept(context.Background()))
}
+32 -25
View File
@@ -13,20 +13,20 @@ import (
"github.com/stretchr/testify/require"
"github.com/luxfi/address"
"github.com/luxfi/vm"
"github.com/luxfi/runtime"
"github.com/luxfi/consensus/core/choices"
validators "github.com/luxfi/validators"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/database/memdb"
"github.com/luxfi/ids"
"github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/node/upgrade"
lux "github.com/luxfi/utxo"
"github.com/luxfi/runtime"
validators "github.com/luxfi/validators"
"github.com/luxfi/vm"
"github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/node/vms/xvm/txs"
"github.com/luxfi/node/vms/xvm/txs/txstest"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
@@ -54,17 +54,21 @@ type envConfig struct {
notLinearized bool
additionalFxs []interface{}
indexTransactions bool // Enable transaction indexing
// merkleRootActivationHeight, when non-nil, overrides the xvm
// execution_root activation height in the VM's config. nil leaves the safe
// default (never sentinel) from DefaultConfig in place.
merkleRootActivationHeight *uint64
}
// testEnv is the test environment
type testEnv struct {
vm *VM
vm *VM
consensusRuntime *runtime.Runtime
genesisBytes []byte
genesisTx *txs.Tx
testLock *sync.Mutex
txBuilder *txstest.Builder
sharedMemory *atomic.Memory
genesisBytes []byte
genesisTx *txs.Tx
testLock *sync.Mutex
txBuilder *txstest.Builder
sharedMemory *atomic.Memory
}
// newGenesisBytesTest creates test genesis bytes
@@ -270,6 +274,9 @@ func setup(t testing.TB, config *envConfig) *testEnv {
if config.indexTransactions {
vmConfig.IndexTransactions = true
}
if config.merkleRootActivationHeight != nil {
vmConfig.MerkleRootActivationHeight = *config.merkleRootActivationHeight
}
configBytes, err := json.Marshal(vmConfig, jsonv1.FormatDurationAsNano(true))
require.NoError(err)
@@ -277,14 +284,14 @@ func setup(t testing.TB, config *envConfig) *testEnv {
require.NoError(vmImpl.Initialize(
context.Background(),
vm.Init{
Runtime: rt,
DB: baseDB,
Genesis: genesisBytes,
Upgrade: nil,
Config: configBytes,
Runtime: rt,
DB: baseDB,
Genesis: genesisBytes,
Upgrade: nil,
Config: configBytes,
ToEngine: toEngine,
Fx: fxs,
Sender: appSender,
Fx: fxs,
Sender: appSender,
},
))
@@ -305,13 +312,13 @@ func setup(t testing.TB, config *envConfig) *testEnv {
txBuilder.SetContextIDs(rt.NetworkID, rt.ChainID)
env := &testEnv{
vm: vmImpl,
vm: vmImpl,
consensusRuntime: rt,
genesisBytes: genesisBytes,
genesisTx: genesisTx,
testLock: testLock,
txBuilder: txBuilder,
sharedMemory: sharedMemory,
genesisBytes: genesisBytes,
genesisTx: genesisTx,
testLock: testLock,
txBuilder: txBuilder,
sharedMemory: sharedMemory,
}
// Register cleanup to prevent goroutine leaks