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

158 lines
6.0 KiB
Go

// 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/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]. It returns
// the *StandardBlock the builder handed to NewBlock so the test can inspect the
// stamped Root. The builder always stamps the xvm execution_root — there is no
// activation gate — so the block's Root is the canonical execution_root over the
// post-block state.
func buildOneTxBlock(
t *testing.T,
ctrl *gomock.Controller,
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()
// The execution_root projection enumerates the post-block occupied UTXO set
// through the parent chain. This unit's tx produces no UTXOs, so the parent
// (and thus the post-block) UTXO set is empty — the UTXO family folds to the
// empty root, leaving the tx family + parent root + height to make the
// stamped root non-empty.
preferredState.EXPECT().UTXOs(gomock.Any(), gomock.Any()).Return(nil, nil).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)
// GetState(preferredID) is consulted by NewDiff and again by the
// execution_root projection (the diff fetches its parent state to enumerate
// the post-block UTXO set), so allow repeated calls.
manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes()
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: &config.Config{},
Log: log.NewNoOpLogger(),
},
manager,
&mockable.Clock{},
memPool,
)
_, err = builder.BuildBlock(context.Background())
require.NoError(err)
require.NotNil(built)
return built
}
// TestBuildBlockStampsExecutionRoot is the builder-side always-active proof: the
// builder always 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 — there is no empty-root path.
func TestBuildBlockStampsExecutionRoot(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
parentRoot := ids.GenerateTestID()
const parentHeight = uint64(41)
built := buildOneTxBlock(t, ctrl, parentRoot, parentHeight)
require.NotEqual(ids.Empty, built.MerkleRoot(), "builder must stamp a non-empty execution_root")
// Recompute the expected root from the canonical projection: parent root +
// the block's tx family at the built height (parentHeight + 1), over a
// post-block state whose occupied UTXO set is empty (this unit's tx produces
// none — same as the parent state the builder projected). The asset family is
// always empty (xvm's UTXO-only executor state has no asset arena).
emptyState := statemock.NewChain(ctrl)
emptyState.EXPECT().UTXOs(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
want, err := blkexecutor.BlockExecutionRoot(parentRoot, built.Txs(), emptyState, parentHeight+1)
require.NoError(err)
require.Equal(want, built.MerkleRoot())
}