Files
node/vms/xvm/block/executor/block_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

1020 lines
35 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package executor
import (
"context"
"errors"
"testing"
"time"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/stretchr/testify/require"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/mock/gomock"
"github.com/luxfi/node/vms/xvm/block"
"github.com/luxfi/node/vms/xvm/config"
"github.com/luxfi/node/vms/xvm/metrics/metricsmock"
"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/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) {
type test struct {
name string
blockFunc func(*gomock.Controller) *Block
expectedErr error
postVerify func(*require.Assertions, *Block)
}
tests := []test{
{
name: "block already verified",
blockFunc: func(ctrl *gomock.Controller) *Block {
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
b := &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{},
},
}
b.manager.blkIDToState[b.ID()] = &blockState{
statelessBlock: b.Block,
}
return b
},
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.Empty).AnyTimes()
now := time.Now()
tooFarInFutureTime := now.Add(SyncBound + 1)
mockBlock.EXPECT().Timestamp().Return(tooFarInFutureTime).AnyTimes()
clk := &mockable.Clock{}
clk.Set(now)
return &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
clk: clk,
},
}
},
expectedErr: ErrTimestampBeyondSyncBound,
},
{
name: "block contains no transactions",
blockFunc: func(ctrl *gomock.Controller) *Block {
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().MerkleRoot().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().Timestamp().Return(time.Now()).AnyTimes()
mockBlock.EXPECT().Txs().Return(nil).AnyTimes()
return &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{},
clk: &mockable.Clock{},
},
}
},
expectedErr: ErrEmptyBlock,
},
{
name: "block transaction fails verification",
blockFunc: func(ctrl *gomock.Controller) *Block {
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().MerkleRoot().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().Timestamp().Return(time.Now()).AnyTimes()
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(errTest)
errTx := &txs.Tx{
Unsigned: mockUnsignedTx,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{errTx}).AnyTimes()
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
return &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
mempool: mempool,
metrics: metricsmock.NewMetrics(ctrl),
blkIDToState: map[ids.ID]*blockState{},
clk: &mockable.Clock{},
},
}
},
expectedErr: errTest,
},
{
name: "parent doesn't exist",
blockFunc: func(ctrl *gomock.Controller) *Block {
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().MerkleRoot().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().Timestamp().Return(time.Now()).AnyTimes()
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil)
tx := &txs.Tx{
Unsigned: mockUnsignedTx,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockState := statemock.NewState(ctrl)
mockState.EXPECT().GetBlock(parentID).Return(nil, errTest)
return &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
state: mockState,
blkIDToState: map[ids.ID]*blockState{},
clk: &mockable.Clock{},
},
}
},
expectedErr: errTest,
},
{
name: "block height isn't parent height + 1",
blockFunc: func(ctrl *gomock.Controller) *Block {
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().MerkleRoot().Return(ids.Empty).AnyTimes()
mockBlock.EXPECT().Timestamp().Return(time.Now()).AnyTimes()
blockHeight := uint64(1337)
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil)
tx := &txs.Tx{
Unsigned: mockUnsignedTx,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockState := statemock.NewState(ctrl)
mockParentBlock := block.NewMockBlock(ctrl)
mockParentBlock.EXPECT().Height().Return(blockHeight) // Should be blockHeight - 1
mockState.EXPECT().GetBlock(parentID).Return(mockParentBlock, nil)
return &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
state: mockState,
blkIDToState: map[ids.ID]*blockState{},
clk: &mockable.Clock{},
},
}
},
expectedErr: ErrIncorrectHeight,
},
{
name: "block timestamp before parent timestamp",
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)
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil)
tx := &txs.Tx{
Unsigned: mockUnsignedTx,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockParentBlock := block.NewMockBlock(ctrl)
mockParentBlock.EXPECT().Height().Return(blockHeight - 1)
mockParentState := statemock.NewDiff(ctrl)
mockParentState.EXPECT().GetLastAccepted().Return(parentID)
mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp.Add(1))
return &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{
parentID: {
onAcceptState: mockParentState,
statelessBlock: mockParentBlock,
},
},
clk: &mockable.Clock{},
lastAccepted: parentID,
},
}
},
expectedErr: ErrChildBlockEarlierThanParent,
},
{
name: "tx fails semantic verification",
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)
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Syntactic verification passes
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(errTest).Times(1) // Semantic verification fails
tx := &txs.Tx{
Unsigned: mockUnsignedTx,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockParentBlock := block.NewMockBlock(ctrl)
mockParentBlock.EXPECT().Height().Return(blockHeight - 1)
mockParentState := statemock.NewDiff(ctrl)
mockParentState.EXPECT().GetLastAccepted().Return(parentID)
mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp)
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
return &Block{
Block: mockBlock,
manager: &manager{
backend: defaultTestBackend(false, nil),
mempool: mempool,
metrics: metricsmock.NewMetrics(ctrl),
blkIDToState: map[ids.ID]*blockState{
parentID: {
onAcceptState: mockParentState,
statelessBlock: mockParentBlock,
},
},
clk: &mockable.Clock{},
lastAccepted: parentID,
},
}
},
expectedErr: errTest,
},
{
name: "tx fails execution",
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)
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
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) // Semantic verification fails
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(errTest).Times(1) // Execution fails
tx := &txs.Tx{
Unsigned: mockUnsignedTx,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockParentBlock := block.NewMockBlock(ctrl)
mockParentBlock.EXPECT().Height().Return(blockHeight - 1)
mockParentState := statemock.NewDiff(ctrl)
mockParentState.EXPECT().GetLastAccepted().Return(parentID)
mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp)
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
return &Block{
Block: mockBlock,
manager: &manager{
mempool: mempool,
metrics: metricsmock.NewMetrics(ctrl),
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{
parentID: {
onAcceptState: mockParentState,
statelessBlock: mockParentBlock,
},
},
clk: &mockable.Clock{},
lastAccepted: parentID,
},
}
},
expectedErr: errTest,
},
{
name: "tx imported inputs overlap",
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)
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
// tx1 and tx2 both consume imported input [inputID]
inputID := ids.GenerateTestID()
mockUnsignedTx1 := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx1.EXPECT().InputIDs().Return(set.NewSet[ids.ID](0)).AnyTimes()
mockUnsignedTx1.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Syntactic verification passes
mockUnsignedTx1.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Semantic verification passes
mockUnsignedTx1.EXPECT().Visit(gomock.Any()).DoAndReturn(
func(visitor txs.Visitor) error {
executor, ok := visitor.(*txexecutor.Executor)
if !ok {
return errors.New("wrong visitor type")
}
executor.Inputs.Add(inputID)
return nil
},
).Times(1) // Execution adds imported inputs
mockUnsignedTx2 := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx2.EXPECT().InputIDs().Return(set.NewSet[ids.ID](0)).AnyTimes()
mockUnsignedTx2.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Syntactic verification passes
mockUnsignedTx2.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Semantic verification passes
mockUnsignedTx2.EXPECT().Visit(gomock.Any()).DoAndReturn(
func(visitor txs.Visitor) error {
executor, ok := visitor.(*txexecutor.Executor)
if !ok {
return errors.New("wrong visitor type")
}
executor.Inputs.Add(inputID)
return nil
},
).Times(1) // Execution adds imported inputs - should conflict with tx1
tx1 := &txs.Tx{
Unsigned: mockUnsignedTx1,
}
tx2 := &txs.Tx{
Unsigned: mockUnsignedTx2,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx1, tx2}).AnyTimes()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockParentBlock := block.NewMockBlock(ctrl)
mockParentBlock.EXPECT().Height().Return(blockHeight - 1)
mockParentState := statemock.NewDiff(ctrl)
mockParentState.EXPECT().GetLastAccepted().Return(parentID).AnyTimes()
mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp).AnyTimes()
mockParentState.EXPECT().SetTimestamp(gomock.Any()).AnyTimes()
mockParentState.EXPECT().SetLastAccepted(gomock.Any()).AnyTimes()
mockParentState.EXPECT().AddBlock(gomock.Any()).AnyTimes()
mockParentState.EXPECT().AddTx(gomock.Any()).AnyTimes()
mockState := statemock.NewState(ctrl)
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
return &Block{
Block: mockBlock,
manager: &manager{
mempool: mempool,
state: mockState,
metrics: metricsmock.NewMetrics(ctrl),
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{
parentID: {
onAcceptState: mockParentState,
statelessBlock: mockParentBlock,
importedInputs: set.NewSet[ids.ID](0),
},
},
clk: &mockable.Clock{},
lastAccepted: parentID,
},
}
},
expectedErr: ErrConflictingBlockTxs,
},
{
name: "tx input overlaps with other tx",
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)
mockBlock.EXPECT().Height().Return(blockHeight).AnyTimes()
// tx1 and parent block both consume [inputID]
inputID := ids.GenerateTestID()
mockUnsignedTx := txsmock.NewUnsignedTx(ctrl)
mockUnsignedTx.EXPECT().InputIDs().Return(set.NewSet[ids.ID](0)).AnyTimes()
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Syntactic verification passes
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Semantic verification passes
mockUnsignedTx.EXPECT().Visit(gomock.Any()).DoAndReturn(
func(visitor txs.Visitor) error {
executor, ok := visitor.(*txexecutor.Executor)
if !ok {
return errors.New("wrong visitor type")
}
executor.Inputs.Add(inputID)
return nil
},
).Times(1) // Execution adds imported inputs
tx := &txs.Tx{
Unsigned: mockUnsignedTx,
}
mockBlock.EXPECT().Txs().Return([]*txs.Tx{tx}).AnyTimes()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockParentBlock := block.NewMockBlock(ctrl)
mockParentBlock.EXPECT().Height().Return(blockHeight - 1).AnyTimes()
mockParentBlock.EXPECT().Parent().Return(ids.Empty).AnyTimes()
mockParentState := statemock.NewDiff(ctrl)
mockParentState.EXPECT().GetLastAccepted().Return(parentID).AnyTimes()
mockParentState.EXPECT().GetTimestamp().Return(blockTimestamp).AnyTimes()
mockParentState.EXPECT().SetTimestamp(gomock.Any()).AnyTimes()
mockParentState.EXPECT().SetLastAccepted(gomock.Any()).AnyTimes()
mockParentState.EXPECT().AddBlock(gomock.Any()).AnyTimes()
mockParentState.EXPECT().AddTx(gomock.Any()).AnyTimes()
mockState := statemock.NewState(ctrl)
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
return &Block{
Block: mockBlock,
manager: &manager{
mempool: mempool,
state: mockState,
metrics: metricsmock.NewMetrics(ctrl),
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{
parentID: {
onAcceptState: mockParentState,
statelessBlock: mockParentBlock,
importedInputs: set.Of(inputID),
},
},
clk: &mockable.Clock{},
lastAccepted: parentID,
},
}
},
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",
blockFunc: func(ctrl *gomock.Controller) *Block {
blockHeight := uint64(1337)
parentRoot := ids.GenerateTestID()
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) // Semantic verification passes
mockUnsignedTx.EXPECT().Visit(gomock.Any()).Return(nil).Times(1) // Execution passes
mockUnsignedTx.EXPECT().InputIDs().AnyTimes()
mockUnsignedTx.EXPECT().SetBytes(gomock.Any()).AnyTimes()
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()
parentID := ids.GenerateTestID()
mockBlock.EXPECT().Parent().Return(parentID).AnyTimes()
mockParentBlock := block.NewMockBlock(ctrl)
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.EXPECT().GetLastAccepted().Return(parentID)
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())
require.NoError(t, err)
return &Block{
Block: mockBlock,
manager: &manager{
mempool: mempool,
metrics: metricsmock.NewMetrics(ctrl),
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{
parentID: {
onAcceptState: mockParentState,
statelessBlock: mockParentBlock,
},
},
clk: &mockable.Clock{},
lastAccepted: parentID,
},
}
},
expectedErr: nil,
postVerify: func(require *require.Assertions, b *Block) {
// Assert block is in the cache
blockState, ok := b.manager.blkIDToState[b.ID()]
require.True(ok)
require.Equal(b.Block, blockState.statelessBlock)
// Assert block is added to on accept state
persistedBlock, err := blockState.onAcceptState.GetBlock(b.ID())
require.NoError(err)
require.Equal(b.Block, persistedBlock)
// Assert block is set to last accepted
lastAccepted := b.ID()
require.Equal(lastAccepted, blockState.onAcceptState.GetLastAccepted())
// Assert txs are added to on accept state
blockTxs := b.Txs()
for _, tx := range blockTxs {
_, err := blockState.onAcceptState.GetTx(tx.ID())
require.NoError(err)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
b := tt.blockFunc(ctrl)
err := b.Verify(context.Background())
require.ErrorIs(err, tt.expectedErr)
if tt.postVerify != nil {
tt.postVerify(require, b)
}
})
}
}
func TestBlockAccept(t *testing.T) {
type test struct {
name string
blockFunc func(*gomock.Controller) *Block
expectedErr error
}
tests := []test{
{
name: "block not found",
blockFunc: func(ctrl *gomock.Controller) *Block {
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(ids.GenerateTestID()).AnyTimes()
mockBlock.EXPECT().Txs().Return([]*txs.Tx{}).AnyTimes()
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
return &Block{
Block: mockBlock,
manager: &manager{
mempool: mempool,
metrics: metricsmock.NewMetrics(ctrl),
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{},
},
}
},
expectedErr: ErrBlockNotFound,
},
{
name: "can't get commit batch",
blockFunc: func(ctrl *gomock.Controller) *Block {
blockID := ids.GenerateTestID()
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(blockID).AnyTimes()
mockBlock.EXPECT().Txs().Return([]*txs.Tx{}).AnyTimes()
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
mockManagerState := statemock.NewState(ctrl)
mockManagerState.EXPECT().CommitBatch().Return(nil, errTest)
mockManagerState.EXPECT().Abort()
mockOnAcceptState := statemock.NewDiff(ctrl)
mockOnAcceptState.EXPECT().Apply(mockManagerState)
return &Block{
Block: mockBlock,
manager: &manager{
state: mockManagerState,
mempool: mempool,
backend: defaultTestBackend(false, nil),
blkIDToState: map[ids.ID]*blockState{
blockID: {
onAcceptState: mockOnAcceptState,
},
},
},
}
},
expectedErr: errTest,
},
{
name: "can't apply shared memory",
blockFunc: func(ctrl *gomock.Controller) *Block {
blockID := ids.GenerateTestID()
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(blockID).AnyTimes()
mockBlock.EXPECT().Txs().Return([]*txs.Tx{}).AnyTimes()
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
mockManagerState := statemock.NewState(ctrl)
// Note the returned batch is nil but not used
// because we mock the call to shared memory
mockManagerState.EXPECT().CommitBatch().Return(nil, nil)
mockManagerState.EXPECT().Abort()
mockSharedMemory := atomicmock.NewSharedMemory(ctrl)
mockSharedMemory.EXPECT().Apply(gomock.Any(), gomock.Any()).Return(errTest)
mockOnAcceptState := statemock.NewDiff(ctrl)
mockOnAcceptState.EXPECT().Apply(mockManagerState)
return &Block{
Block: mockBlock,
manager: &manager{
state: mockManagerState,
mempool: mempool,
backend: defaultTestBackend(false, mockSharedMemory),
blkIDToState: map[ids.ID]*blockState{
blockID: {
onAcceptState: mockOnAcceptState,
},
},
},
}
},
expectedErr: errTest,
},
{
name: "failed to apply metrics",
blockFunc: func(ctrl *gomock.Controller) *Block {
blockID := ids.GenerateTestID()
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(blockID).AnyTimes()
mockBlock.EXPECT().Txs().Return([]*txs.Tx{}).AnyTimes()
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
mockManagerState := statemock.NewState(ctrl)
// Note the returned batch is nil but not used
// because we mock the call to shared memory
mockManagerState.EXPECT().CommitBatch().Return(nil, nil)
mockManagerState.EXPECT().Abort()
mockSharedMemory := atomicmock.NewSharedMemory(ctrl)
mockSharedMemory.EXPECT().Apply(gomock.Any(), gomock.Any()).Return(nil)
mockOnAcceptState := statemock.NewDiff(ctrl)
mockOnAcceptState.EXPECT().Apply(mockManagerState)
metrics := metricsmock.NewMetrics(ctrl)
metrics.EXPECT().MarkBlockAccepted(gomock.Any()).Return(errTest)
return &Block{
Block: mockBlock,
manager: &manager{
state: mockManagerState,
mempool: mempool,
metrics: metrics,
backend: defaultTestBackend(false, mockSharedMemory),
blkIDToState: map[ids.ID]*blockState{
blockID: {
onAcceptState: mockOnAcceptState,
},
},
},
}
},
expectedErr: errTest,
},
{
name: "no error",
blockFunc: func(ctrl *gomock.Controller) *Block {
blockID := ids.GenerateTestID()
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(blockID).AnyTimes()
mockBlock.EXPECT().Height().Return(uint64(0)).AnyTimes()
mockBlock.EXPECT().Parent().Return(ids.GenerateTestID()).AnyTimes()
mockBlock.EXPECT().Txs().Return([]*txs.Tx{}).AnyTimes()
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
mockManagerState := statemock.NewState(ctrl)
// Note the returned batch is nil but not used
// because we mock the call to shared memory
mockManagerState.EXPECT().CommitBatch().Return(nil, nil)
mockManagerState.EXPECT().Abort()
// Checksum() is only called if LuxCtx is set and supports Trace logging
// Since we don't set LuxCtx in this test, Checksum() won't be called
// mockManagerState.EXPECT().Checksum().Return(ids.Empty)
mockSharedMemory := atomicmock.NewSharedMemory(ctrl)
mockSharedMemory.EXPECT().Apply(gomock.Any(), gomock.Any()).Return(nil)
mockOnAcceptState := statemock.NewDiff(ctrl)
mockOnAcceptState.EXPECT().Apply(mockManagerState)
metrics := metricsmock.NewMetrics(ctrl)
metrics.EXPECT().MarkBlockAccepted(gomock.Any()).Return(nil)
return &Block{
Block: mockBlock,
manager: &manager{
state: mockManagerState,
mempool: mempool,
metrics: metrics,
backend: defaultTestBackend(false, mockSharedMemory),
blkIDToState: map[ids.ID]*blockState{
blockID: {
onAcceptState: mockOnAcceptState,
},
},
},
}
},
expectedErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
b := tt.blockFunc(ctrl)
err := b.Accept(context.Background())
require.ErrorIs(err, tt.expectedErr)
if err == nil {
// Make sure block is removed from cache
_, ok := b.manager.blkIDToState[b.ID()]
require.False(ok)
}
})
}
}
func TestBlockReject(t *testing.T) {
type test struct {
name string
blockFunc func(*gomock.Controller) *Block
}
tests := []test{
{
name: "one tx passes verification; one fails syntactic verification; one fails semantic verification; one fails execution",
blockFunc: func(ctrl *gomock.Controller) *Block {
blockID := ids.GenerateTestID()
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(blockID).AnyTimes()
mockBlock.EXPECT().Height().Return(uint64(0)).AnyTimes()
mockBlock.EXPECT().Parent().Return(ids.GenerateTestID()).AnyTimes()
unsignedValidTx := txsmock.NewUnsignedTx(ctrl)
unsignedValidTx.EXPECT().SetBytes(gomock.Any())
unsignedValidTx.EXPECT().Visit(gomock.Any()).Return(nil).AnyTimes() // Passes verification and execution
unsignedValidTx.EXPECT().InputIDs().Return(nil)
unsignedSyntacticallyInvalidTx := txsmock.NewUnsignedTx(ctrl)
unsignedSyntacticallyInvalidTx.EXPECT().SetBytes(gomock.Any())
unsignedSyntacticallyInvalidTx.EXPECT().Visit(gomock.Any()).Return(errTest) // Fails syntactic verification
unsignedSemanticallyInvalidTx := txsmock.NewUnsignedTx(ctrl)
unsignedSemanticallyInvalidTx.EXPECT().SetBytes(gomock.Any())
unsignedSemanticallyInvalidTx.EXPECT().Visit(gomock.Any()).Return(nil) // Passes syntactic verification
unsignedSemanticallyInvalidTx.EXPECT().Visit(gomock.Any()).Return(errTest) // Fails semantic verification
unsignedExecutionFailsTx := txsmock.NewUnsignedTx(ctrl)
unsignedExecutionFailsTx.EXPECT().SetBytes(gomock.Any())
unsignedExecutionFailsTx.EXPECT().Visit(gomock.Any()).Return(nil) // Passes syntactic verification
unsignedExecutionFailsTx.EXPECT().Visit(gomock.Any()).Return(nil) // Passes semantic verification
unsignedExecutionFailsTx.EXPECT().Visit(gomock.Any()).Return(errTest) // Fails execution
// Give each tx a unique ID
validTx := &txs.Tx{Unsigned: unsignedValidTx}
validTx.SetBytes(utils.RandomBytes(16), utils.RandomBytes(16))
syntacticallyInvalidTx := &txs.Tx{Unsigned: unsignedSyntacticallyInvalidTx}
syntacticallyInvalidTx.SetBytes(utils.RandomBytes(16), utils.RandomBytes(16))
semanticallyInvalidTx := &txs.Tx{Unsigned: unsignedSemanticallyInvalidTx}
semanticallyInvalidTx.SetBytes(utils.RandomBytes(16), utils.RandomBytes(16))
executionFailsTx := &txs.Tx{Unsigned: unsignedExecutionFailsTx}
executionFailsTx.SetBytes(utils.RandomBytes(16), utils.RandomBytes(16))
mockBlock.EXPECT().Txs().Return([]*txs.Tx{
validTx,
syntacticallyInvalidTx,
semanticallyInvalidTx,
executionFailsTx,
})
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
lastAcceptedID := ids.GenerateTestID()
mockState := statemock.NewState(ctrl)
mockState.EXPECT().GetLastAccepted().Return(lastAcceptedID).AnyTimes()
mockState.EXPECT().GetTimestamp().Return(time.Now()).AnyTimes()
return &Block{
Block: mockBlock,
manager: &manager{
lastAccepted: lastAcceptedID,
mempool: mempool,
metrics: metricsmock.NewMetrics(ctrl),
backend: defaultTestBackend(true, nil),
state: mockState,
blkIDToState: map[ids.ID]*blockState{
blockID: {},
},
},
}
},
},
{
name: "all txs valid",
blockFunc: func(ctrl *gomock.Controller) *Block {
blockID := ids.GenerateTestID()
mockBlock := block.NewMockBlock(ctrl)
mockBlock.EXPECT().ID().Return(blockID).AnyTimes()
mockBlock.EXPECT().Height().Return(uint64(0)).AnyTimes()
mockBlock.EXPECT().Parent().Return(ids.GenerateTestID()).AnyTimes()
unsignedTx1 := txsmock.NewUnsignedTx(ctrl)
unsignedTx1.EXPECT().SetBytes(gomock.Any())
unsignedTx1.EXPECT().Visit(gomock.Any()).Return(nil).AnyTimes() // Passes verification and execution
unsignedTx1.EXPECT().InputIDs().Return(nil)
unsignedTx2 := txsmock.NewUnsignedTx(ctrl)
unsignedTx2.EXPECT().SetBytes(gomock.Any())
unsignedTx2.EXPECT().Visit(gomock.Any()).Return(nil).AnyTimes() // Passes verification and execution
unsignedTx2.EXPECT().InputIDs().Return(nil)
// Give each tx a unique ID
tx1 := &txs.Tx{Unsigned: unsignedTx1}
tx1.SetBytes(utils.RandomBytes(16), utils.RandomBytes(16))
tx2 := &txs.Tx{Unsigned: unsignedTx2}
tx2.SetBytes(utils.RandomBytes(16), utils.RandomBytes(16))
mockBlock.EXPECT().Txs().Return([]*txs.Tx{
tx1,
tx2,
})
mempool, err := mempool.New("", metric.NewRegistry())
require.NoError(t, err)
lastAcceptedID := ids.GenerateTestID()
mockState := statemock.NewState(ctrl)
mockState.EXPECT().GetLastAccepted().Return(lastAcceptedID).AnyTimes()
mockState.EXPECT().GetTimestamp().Return(time.Now()).AnyTimes()
return &Block{
Block: mockBlock,
manager: &manager{
lastAccepted: lastAcceptedID,
mempool: mempool,
metrics: metricsmock.NewMetrics(ctrl),
backend: defaultTestBackend(true, nil),
state: mockState,
blkIDToState: map[ids.ID]*blockState{
blockID: {},
},
},
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
ctrl := gomock.NewController(t)
b := tt.blockFunc(ctrl)
require.NoError(b.Reject(context.Background()))
_, ok := b.manager.blkIDToState[b.ID()]
require.False(ok)
})
}
}
func defaultTestBackend(bootstrapped bool, sharedMemory atomic.SharedMemory) *txexecutor.Backend {
ctx := context.Background()
backend := &txexecutor.Backend{
Bootstrapped: bootstrapped,
Ctx: ctx,
Runtime: &runtime.Runtime{ChainID: ids.GenerateTestID()},
Config: &config.Config{
TxFee: 0,
CreateAssetTxFee: 0,
},
Log: log.NoLog{},
}
if sharedMemory != nil {
backend.SharedMemory = &sharedMemoryAdapter{sm: sharedMemory}
}
return backend
}
// sharedMemoryAdapter adapts atomic.SharedMemory to txexecutor.SharedMemory
type sharedMemoryAdapter struct {
sm atomic.SharedMemory
}
func (s *sharedMemoryAdapter) Get(peerChainID ids.ID, keys [][]byte) ([][]byte, error) {
return s.sm.Get(peerChainID, keys)
}
func (s *sharedMemoryAdapter) Apply(requests map[ids.ID]interface{}, batchArgs ...interface{}) error {
// Convert map[ids.ID]interface{} to map[ids.ID]*atomic.Requests
atomicRequests := make(map[ids.ID]*atomic.Requests)
for chainID, req := range requests {
if atomicReq, ok := req.(*atomic.Requests); ok {
atomicRequests[chainID] = atomicReq
}
}
// Extract database.Batch from variadic args
var batch database.Batch
if len(batchArgs) > 0 {
if b, ok := batchArgs[0].(database.Batch); ok {
batch = b
}
}
return s.sm.Apply(atomicRequests, batch)
}