Files
coreth/plugin/evm/syncervm_test.go
zeekayandHanzo Dev f2d18226a0 Convert BlackholeAddr to a geth address where one is wanted
constants.BlackholeAddr is a plain [20]byte now, so constants no longer imports
geth. Go assigns an unnamed [20]byte to common.Address implicitly, so every call
site still compiled, but one of them changed behavior: fmt %v prints
[1 0 0 ...] for an array and 0x0100000000000000000000000000000000000000 for a
common.Address, so the invalid-coinbase error in syntacticVerify lost its hex
form. Writing common.Address(constants.BlackholeAddr) restores the message and
names the type at the boundary; the conversion is free.

Six sites take the conversion: the etherbase in initializeChain, the simulated
backend etherbase, the coinbase error text, the module address in the registerer
test, and the two SetCoinbase calls. The last two sit in files carrying
//go:build ignore, so the compiler does not check them.

Two sites compare bytes and stay as they are, the registerer against a module
address and syntacticVerify against the header coinbase.

go.mod is untouched. The requirement still reads constants v1.6.2 and the bump
waits on the tag.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-06 00:28:51 -07:00

689 lines
24 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build ignore
// +build ignore
// TODO: This file needs refactoring for new consensus API changes:
// - AppRequestFailed expects *AppError not error
// - Initialize signature changed
// - Sender field names changed (SendAppResponseF -> SendResponseF, etc.)
// - quasar package references need to be updated
package evm
import (
"context"
"fmt"
"math/big"
"math/rand"
"sync"
"testing"
"time"
"github.com/luxfi/coreth/plugin/evm/atomic"
"github.com/luxfi/coreth/plugin/evm/atomic/atomictest"
atomicvm "github.com/luxfi/coreth/plugin/evm/atomic/vm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/luxfi/consensus"
commonEng "github.com/luxfi/consensus/core"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/consensus/engine/enginetest"
"github.com/luxfi/constants"
"github.com/luxfi/crypto/secp256k1"
luxdatabase "github.com/luxfi/database"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/upgrade/upgradetest"
"github.com/luxfi/vm/api/metrics"
luxatomic "github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/warp"
"github.com/luxfi/constants"
"github.com/luxfi/coreth/consensus/dummy"
"github.com/luxfi/coreth/core"
"github.com/luxfi/coreth/core/coretest"
"github.com/luxfi/coreth/params"
"github.com/luxfi/coreth/plugin/evm/customrawdb"
"github.com/luxfi/coreth/plugin/evm/customtypes"
"github.com/luxfi/coreth/plugin/evm/database"
"github.com/luxfi/coreth/predicate"
statesyncclient "github.com/luxfi/coreth/sync/client"
"github.com/luxfi/coreth/sync/statesync/statesynctest"
syncervm "github.com/luxfi/coreth/sync/vm"
"github.com/luxfi/coreth/utils/utilstest"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/ethdb"
"github.com/luxfi/geth/rlp"
"github.com/luxfi/geth/trie"
"github.com/luxfi/geth/triedb"
log "github.com/luxfi/log"
)
func TestSkipStateSync(t *testing.T) {
rand.Seed(1)
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 300, // must be greater than [syncableInterval] to skip sync
syncMode: block.StateSyncSkipped,
}
vmSetup := createSyncServerAndClientVMs(t, test, syncervm.ParentsToFetch)
testSyncerVM(t, vmSetup, test)
}
func TestStateSyncFromScratch(t *testing.T) {
rand.Seed(1)
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
}
vmSetup := createSyncServerAndClientVMs(t, test, syncervm.ParentsToFetch)
testSyncerVM(t, vmSetup, test)
}
func TestStateSyncFromScratchExceedParent(t *testing.T) {
rand.Seed(1)
numToGen := syncervm.ParentsToFetch + uint64(32)
test := syncTest{
syncableInterval: numToGen,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
}
vmSetup := createSyncServerAndClientVMs(t, test, int(numToGen))
testSyncerVM(t, vmSetup, test)
}
func TestStateSyncToggleEnabledToDisabled(t *testing.T) {
rand.Seed(1)
var lock sync.Mutex
reqCount := 0
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
responseIntercept: func(syncerVM *VM, nodeID ids.NodeID, requestID uint32, response []byte) {
lock.Lock()
defer lock.Unlock()
reqCount++
// Fail all requests after number 50 to interrupt the sync
if reqCount > 50 {
if err := syncerVM.AppRequestFailed(context.Background(), nodeID, requestID, consensus.ErrTimeout); err != nil {
panic(err)
}
if err := syncerVM.Client.Shutdown(); err != nil {
panic(err)
}
} else {
syncerVM.AppResponse(context.Background(), nodeID, requestID, response)
}
},
expectedErr: context.Canceled,
}
vmSetup := createSyncServerAndClientVMs(t, test, syncervm.ParentsToFetch)
// Perform sync resulting in early termination.
testSyncerVM(t, vmSetup, test)
test.syncMode = block.StateSyncStatic
test.responseIntercept = nil
test.expectedErr = nil
syncDisabledVM := atomicvm.WrapVM(&VM{})
appSender := &enginetest.Sender{}
appSender.SendGossipF = func(context.Context, warp.SendConfig, []byte) error { return nil }
appSender.SendRequestF = func(ctx context.Context, nodeSet set.Set[ids.NodeID], requestID uint32, request []byte) error {
nodeID, hasItem := nodeSet.Pop()
if !hasItem {
t.Fatal("expected nodeSet to contain at least 1 nodeID")
}
go vmSetup.serverVM.AppRequest(ctx, nodeID, requestID, time.Now().Add(1*time.Second), request)
return nil
}
// Reset metrics to allow re-initialization
vmSetup.syncerVM.ctx.Metrics = metrics.NewPrefixGatherer()
stateSyncDisabledConfigJSON := `{"state-sync-enabled":false}`
genesisJSONBytes := []byte(genesisJSON(forkToChainConfig[upgradetest.Latest]))
if err := syncDisabledVM.Initialize(
context.Background(),
vmSetup.syncerVM.ctx,
vmSetup.syncerDB,
genesisJSONBytes,
nil,
[]byte(stateSyncDisabledConfigJSON),
[]*commonEng.Fx{},
appSender,
); err != nil {
t.Fatal(err)
}
defer func() {
if err := syncDisabledVM.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
if height := syncDisabledVM.LastAcceptedExtendedBlock().Height(); height != 0 {
t.Fatalf("Unexpected last accepted height: %d", height)
}
enabled, err := syncDisabledVM.StateSyncEnabled(context.Background())
assert.NoError(t, err)
assert.False(t, enabled, "sync should be disabled")
// Process the first 10 blocks from the serverVM
for i := uint64(1); i < 10; i++ {
ethBlock := vmSetup.serverVM.blockChain.GetBlockByNumber(i)
if ethBlock == nil {
t.Fatalf("VM Server did not have a block available at height %d", i)
}
b, err := rlp.EncodeToBytes(ethBlock)
if err != nil {
t.Fatal(err)
}
blk, err := syncDisabledVM.ParseBlock(context.Background(), b)
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
}
// Verify the snapshot disk layer matches the last block root
lastRoot := syncDisabledVM.Blockchain().CurrentBlock().Root
if err := syncDisabledVM.Blockchain().Snapshots().Verify(lastRoot); err != nil {
t.Fatal(err)
}
syncDisabledVM.Blockchain().DrainAcceptorQueue()
// Create a new VM from the same database with state sync enabled.
syncReEnabledInnerVM := &VM{}
syncReEnabledVM := atomicvm.WrapVM(syncReEnabledInnerVM)
// Enable state sync in configJSON
configJSON := fmt.Sprintf(
`{"state-sync-enabled":true, "state-sync-min-blocks":%d}`,
test.stateSyncMinBlocks,
)
// Reset metrics to allow re-initialization
vmSetup.syncerVM.ctx.Metrics = metrics.NewPrefixGatherer()
if err := syncReEnabledVM.Initialize(
context.Background(),
vmSetup.syncerVM.ctx,
vmSetup.syncerDB,
genesisJSON,
nil,
[]byte(configJSON),
[]*commonEng.Fx{},
appSender,
); err != nil {
t.Fatal(err)
}
// override [serverVM]'s SendAppResponse function to trigger AppResponse on [syncerVM]
vmSetup.serverAppSender.SendAppResponseF = func(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error {
if test.responseIntercept == nil {
go syncReEnabledVM.AppResponse(ctx, nodeID, requestID, response)
} else {
go test.responseIntercept(syncReEnabledInnerVM, nodeID, requestID, response)
}
return nil
}
// connect peer to [syncerVM]
assert.NoError(t, syncReEnabledVM.Connected(
context.Background(),
vmSetup.serverVM.ctx.NodeID,
statesyncclient.StateSyncVersion,
))
enabled, err = syncReEnabledVM.StateSyncEnabled(context.Background())
assert.NoError(t, err)
assert.True(t, enabled, "sync should be enabled")
vmSetup.syncerVM = syncReEnabledInnerVM
testSyncerVM(t, vmSetup, test)
}
func TestVMShutdownWhileSyncing(t *testing.T) {
var (
lock sync.Mutex
vmSetup *syncVMSetup
)
reqCount := 0
test := syncTest{
syncableInterval: 256,
stateSyncMinBlocks: 50, // must be less than [syncableInterval] to perform sync
syncMode: block.StateSyncStatic,
responseIntercept: func(syncerVM *VM, nodeID ids.NodeID, requestID uint32, response []byte) {
lock.Lock()
defer lock.Unlock()
reqCount++
// Shutdown the VM after 50 requests to interrupt the sync
if reqCount == 50 {
// Note this verifies the VM shutdown does not time out while syncing.
require.NoError(t, vmSetup.shutdownOnceSyncerVM.Shutdown(context.Background()))
} else if reqCount < 50 {
require.NoError(t, syncerVM.AppResponse(context.Background(), nodeID, requestID, response))
}
},
expectedErr: context.Canceled,
}
vmSetup = createSyncServerAndClientVMs(t, test, syncervm.ParentsToFetch)
// Perform sync resulting in early termination.
testSyncerVM(t, vmSetup, test)
}
func createSyncServerAndClientVMs(t *testing.T, test syncTest, numBlocks int) *syncVMSetup {
var (
require = require.New(t)
importAmount = 2000000 * constants.Lux // 2M lux
alloc = map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
}
)
configJSON := fmt.Sprintf(`{"commit-interval": %d, "state-sync-commit-interval": %d}`, test.syncableInterval, test.syncableInterval)
server := newVM(t, testVMConfig{
utxos: alloc,
configJSON: configJSON,
})
t.Cleanup(func() {
log.Info("Shutting down server VM")
require.NoError(server.vm.Shutdown(context.Background()))
})
var (
importTx, exportTx *atomic.Tx
err error
)
generateAndAcceptBlocks(t, server.vm, numBlocks, func(i int, gen *core.BlockGen) {
b, err := predicate.NewResults().Bytes()
if err != nil {
t.Fatal(err)
}
gen.AppendExtra(b)
switch i {
case 0:
// spend the UTXOs from shared memory
importTx, err = server.atomicVM.NewImportTx(server.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
require.NoError(err)
require.NoError(server.atomicVM.AtomicMempool.AddLocalTx(importTx))
case 1:
// export some of the imported UTXOs to test exportTx is properly synced
exportTx, err = server.atomicVM.NewExportTx(
server.vm.ctx.UTXOAssetID,
importAmount/2,
server.vm.ctx.XChainID,
testShortIDAddrs[0],
initialBaseFee,
[]*secp256k1.PrivateKey{testKeys[0]},
)
require.NoError(err)
require.NoError(server.atomicVM.AtomicMempool.AddLocalTx(exportTx))
default: // Generate simple transfer transactions.
pk := testKeys[0].ToECDSA()
tx := types.NewTransaction(gen.TxNonce(testEthAddrs[0]), testEthAddrs[1], common.Big1, params.TxGas, initialBaseFee, nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(server.vm.chainID), pk)
require.NoError(err)
gen.AddTx(signedTx)
}
}, nil)
// override serverAtomicTrie's commitInterval so the call to [serverAtomicTrie.Index]
// creates a commit at the height [syncableInterval]. This is necessary to support
// fetching a state summary.
serverAtomicTrie := server.atomicVM.AtomicBackend.AtomicTrie()
require.NoError(serverAtomicTrie.Commit(test.syncableInterval, serverAtomicTrie.LastAcceptedRoot()))
require.NoError(server.vm.versiondb.Commit())
serverSharedMemories := atomictest.NewSharedMemories(server.atomicMemory, server.vm.ctx.ChainID, server.vm.ctx.XChainID)
importOps, err := atomictest.ConvertToAtomicOps(importTx)
require.NoError(err)
exportOps, err := atomictest.ConvertToAtomicOps(exportTx)
require.NoError(err)
serverSharedMemories.AssertOpsApplied(t, importOps)
serverSharedMemories.AssertOpsApplied(t, exportOps)
// make some accounts
trieDB := triedb.NewDatabase(server.vm.chaindb, nil)
root, accounts := statesynctest.FillAccountsWithOverlappingStorage(t, trieDB, types.EmptyRootHash, 1000, 16)
// patch serverVM's lastAcceptedBlock to have the new root
// and update the vm's state so the trie with accounts will
// be returned by StateSyncGetLastSummary
lastAccepted := server.vm.blockChain.LastAcceptedBlock()
patchedBlock := patchBlock(lastAccepted, root, server.vm.chaindb)
blockBytes, err := rlp.EncodeToBytes(patchedBlock)
require.NoError(err)
internalBlock, err := server.vm.parseBlock(context.Background(), blockBytes)
require.NoError(err)
require.NoError(server.vm.State.SetLastAcceptedBlock(internalBlock))
// initialise [syncerVM] with blank genesis state
stateSyncEnabledJSON := fmt.Sprintf(`{"state-sync-enabled":true, "state-sync-min-blocks": %d, "transaction-history": %d, "commit-interval": %d}`, test.stateSyncMinBlocks, 4, test.syncableInterval)
syncer := newVM(t, testVMConfig{
isSyncing: true,
configJSON: stateSyncEnabledJSON,
utxos: alloc,
})
shutdownOnceSyncerVM := &shutdownOnceVM{VM: syncer.vm}
t.Cleanup(func() {
require.NoError(shutdownOnceSyncerVM.Shutdown(context.Background()))
})
require.NoError(syncer.vm.SetState(context.Background(), quasar.StateSyncing))
enabled, err := syncer.vm.StateSyncEnabled(context.Background())
require.NoError(err)
require.True(enabled)
// override [serverVM]'s SendAppResponse function to trigger AppResponse on [syncerVM]
server.appSender.SendAppResponseF = func(ctx context.Context, nodeID ids.NodeID, requestID uint32, response []byte) error {
if test.responseIntercept == nil {
go syncer.vm.AppResponse(ctx, nodeID, requestID, response)
} else {
go test.responseIntercept(syncer.vm, nodeID, requestID, response)
}
return nil
}
// connect peer to [syncerVM]
require.NoError(
syncer.vm.Connected(
context.Background(),
server.vm.ctx.NodeID,
statesyncclient.StateSyncVersion,
),
)
// override [syncerVM]'s SendAppRequest function to trigger AppRequest on [serverVM]
syncer.appSender.SendAppRequestF = func(ctx context.Context, nodeSet set.Set[ids.NodeID], requestID uint32, request []byte) error {
nodeID, hasItem := nodeSet.Pop()
require.True(hasItem, "expected nodeSet to contain at least 1 nodeID")
require.NoError(server.vm.AppRequest(ctx, nodeID, requestID, time.Now().Add(1*time.Second), request))
return nil
}
return &syncVMSetup{
serverVM: server.vm,
serverAppSender: server.appSender,
includedAtomicTxs: []*atomic.Tx{
importTx,
exportTx,
},
fundedAccounts: accounts,
syncerVM: syncer.vm,
syncerDB: syncer.db,
syncerAtomicMemory: syncer.atomicMemory,
shutdownOnceSyncerVM: shutdownOnceSyncerVM,
}
}
// syncVMSetup contains the required set up for a client VM to perform state sync
// off of a server VM.
type syncVMSetup struct {
serverVM *VM
serverAppSender *enginetest.Sender
includedAtomicTxs []*atomic.Tx
fundedAccounts map[*utilstest.Key]*types.StateAccount
syncerVM *VM
syncerDB luxdatabase.Database
syncerAtomicMemory *luxatomic.Memory
shutdownOnceSyncerVM *shutdownOnceVM
}
type shutdownOnceVM struct {
*VM
shutdownOnce sync.Once
}
func (vm *shutdownOnceVM) Shutdown(ctx context.Context) error {
var err error
vm.shutdownOnce.Do(func() { err = vm.VM.Shutdown(ctx) })
return err
}
// syncTest contains both the actual VMs as well as the parameters with the expected output.
type syncTest struct {
responseIntercept func(vm *VM, nodeID ids.NodeID, requestID uint32, response []byte)
stateSyncMinBlocks uint64
syncableInterval uint64
syncMode block.StateSyncMode
expectedErr error
}
func testSyncerVM(t *testing.T, vmSetup *syncVMSetup, test syncTest) {
t.Helper()
var (
require = require.New(t)
serverVM = vmSetup.serverVM
includedAtomicTxs = vmSetup.includedAtomicTxs
fundedAccounts = vmSetup.fundedAccounts
syncerVM = vmSetup.syncerVM
syncerAtomicMemory = vmSetup.syncerAtomicMemory
)
// get last summary and test related methods
summary, err := serverVM.GetLastStateSummary(context.Background())
require.NoError(err, "error getting state sync last summary")
parsedSummary, err := syncerVM.ParseStateSummary(context.Background(), summary.Bytes())
require.NoError(err, "error parsing state summary")
retrievedSummary, err := serverVM.GetStateSummary(context.Background(), parsedSummary.Height())
require.NoError(err, "error getting state sync summary at height")
require.Equal(summary, retrievedSummary)
syncMode, err := parsedSummary.Accept(context.Background())
require.NoError(err, "error accepting state summary")
require.Equal(test.syncMode, syncMode)
if syncMode == block.StateSyncSkipped {
return
}
msg, err := syncerVM.WaitForEvent(context.Background())
require.Equal(commonEng.StateSyncDone, msg)
require.NoError(err)
// If the test is expected to error, assert the correct error is returned and finish the test.
err = syncerVM.Client.Error()
if test.expectedErr != nil {
require.ErrorIs(err, test.expectedErr)
// Note we re-open the database here to avoid a closed error when the test is for a shutdown VM.
chaindb := database.WrapDatabase(prefixdb.NewNested(ethDBPrefix, syncerVM.db))
assertSyncPerformedHeights(t, chaindb, map[uint64]struct{}{})
return
}
require.NoError(err, "state sync failed")
// set [syncerVM] to bootstrapping and verify the last accepted block has been updated correctly
// and that we can bootstrap and process some blocks.
require.NoError(syncerVM.SetState(context.Background(), quasar.Bootstrapping))
require.Equal(serverVM.LastAcceptedBlock().Height(), syncerVM.LastAcceptedBlock().Height(), "block height mismatch between syncer and server")
require.Equal(serverVM.LastAcceptedBlock().ID(), syncerVM.LastAcceptedBlock().ID(), "blockID mismatch between syncer and server")
require.True(syncerVM.blockChain.HasState(syncerVM.blockChain.LastAcceptedBlock().Root()), "unavailable state for last accepted block")
assertSyncPerformedHeights(t, syncerVM.chaindb, map[uint64]struct{}{retrievedSummary.Height(): {}})
lastNumber := syncerVM.blockChain.LastAcceptedBlock().NumberU64()
// check the last block is indexed
lastSyncedBlock := rawdb.ReadBlock(syncerVM.chaindb, rawdb.ReadCanonicalHash(syncerVM.chaindb, lastNumber), lastNumber)
for _, tx := range lastSyncedBlock.Transactions() {
index := rawdb.ReadTxLookupEntry(syncerVM.chaindb, tx.Hash())
require.NotNilf(index, "Miss transaction indices, number %d hash %s", lastNumber, tx.Hash().Hex())
}
// tail should be the last block synced
if syncerVM.ethConfig.TransactionHistory != 0 {
tail := lastSyncedBlock.NumberU64()
coretest.CheckTxIndices(t, &tail, tail, tail, tail, syncerVM.chaindb, true)
}
blocksToBuild := 10
txsPerBlock := 10
toAddress := testEthAddrs[1] // arbitrary choice
generateAndAcceptBlocks(t, syncerVM, blocksToBuild, func(_ int, gen *core.BlockGen) {
b, err := predicate.NewResults().Bytes()
if err != nil {
t.Fatal(err)
}
gen.AppendExtra(b)
i := 0
for k := range fundedAccounts {
tx := types.NewTransaction(gen.TxNonce(k.Address), toAddress, big.NewInt(1), 21000, initialBaseFee, nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(serverVM.chainConfig.ChainID), k.PrivateKey)
require.NoError(err)
gen.AddTx(signedTx)
i++
if i >= txsPerBlock {
break
}
}
},
func(block *types.Block) {
if syncerVM.ethConfig.TransactionHistory != 0 {
tail := block.NumberU64() - syncerVM.ethConfig.TransactionHistory + 1
// tail should be the minimum last synced block, since we skipped it to the last block
if tail < lastSyncedBlock.NumberU64() {
tail = lastSyncedBlock.NumberU64()
}
coretest.CheckTxIndices(t, &tail, tail, block.NumberU64(), block.NumberU64(), syncerVM.chaindb, true)
}
},
)
// check we can transition to [NormalOp] state and continue to process blocks.
require.NoError(syncerVM.SetState(context.Background(), quasar.NormalOp))
require.True(syncerVM.bootstrapped.Get())
// check atomic memory was synced properly
syncerSharedMemories := atomictest.NewSharedMemories(syncerAtomicMemory, syncerVM.ctx.ChainID, syncerVM.ctx.XChainID)
for _, tx := range includedAtomicTxs {
atomicOps, err := atomictest.ConvertToAtomicOps(tx)
require.NoError(err)
syncerSharedMemories.AssertOpsApplied(t, atomicOps)
}
// Generate blocks after we have entered normal consensus as well
generateAndAcceptBlocks(t, syncerVM, blocksToBuild, func(_ int, gen *core.BlockGen) {
b, err := predicate.NewResults().Bytes()
if err != nil {
t.Fatal(err)
}
gen.AppendExtra(b)
i := 0
for k := range fundedAccounts {
tx := types.NewTransaction(gen.TxNonce(k.Address), toAddress, big.NewInt(1), 21000, initialBaseFee, nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(serverVM.chainConfig.ChainID), k.PrivateKey)
require.NoError(err)
gen.AddTx(signedTx)
i++
if i >= txsPerBlock {
break
}
}
},
func(block *types.Block) {
if syncerVM.ethConfig.TransactionHistory != 0 {
tail := block.NumberU64() - syncerVM.ethConfig.TransactionHistory + 1
// tail should be the minimum last synced block, since we skipped it to the last block
if tail < lastSyncedBlock.NumberU64() {
tail = lastSyncedBlock.NumberU64()
}
coretest.CheckTxIndices(t, &tail, tail, block.NumberU64(), block.NumberU64(), syncerVM.chaindb, true)
}
},
)
}
// patchBlock returns a copy of [blk] with [root] and updates [db] to
// include the new block as canonical for [blk]'s height.
// This breaks the digestibility of the chain since after this call
// [blk] does not necessarily define a state transition from its parent
// state to the new state root.
func patchBlock(blk *types.Block, root common.Hash, db ethdb.Database) *types.Block {
header := blk.Header()
header.Root = root
receipts := rawdb.ReadRawReceipts(db, blk.Hash(), blk.NumberU64())
newBlk := customtypes.NewBlockWithExtData(
header, blk.Transactions(), blk.Uncles(), receipts, trie.NewStackTrie(nil), customtypes.BlockExtData(blk), true,
)
rawdb.WriteBlock(db, newBlk)
rawdb.WriteCanonicalHash(db, newBlk.Hash(), newBlk.NumberU64())
return newBlk
}
// generateAndAcceptBlocks uses [core.GenerateChain] to generate blocks, then
// calls Verify and Accept on each generated block
// TODO: consider using this helper function in vm_test.go and elsewhere in this package to clean up tests
func generateAndAcceptBlocks(t *testing.T, vm *VM, numBlocks int, gen func(int, *core.BlockGen), accepted func(*types.Block)) {
t.Helper()
// acceptExternalBlock defines a function to parse, verify, and accept a block once it has been
// generated by GenerateChain
acceptExternalBlock := func(block *types.Block) {
bytes, err := rlp.EncodeToBytes(block)
if err != nil {
t.Fatal(err)
}
vmBlock, err := vm.ParseBlock(context.Background(), bytes)
if err != nil {
t.Fatal(err)
}
if err := vmBlock.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vmBlock.Accept(context.Background()); err != nil {
t.Fatal(err)
}
if accepted != nil {
accepted(block)
}
}
_, _, err := core.GenerateChain(
vm.chainConfig,
vm.blockChain.LastAcceptedBlock(),
dummy.NewFakerWithCallbacks(vm.extensionConfig.ConsensusCallbacks),
vm.chaindb,
numBlocks,
10,
func(i int, g *core.BlockGen) {
g.SetOnBlockGenerated(acceptExternalBlock)
g.SetCoinbase(common.Address(constants.BlackholeAddr)) // necessary for syntactic validation of the block
gen(i, g)
},
)
if err != nil {
t.Fatal(err)
}
vm.blockChain.DrainAcceptorQueue()
}
// assertSyncPerformedHeights iterates over all heights the VM has synced to and
// verifies it matches [expected].
func assertSyncPerformedHeights(t *testing.T, db ethdb.Iteratee, expected map[uint64]struct{}) {
it := customrawdb.NewSyncPerformedIterator(db)
defer it.Release()
found := make(map[uint64]struct{}, len(expected))
for it.Next() {
found[customrawdb.UnpackSyncPerformedKey(it.Key())] = struct{}{}
}
require.NoError(t, it.Error())
require.Equal(t, expected, found)
}