mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
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).
This commit is contained in:
+8
-37
@@ -8,23 +8,12 @@
|
|||||||
package upgrade
|
package upgrade
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/luxfi/constants"
|
"github.com/luxfi/constants"
|
||||||
"github.com/luxfi/ids"
|
"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
|
// InitiallyActiveTime is the canonical Lux genesis-activation timestamp. It is
|
||||||
// referenced by code paths that historically demanded "post-fork timestamp" as
|
// referenced by code paths that historically demanded "post-fork timestamp" as
|
||||||
// an input and now want a single deterministic value.
|
// an input and now want a single deterministic value.
|
||||||
@@ -36,24 +25,9 @@ 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.
|
// X-Chain genesis state at boot. It is a value, not a gate.
|
||||||
// - EpochDuration is the LP-181 epoch duration; per-network value
|
// - EpochDuration is the LP-181 epoch duration; per-network value
|
||||||
// (5m on mainnet, 30s on test/dev) tunes consensus pacing.
|
// (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 {
|
type Config struct {
|
||||||
XChainStopVertexID ids.ID `json:"xChainStopVertexID"`
|
XChainStopVertexID ids.ID `json:"xChainStopVertexID"`
|
||||||
EpochDuration time.Duration `json:"epochDuration"`
|
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-
|
// Validate is retained for callers that still invoke it; under activate-all-
|
||||||
@@ -63,19 +37,16 @@ func (*Config) Validate() error { return nil }
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
Mainnet = Config{
|
Mainnet = Config{
|
||||||
XChainStopVertexID: ids.FromStringOrPanic("jrGWDh5Po9FMj54depyunNixpia5PN4aAYxfmNzU8n752Rjga"),
|
XChainStopVertexID: ids.FromStringOrPanic("jrGWDh5Po9FMj54depyunNixpia5PN4aAYxfmNzU8n752Rjga"),
|
||||||
EpochDuration: 5 * time.Minute,
|
EpochDuration: 5 * time.Minute,
|
||||||
MerkleRootActivationHeight: MerkleRootNeverActivate, // unset until a real upgrade picks a height
|
|
||||||
}
|
}
|
||||||
Testnet = Config{
|
Testnet = Config{
|
||||||
XChainStopVertexID: ids.FromStringOrPanic("2D1cmbiG36BqQMRyHt4kFhWarmatA1ighSpND3FeFgz3vFVtCZ"),
|
XChainStopVertexID: ids.FromStringOrPanic("2D1cmbiG36BqQMRyHt4kFhWarmatA1ighSpND3FeFgz3vFVtCZ"),
|
||||||
EpochDuration: 30 * time.Second,
|
EpochDuration: 30 * time.Second,
|
||||||
MerkleRootActivationHeight: MerkleRootNeverActivate, // unset until a real upgrade picks a height
|
|
||||||
}
|
}
|
||||||
Default = Config{
|
Default = Config{
|
||||||
XChainStopVertexID: ids.Empty,
|
XChainStopVertexID: ids.Empty,
|
||||||
EpochDuration: 30 * time.Second,
|
EpochDuration: 30 * time.Second,
|
||||||
MerkleRootActivationHeight: MerkleRootNeverActivate, // unset until a real upgrade picks a height
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -166,25 +166,12 @@ func (b *builder) BuildBlock(context.Context) (chain.Block, error) {
|
|||||||
return nil, ErrNoTransactions
|
return nil, ErrNoTransactions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Below the xvm execution_root activation height (the default on every
|
// Stamp the xvm execution_root computed over the post-block state into the
|
||||||
// published network, where MerkleRootActivationHeight is the never
|
// block so the verifier can recompute and check it. The root is the tagged
|
||||||
// sentinel), the block carries an empty root — byte-for-byte the historical
|
// Merkle-tree fold over the post-block state (utxo ‖ asset ‖ tx ‖ height);
|
||||||
// shape. At and above activation, stamp the execution_root computed over the
|
// the builder and the executor share exactly one computation
|
||||||
// post-block state so the verifier can recompute and check it.
|
// (BlockExecutionRoot) so the stamped root and the verified root can never
|
||||||
if !b.backend.Config.IsMerkleRootActivated(nextHeight) {
|
// disagree.
|
||||||
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()
|
parentRoot := preferred.MerkleRoot()
|
||||||
root, err := blockexecutor.BlockExecutionRoot(
|
root, err := blockexecutor.BlockExecutionRoot(
|
||||||
parentRoot,
|
parentRoot,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
|
|
||||||
"github.com/luxfi/ids"
|
"github.com/luxfi/ids"
|
||||||
"github.com/luxfi/math/set"
|
"github.com/luxfi/math/set"
|
||||||
"github.com/luxfi/node/upgrade"
|
|
||||||
"github.com/luxfi/node/vms/pcodecs/pcodecsmock"
|
"github.com/luxfi/node/vms/pcodecs/pcodecsmock"
|
||||||
"github.com/luxfi/node/vms/xvm/block"
|
"github.com/luxfi/node/vms/xvm/block"
|
||||||
blkexecutor "github.com/luxfi/node/vms/xvm/block/executor"
|
blkexecutor "github.com/luxfi/node/vms/xvm/block/executor"
|
||||||
@@ -31,14 +30,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// buildOneTxBlock drives BuildBlock with a single always-valid mocked tx over a
|
// buildOneTxBlock drives BuildBlock with a single always-valid mocked tx over a
|
||||||
// mocked manager/state, parent at [parentHeight] with [parentRoot], and the
|
// mocked manager/state, parent at [parentHeight] with [parentRoot]. It returns
|
||||||
// given xvm config (which carries the execution_root activation height). It
|
// the *StandardBlock the builder handed to NewBlock so the test can inspect the
|
||||||
// returns the *StandardBlock the builder handed to NewBlock so the test can
|
// stamped Root. The builder always stamps the xvm execution_root — there is no
|
||||||
// inspect the stamped Root.
|
// activation gate — so the block's Root is the canonical execution_root over the
|
||||||
|
// post-block state.
|
||||||
func buildOneTxBlock(
|
func buildOneTxBlock(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
ctrl *gomock.Controller,
|
ctrl *gomock.Controller,
|
||||||
cfg *config.Config,
|
|
||||||
parentRoot ids.ID,
|
parentRoot ids.ID,
|
||||||
parentHeight uint64,
|
parentHeight uint64,
|
||||||
) *block.StandardBlock {
|
) *block.StandardBlock {
|
||||||
@@ -116,7 +115,7 @@ func buildOneTxBlock(
|
|||||||
Ctx: context.Background(),
|
Ctx: context.Background(),
|
||||||
Runtime: testRuntime(),
|
Runtime: testRuntime(),
|
||||||
Codec: codec,
|
Codec: codec,
|
||||||
Config: cfg,
|
Config: &config.Config{},
|
||||||
Log: log.NewNoOpLogger(),
|
Log: log.NewNoOpLogger(),
|
||||||
},
|
},
|
||||||
manager,
|
manager,
|
||||||
@@ -130,45 +129,20 @@ func buildOneTxBlock(
|
|||||||
return built
|
return built
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBuildBlockMerkleRootGateOff is deliverable case (a), builder side: with
|
// TestBuildBlockStampsExecutionRoot is the builder-side always-active proof: the
|
||||||
// the gate OFF (the default, never-sentinel height) the builder leaves the
|
// builder always stamps the xvm execution_root computed over the post-block
|
||||||
// block's merkle root empty — byte-for-byte the historical behavior.
|
// state. The stamped root must be non-empty and must equal the canonical
|
||||||
func TestBuildBlockMerkleRootGateOff(t *testing.T) {
|
// BlockExecutionRoot over the same inputs — there is no empty-root path.
|
||||||
require := require.New(t)
|
func TestBuildBlockStampsExecutionRoot(t *testing.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)
|
require := require.New(t)
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
|
|
||||||
parentRoot := ids.GenerateTestID()
|
parentRoot := ids.GenerateTestID()
|
||||||
const parentHeight = uint64(41)
|
const parentHeight = uint64(41)
|
||||||
cfg := &config.Config{MerkleRootActivationHeight: 0} // activate from genesis
|
|
||||||
|
|
||||||
built := buildOneTxBlock(t, ctrl, cfg, parentRoot, parentHeight)
|
built := buildOneTxBlock(t, ctrl, parentRoot, parentHeight)
|
||||||
|
|
||||||
require.NotEqual(ids.Empty, built.MerkleRoot(), "gate on must stamp a non-empty root")
|
require.NotEqual(ids.Empty, built.MerkleRoot(), "builder must stamp a non-empty execution_root")
|
||||||
|
|
||||||
// Recompute the expected root from the canonical projection: parent root +
|
// Recompute the expected root from the canonical projection: parent root +
|
||||||
// the block's tx family at the built height (parentHeight + 1), over a
|
// the block's tx family at the built height (parentHeight + 1), over a
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/luxfi/mock/gomock"
|
"github.com/luxfi/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
chain "github.com/luxfi/vm/chain"
|
|
||||||
"github.com/luxfi/constants"
|
"github.com/luxfi/constants"
|
||||||
"github.com/luxfi/crypto/secp256k1"
|
"github.com/luxfi/crypto/secp256k1"
|
||||||
"github.com/luxfi/database/memdb"
|
"github.com/luxfi/database/memdb"
|
||||||
@@ -38,6 +37,7 @@ import (
|
|||||||
"github.com/luxfi/timer/mockable"
|
"github.com/luxfi/timer/mockable"
|
||||||
lux "github.com/luxfi/utxo"
|
lux "github.com/luxfi/utxo"
|
||||||
"github.com/luxfi/utxo/secp256k1fx"
|
"github.com/luxfi/utxo/secp256k1fx"
|
||||||
|
chain "github.com/luxfi/vm/chain"
|
||||||
)
|
)
|
||||||
|
|
||||||
const trackChecksums = false
|
const trackChecksums = false
|
||||||
@@ -260,10 +260,16 @@ func TestBuilderBuildBlock(t *testing.T) {
|
|||||||
preferredBlock := block.NewMockBlock(ctrl)
|
preferredBlock := block.NewMockBlock(ctrl)
|
||||||
preferredBlock.EXPECT().Height().Return(preferredHeight)
|
preferredBlock.EXPECT().Height().Return(preferredHeight)
|
||||||
preferredBlock.EXPECT().Timestamp().Return(preferredTimestamp)
|
preferredBlock.EXPECT().Timestamp().Return(preferredTimestamp)
|
||||||
|
// The builder always stamps the execution_root, recomputed from
|
||||||
|
// the parent's root.
|
||||||
|
preferredBlock.EXPECT().MerkleRoot().Return(ids.GenerateTestID()).AnyTimes()
|
||||||
|
|
||||||
preferredState := statemock.NewChain(ctrl)
|
preferredState := statemock.NewChain(ctrl)
|
||||||
preferredState.EXPECT().GetLastAccepted().Return(preferredID)
|
preferredState.EXPECT().GetLastAccepted().Return(preferredID)
|
||||||
preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp)
|
preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp)
|
||||||
|
// The execution_root projection enumerates the post-block UTXO set
|
||||||
|
// through the parent; this unit produces none.
|
||||||
|
preferredState.EXPECT().UTXOs(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
|
|
||||||
// tx1 and tx2 both consume [inputID].
|
// tx1 and tx2 both consume [inputID].
|
||||||
// tx1 is added to the block first, so tx2 should be dropped.
|
// tx1 is added to the block first, so tx2 should be dropped.
|
||||||
@@ -308,7 +314,10 @@ func TestBuilderBuildBlock(t *testing.T) {
|
|||||||
manager := executormock.NewManager(ctrl)
|
manager := executormock.NewManager(ctrl)
|
||||||
manager.EXPECT().Preferred().Return(preferredID)
|
manager.EXPECT().Preferred().Return(preferredID)
|
||||||
manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil)
|
manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil)
|
||||||
manager.EXPECT().GetState(preferredID).Return(preferredState, true)
|
// GetState is consulted by NewDiff and again by the execution_root
|
||||||
|
// projection (the diff pages its parent to enumerate the post-block
|
||||||
|
// UTXO set), so allow repeated calls.
|
||||||
|
manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes()
|
||||||
// VerifyUniqueInputs is called once for tx1. tx2 should be dropped due to
|
// VerifyUniqueInputs is called once for tx1. tx2 should be dropped due to
|
||||||
// inputs.Overlaps check, so VerifyUniqueInputs should not be called for tx2
|
// inputs.Overlaps check, so VerifyUniqueInputs should not be called for tx2
|
||||||
// But if it's being called twice, we need to handle it
|
// But if it's being called twice, we need to handle it
|
||||||
@@ -358,6 +367,7 @@ func TestBuilderBuildBlock(t *testing.T) {
|
|||||||
preferredBlock := block.NewMockBlock(ctrl)
|
preferredBlock := block.NewMockBlock(ctrl)
|
||||||
preferredBlock.EXPECT().Height().Return(preferredHeight)
|
preferredBlock.EXPECT().Height().Return(preferredHeight)
|
||||||
preferredBlock.EXPECT().Timestamp().Return(preferredTimestamp)
|
preferredBlock.EXPECT().Timestamp().Return(preferredTimestamp)
|
||||||
|
preferredBlock.EXPECT().MerkleRoot().Return(ids.GenerateTestID()).AnyTimes()
|
||||||
|
|
||||||
// Clock reads just before the preferred timestamp.
|
// Clock reads just before the preferred timestamp.
|
||||||
// Created block should have the preferred timestamp since it's later.
|
// Created block should have the preferred timestamp since it's later.
|
||||||
@@ -367,11 +377,12 @@ func TestBuilderBuildBlock(t *testing.T) {
|
|||||||
preferredState := statemock.NewChain(ctrl)
|
preferredState := statemock.NewChain(ctrl)
|
||||||
preferredState.EXPECT().GetLastAccepted().Return(preferredID)
|
preferredState.EXPECT().GetLastAccepted().Return(preferredID)
|
||||||
preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp)
|
preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp)
|
||||||
|
preferredState.EXPECT().UTXOs(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
|
|
||||||
manager := executormock.NewManager(ctrl)
|
manager := executormock.NewManager(ctrl)
|
||||||
manager.EXPECT().Preferred().Return(preferredID)
|
manager.EXPECT().Preferred().Return(preferredID)
|
||||||
manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil)
|
manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil)
|
||||||
manager.EXPECT().GetState(preferredID).Return(preferredState, true)
|
manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes()
|
||||||
manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil)
|
manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil)
|
||||||
// Assert that the created block has the right timestamp
|
// Assert that the created block has the right timestamp
|
||||||
manager.EXPECT().NewBlock(gomock.Any()).DoAndReturn(
|
manager.EXPECT().NewBlock(gomock.Any()).DoAndReturn(
|
||||||
@@ -433,6 +444,7 @@ func TestBuilderBuildBlock(t *testing.T) {
|
|||||||
preferredBlock := block.NewMockBlock(ctrl)
|
preferredBlock := block.NewMockBlock(ctrl)
|
||||||
preferredBlock.EXPECT().Height().Return(preferredHeight)
|
preferredBlock.EXPECT().Height().Return(preferredHeight)
|
||||||
preferredBlock.EXPECT().Timestamp().Return(preferredTimestamp)
|
preferredBlock.EXPECT().Timestamp().Return(preferredTimestamp)
|
||||||
|
preferredBlock.EXPECT().MerkleRoot().Return(ids.GenerateTestID()).AnyTimes()
|
||||||
|
|
||||||
// Clock reads after the preferred timestamp.
|
// Clock reads after the preferred timestamp.
|
||||||
// Created block should have [now] timestamp since it's later.
|
// Created block should have [now] timestamp since it's later.
|
||||||
@@ -442,11 +454,12 @@ func TestBuilderBuildBlock(t *testing.T) {
|
|||||||
preferredState := statemock.NewChain(ctrl)
|
preferredState := statemock.NewChain(ctrl)
|
||||||
preferredState.EXPECT().GetLastAccepted().Return(preferredID)
|
preferredState.EXPECT().GetLastAccepted().Return(preferredID)
|
||||||
preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp)
|
preferredState.EXPECT().GetTimestamp().Return(preferredTimestamp)
|
||||||
|
preferredState.EXPECT().UTXOs(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
|
|
||||||
manager := executormock.NewManager(ctrl)
|
manager := executormock.NewManager(ctrl)
|
||||||
manager.EXPECT().Preferred().Return(preferredID)
|
manager.EXPECT().Preferred().Return(preferredID)
|
||||||
manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil)
|
manager.EXPECT().GetStatelessBlock(preferredID).Return(preferredBlock, nil)
|
||||||
manager.EXPECT().GetState(preferredID).Return(preferredState, true)
|
manager.EXPECT().GetState(preferredID).Return(preferredState, true).AnyTimes()
|
||||||
manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil)
|
manager.EXPECT().VerifyUniqueInputs(preferredID, gomock.Any()).Return(nil)
|
||||||
// Assert that the created block has the right timestamp
|
// Assert that the created block has the right timestamp
|
||||||
manager.EXPECT().NewBlock(gomock.Any()).DoAndReturn(
|
manager.EXPECT().NewBlock(gomock.Any()).DoAndReturn(
|
||||||
|
|||||||
@@ -87,19 +87,12 @@ func (b *Block) Verify(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gate the merkle-root rule on the xvm execution_root activation height.
|
// The block's MerkleRoot carries the xvm execution_root over the post-block
|
||||||
// Below activation (the default on every published network, where
|
// state. It is verified unconditionally once the parent and the block's txs
|
||||||
// MerkleRootActivationHeight is the never sentinel) the historical rule
|
// are available: the executor recomputes the canonical execution_root and
|
||||||
// holds verbatim: the block must carry an empty root, and a non-empty root
|
// rejects any block whose stamped root disagrees (see the recompute below,
|
||||||
// is rejected here. At and above activation the block must carry the
|
// after the txs have been applied to the state diff).
|
||||||
// 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()
|
merkleRoot := b.Block.MerkleRoot()
|
||||||
if merkleRoot != ids.Empty && !b.manager.backend.Config.IsMerkleRootActivated(b.Height()) {
|
|
||||||
return fmt.Errorf("%w: %s", ErrUnexpectedMerkleRoot, merkleRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow timestamp to reasonably far forward
|
// Only allow timestamp to reasonably far forward
|
||||||
newChainTime := b.Timestamp()
|
newChainTime := b.Timestamp()
|
||||||
@@ -258,25 +251,22 @@ func (b *Block) Verify(ctx context.Context) error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// At and above the xvm execution_root activation height, the block must
|
// The block must carry the execution_root over the now-fully-applied
|
||||||
// carry the execution_root over the now-fully-applied post-block state.
|
// post-block state. Recompute it from the same canonical projection the
|
||||||
// Recompute it from the same canonical projection the builder stamped (one
|
// builder stamped (one shared code path — see BlockExecutionRoot) and reject
|
||||||
// shared code path — see BlockExecutionRoot) and reject any mismatch. Below
|
// any mismatch. [height] was already resolved (and validated against the
|
||||||
// activation this is skipped and the empty-root rule checked above stands.
|
// parent) above.
|
||||||
// [height] was already resolved (and validated against the parent) above.
|
expectedRoot, err := BlockExecutionRoot(parent.MerkleRoot(), txs, stateDiff, height)
|
||||||
if b.manager.backend.Config.IsMerkleRootActivated(height) {
|
if err != nil {
|
||||||
expectedRoot, err := BlockExecutionRoot(parent.MerkleRoot(), txs, stateDiff, height)
|
return fmt.Errorf("failed to compute expected block execution root: %w", err)
|
||||||
if err != nil {
|
}
|
||||||
return fmt.Errorf("failed to compute expected block execution root: %w", err)
|
if merkleRoot != expectedRoot {
|
||||||
}
|
return fmt.Errorf(
|
||||||
if merkleRoot != expectedRoot {
|
"%w: block root %s, expected %s",
|
||||||
return fmt.Errorf(
|
ErrUnexpectedMerkleRoot,
|
||||||
"%w: block root %s, expected %s",
|
merkleRoot,
|
||||||
ErrUnexpectedMerkleRoot,
|
expectedRoot,
|
||||||
merkleRoot,
|
)
|
||||||
expectedRoot,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that the block has been executed, we can add the block data to the
|
// Now that the block has been executed, we can add the block data to the
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"github.com/luxfi/ids"
|
"github.com/luxfi/ids"
|
||||||
"github.com/luxfi/math/set"
|
"github.com/luxfi/math/set"
|
||||||
"github.com/luxfi/mock/gomock"
|
"github.com/luxfi/mock/gomock"
|
||||||
"github.com/luxfi/node/upgrade"
|
|
||||||
"github.com/luxfi/node/vms/xvm/block"
|
"github.com/luxfi/node/vms/xvm/block"
|
||||||
"github.com/luxfi/node/vms/xvm/config"
|
"github.com/luxfi/node/vms/xvm/config"
|
||||||
"github.com/luxfi/node/vms/xvm/metrics/metricsmock"
|
"github.com/luxfi/node/vms/xvm/metrics/metricsmock"
|
||||||
@@ -60,25 +59,6 @@ func TestBlockVerify(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedErr: nil,
|
expectedErr: nil,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "block timestamp too far in the future",
|
|
||||||
blockFunc: func(ctrl *gomock.Controller) *Block {
|
|
||||||
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{
|
|
||||||
backend: defaultTestBackend(false, nil),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectedErr: ErrUnexpectedMerkleRoot,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "block timestamp too far in the future",
|
name: "block timestamp too far in the future",
|
||||||
blockFunc: func(ctrl *gomock.Controller) *Block {
|
blockFunc: func(ctrl *gomock.Controller) *Block {
|
||||||
@@ -524,24 +504,44 @@ func TestBlockVerify(t *testing.T) {
|
|||||||
expectedErr: ErrConflictingParentTxs,
|
expectedErr: ErrConflictingParentTxs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// Happy path under the always-active execution_root rule: the block
|
||||||
|
// carries the canonical execution_root over the post-block state, so
|
||||||
|
// the executor's unconditional recompute matches and the block
|
||||||
|
// verifies. The post-block UTXO set is empty (this tx produces none),
|
||||||
|
// so the root is the canonical compose over the empty UTXO/asset
|
||||||
|
// families, the tx family, the parent root, and the height.
|
||||||
name: "happy path",
|
name: "happy path",
|
||||||
blockFunc: func(ctrl *gomock.Controller) *Block {
|
blockFunc: func(ctrl *gomock.Controller) *Block {
|
||||||
mockBlock := block.NewMockBlock(ctrl)
|
|
||||||
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
|
|
||||||
mockBlock.EXPECT().MerkleRoot().Return(ids.Empty).AnyTimes()
|
|
||||||
blockTimestamp := time.Now()
|
|
||||||
mockBlock.EXPECT().Timestamp().Return(blockTimestamp).AnyTimes()
|
|
||||||
blockHeight := uint64(1337)
|
blockHeight := uint64(1337)
|
||||||
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
|
parentRoot := ids.GenerateTestID()
|
||||||
|
|
||||||
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
|
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
|
||||||
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Syntactic verification passes
|
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Syntactic verification passes
|
||||||
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Semantic verification fails
|
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Semantic verification passes
|
||||||
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Execution passes
|
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Execution passes
|
||||||
mockUnsignedTx.EXPECT().InputIDs().AnyTimes()
|
mockUnsignedTx.EXPECT().InputIDs().AnyTimes()
|
||||||
tx := &txs.Tx{
|
mockUnsignedTx.EXPECT().SetBytes(gomock.Any()).AnyTimes()
|
||||||
Unsigned: mockUnsignedTx,
|
tx := &txs.Tx{Unsigned: mockUnsignedTx}
|
||||||
}
|
// Stable tx ID so the tx-family leaf (and thus the expected root)
|
||||||
|
// is deterministic for both the executor and this test.
|
||||||
|
tx.SetBytes(nil, []byte{0x01, 0x02, 0x03, 0x04})
|
||||||
|
|
||||||
|
// The execution_root the block must carry: the canonical root over
|
||||||
|
// the parent root + this block's tx family at [blockHeight], with an
|
||||||
|
// empty post-block UTXO set (this tx produces no UTXOs). Computed via
|
||||||
|
// the same shared BlockExecutionRoot the executor recomputes.
|
||||||
|
emptyState := statemock.NewChain(ctrl)
|
||||||
|
emptyState.EXPECT().UTXOs(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
|
expectedRoot, err := BlockExecutionRoot(parentRoot, []*txs.Tx{tx}, emptyState, blockHeight)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, ids.Empty, expectedRoot)
|
||||||
|
|
||||||
|
mockBlock := block.NewMockBlock(ctrl)
|
||||||
|
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
|
||||||
|
mockBlock.EXPECT().MerkleRoot().Return(expectedRoot).AnyTimes()
|
||||||
|
blockTimestamp := time.Now()
|
||||||
|
mockBlock.EXPECT().Timestamp().Return(blockTimestamp).AnyTimes()
|
||||||
|
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
|
||||||
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
|
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
|
||||||
|
|
||||||
parentID := ids.GenerateTestID()
|
parentID := ids.GenerateTestID()
|
||||||
@@ -549,10 +549,16 @@ func TestBlockVerify(t *testing.T) {
|
|||||||
|
|
||||||
mockParentBlock := block.NewMockBlock(ctrl)
|
mockParentBlock := block.NewMockBlock(ctrl)
|
||||||
mockParentBlock.EXPECT().Height().Return(blockHeight - 1)
|
mockParentBlock.EXPECT().Height().Return(blockHeight - 1)
|
||||||
|
// The executor recomputes the execution_root from the parent's root.
|
||||||
|
mockParentBlock.EXPECT().MerkleRoot().Return(parentRoot).AnyTimes()
|
||||||
|
|
||||||
mockParentState := statemock.NewDiff(ctrl)
|
mockParentState := statemock.NewDiff(ctrl)
|
||||||
mockParentState.EXPECT().GetLastAccepted().Return(parentID)
|
mockParentState.EXPECT().GetLastAccepted().Return(parentID)
|
||||||
mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp)
|
mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp)
|
||||||
|
// The post-block state diff pages the parent to enumerate the
|
||||||
|
// occupied UTXO set for the execution_root projection; the parent
|
||||||
|
// has none.
|
||||||
|
mockParentState.EXPECT().UTXOs(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
|
|
||||||
mempool, err := mempool.New("", metric.NewRegistry())
|
mempool, err := mempool.New("", metric.NewRegistry())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -976,10 +982,6 @@ func defaultTestBackend(bootstrapped bool, sharedMemory atomic.SharedMemory) *tx
|
|||||||
Config: &config.Config{
|
Config: &config.Config{
|
||||||
TxFee: 0,
|
TxFee: 0,
|
||||||
CreateAssetTxFee: 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{},
|
Log: log.NoLog{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/go-json-experiment/json"
|
"github.com/go-json-experiment/json"
|
||||||
jsonv1 "github.com/go-json-experiment/json/v1"
|
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/config"
|
||||||
"github.com/luxfi/node/vms/xvm/network"
|
"github.com/luxfi/node/vms/xvm/network"
|
||||||
)
|
)
|
||||||
@@ -18,12 +17,6 @@ var DefaultConfig = Config{
|
|||||||
Config: config.Config{
|
Config: config.Config{
|
||||||
TxFee: 1000, // 1000 nanoLux base transaction fee
|
TxFee: 1000, // 1000 nanoLux base transaction fee
|
||||||
CreateAssetTxFee: 10000, // 10000 nanoLux for asset creation
|
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,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,30 +13,4 @@ type Config struct {
|
|||||||
|
|
||||||
// IndexTransactions enables transaction indexing by address
|
// IndexTransactions enables transaction indexing by address
|
||||||
IndexTransactions bool `json:"indexTransactions"`
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ import (
|
|||||||
txexecutor "github.com/luxfi/node/vms/xvm/txs/executor"
|
txexecutor "github.com/luxfi/node/vms/xvm/txs/executor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func u64ptr(v uint64) *uint64 { return &v }
|
// TestMerkleRootStampedAndVerified is the always-active end-to-end proof: a VM
|
||||||
|
// with the default config builds a real block that carries the xvm
|
||||||
// TestMerkleRootGateOffDefaultBlockEmpty is deliverable case (a), end to end: a
|
// execution_root over the post-block state (never empty), that block verifies
|
||||||
// VM with the default config (gate OFF, never sentinel) builds and accepts a
|
// and accepts, and a sibling block whose root is tampered is rejected with
|
||||||
// real block whose merkle root is empty — the historical behavior, unchanged.
|
// ErrUnexpectedMerkleRoot. There is no activation gate — the execution_root is
|
||||||
func TestMerkleRootGateOffDefaultBlockEmpty(t *testing.T) {
|
// part of the current version, stamped and verified unconditionally.
|
||||||
|
func TestMerkleRootStampedAndVerified(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
env := setup(t, &envConfig{fork: upgrade.Default})
|
env := setup(t, &envConfig{fork: upgrade.Default})
|
||||||
@@ -37,33 +38,6 @@ func TestMerkleRootGateOffDefaultBlockEmpty(t *testing.T) {
|
|||||||
blkIntf, err := env.vm.BuildBlock(context.Background())
|
blkIntf, err := env.vm.BuildBlock(context.Background())
|
||||||
require.NoError(err)
|
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
|
// The builder stamped the execution_root: non-empty, and equal to the
|
||||||
// canonical projection over the block's parent root + post-block state. The
|
// canonical projection over the block's parent root + post-block state. The
|
||||||
// CreateAssetTx in this block produces UTXOs, so the projected UTXO family is
|
// CreateAssetTx in this block produces UTXOs, so the projected UTXO family is
|
||||||
@@ -71,7 +45,7 @@ func TestMerkleRootGateOnAcceptsAndRejects(t *testing.T) {
|
|||||||
// (empty-family) value.
|
// (empty-family) value.
|
||||||
built := blkIntf.(*blkexecutor.Block)
|
built := blkIntf.(*blkexecutor.Block)
|
||||||
root := built.MerkleRoot()
|
root := built.MerkleRoot()
|
||||||
require.NotEqual(ids.Empty, root, "gate on: built block must carry a non-empty root")
|
require.NotEqual(ids.Empty, root, "built block must carry a non-empty execution_root")
|
||||||
|
|
||||||
parentID := built.Parent()
|
parentID := built.Parent()
|
||||||
parentBlk, err := env.vm.chainManager.GetStatelessBlock(parentID)
|
parentBlk, err := env.vm.chainManager.GetStatelessBlock(parentID)
|
||||||
@@ -91,7 +65,7 @@ func TestMerkleRootGateOnAcceptsAndRejects(t *testing.T) {
|
|||||||
// produced outputs), so the stamped root is the real execution_root.
|
// produced outputs), so the stamped root is the real execution_root.
|
||||||
postUTXOs, err := postState.UTXOs(ids.Empty, 0)
|
postUTXOs, err := postState.UTXOs(ids.Empty, 0)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.NotEmpty(postUTXOs, "gate on: the post-block UTXO set the root commits to must be non-empty")
|
require.NotEmpty(postUTXOs, "the post-block UTXO set the root commits to must be non-empty")
|
||||||
|
|
||||||
// Build a sibling block identical in every way EXCEPT a deliberately wrong
|
// 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
|
// root, parse it through the VM, and verify it: the executor recomputes the
|
||||||
@@ -118,6 +92,43 @@ func TestMerkleRootGateOnAcceptsAndRejects(t *testing.T) {
|
|||||||
require.NoError(blkIntf.Accept(context.Background()))
|
require.NoError(blkIntf.Accept(context.Background()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMerkleRootEmptyRootRejected confirms the empty root is not a valid block
|
||||||
|
// root under the always-active rule: a block whose root is ids.Empty (the
|
||||||
|
// historical pre-activation shape) no longer verifies, because the executor
|
||||||
|
// recomputes the real execution_root and rejects the mismatch. This pins that
|
||||||
|
// there is no surviving empty-root path.
|
||||||
|
func TestMerkleRootEmptyRootRejected(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)
|
||||||
|
built := blkIntf.(*blkexecutor.Block)
|
||||||
|
|
||||||
|
// A sibling block identical to the built block but carrying an EMPTY root.
|
||||||
|
cm := env.vm.parser.Codec()
|
||||||
|
empty, err := block.NewStandardBlock(
|
||||||
|
built.Parent(),
|
||||||
|
built.Height(),
|
||||||
|
time.Unix(int64(built.Timestamp().Unix()), 0),
|
||||||
|
built.Txs(),
|
||||||
|
cm,
|
||||||
|
)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Equal(ids.Empty, empty.MerkleRoot())
|
||||||
|
|
||||||
|
emptyParsed, err := env.vm.ParseBlock(context.Background(), empty.Bytes())
|
||||||
|
require.NoError(err)
|
||||||
|
err = emptyParsed.Verify(context.Background())
|
||||||
|
require.ErrorIs(err, blkexecutor.ErrUnexpectedMerkleRoot,
|
||||||
|
"an empty root must be rejected: the real execution_root is non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
// postBlockState reconstructs the post-block state a block's execution_root is
|
// postBlockState reconstructs the post-block state a block's execution_root is
|
||||||
// projected over: a state diff on [parentID] with [blkTxs] executed in order.
|
// projected over: a state diff on [parentID] with [blkTxs] executed in order.
|
||||||
// It mirrors the state mutation block verification performs (vms/xvm/block/
|
// It mirrors the state mutation block verification performs (vms/xvm/block/
|
||||||
|
|||||||
@@ -54,10 +54,6 @@ type envConfig struct {
|
|||||||
notLinearized bool
|
notLinearized bool
|
||||||
additionalFxs []interface{}
|
additionalFxs []interface{}
|
||||||
indexTransactions bool // Enable transaction indexing
|
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
|
// testEnv is the test environment
|
||||||
@@ -274,9 +270,6 @@ func setup(t testing.TB, config *envConfig) *testEnv {
|
|||||||
if config.indexTransactions {
|
if config.indexTransactions {
|
||||||
vmConfig.IndexTransactions = true
|
vmConfig.IndexTransactions = true
|
||||||
}
|
}
|
||||||
if config.merkleRootActivationHeight != nil {
|
|
||||||
vmConfig.MerkleRootActivationHeight = *config.merkleRootActivationHeight
|
|
||||||
}
|
|
||||||
configBytes, err := json.Marshal(vmConfig, jsonv1.FormatDurationAsNano(true))
|
configBytes, err := json.Marshal(vmConfig, jsonv1.FormatDurationAsNano(true))
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user