diff --git a/go/replay/replay.go b/go/replay/replay.go index 15d66d87..9c4df1ad 100644 --- a/go/replay/replay.go +++ b/go/replay/replay.go @@ -209,7 +209,7 @@ func Replay(ctx context.Context, args ReplayArgs) (err error) { }() // Prepare the state - if err := prepareState(&args, chain, genesis); err != nil { + if err := prepareState(&args, chain, blockDb, genesis); err != nil { return fmt.Errorf("failed to prepare state: %w", err) } @@ -351,7 +351,7 @@ func cleanupStateDbDir(args *ReplayArgs, snapshotHandler *SnapshotHandler, outer return err } -func prepareState(args *ReplayArgs, chain *stateChainAdapter, genesis *Genesis) error { +func prepareState(args *ReplayArgs, chain *stateChainAdapter, blockDb blockdb.BlockDB, genesis *Genesis) error { if args.StartBlock == 0 { slog.Info("Applying genesis") // Apply genesis data to the state database. @@ -359,6 +359,9 @@ func prepareState(args *ReplayArgs, chain *stateChainAdapter, genesis *Genesis) return fmt.Errorf("failed to apply genesis data: %w", err) } } + if err := seedBlockHashHistory(blockDb, chain.chainID, args.StartBlock, chain.blockHashHistory); err != nil { + return fmt.Errorf("failed to seed block hash history: %w", err) + } stateRoot, err := chain.state.GetStateRoot().Await().Get() if err != nil { return fmt.Errorf("failed to get state root: %w", err) @@ -367,6 +370,41 @@ func prepareState(args *ReplayArgs, chain *stateChainAdapter, genesis *Genesis) return nil } +func seedBlockHashHistory( + blockDb blockdb.BlockDB, + chainID uint64, + firstBlockToApply uint64, + hashHistory *blockHashHistory, +) error { + if firstBlockToApply == 0 { + return nil + } + var start uint64 = 1 + if firstBlockToApply > blockHashHistoryLen-1 { + start = firstBlockToApply - (blockHashHistoryLen - 1) + } + end := firstBlockToApply + count := uint64(0) + for block, err := range blockDb.GetRange(chainID, start, end) { + if err != nil { + return fmt.Errorf("failed to read block for hash history seeding: %w", err) + } + if block.Number == 0 { + continue + } + hashHistory.SetBlockHash(block.Number-1, common.BytesToHash(block.ParentHash)) + count++ + } + expected := end - start + 1 + if count != expected { + return fmt.Errorf( + "expected %d blocks in [%d, %d] for chain %d, got %d", + expected, start, end, chainID, count, + ) + } + return nil +} + // runReplayLoop processes the blocks from the given iterator, applying them // to the chain and checking the results against the expected values in the // blocks. This is the main business logic of the replay command. @@ -900,12 +938,14 @@ type ReplayLoopContext struct { // --- block hash history tracking --- +const blockHashHistoryLen = 256 + // blockHashHistory keeps track of the last 256 block hashes. This is required // for the BLOCKHASH opcode in the EVM. // It implements the evmcore.DummyChain interface, allowing it to // be used with the EVM state processor to serve historic block hashes. type blockHashHistory struct { - historicHashes [256]common.Hash + historicHashes [blockHashHistoryLen]common.Hash } func (h *blockHashHistory) Clone() *blockHashHistory { @@ -917,11 +957,11 @@ func (h *blockHashHistory) Clone() *blockHashHistory { } func (h *blockHashHistory) GetBlockHash(number uint64) common.Hash { - return h.historicHashes[number%256] + return h.historicHashes[number%blockHashHistoryLen] } func (h *blockHashHistory) SetBlockHash(number uint64, hash common.Hash) { - h.historicHashes[number%256] = hash + h.historicHashes[number%blockHashHistoryLen] = hash } func (h *blockHashHistory) Header(_ common.Hash, number uint64) *evmcore.EvmHeader { diff --git a/go/replay/replay_test.go b/go/replay/replay_test.go index a0155213..b9593b0f 100644 --- a/go/replay/replay_test.go +++ b/go/replay/replay_test.go @@ -1213,6 +1213,143 @@ func TestStateChainAdapter_GetChainConfigAndUpgrades_ReadsFromMetadataStoreForNo } } +// synthetic block hash for tests that encodes the block number in the last 8 +// bytes so hashes are unique across a 256-block window. +func testBlockHash(n uint64) common.Hash { + var h common.Hash + binary.BigEndian.PutUint64(h[24:], n) + return h +} + +func TestSeedBlockHashHistory(t *testing.T) { + // makeParents constructs count blocks starting at startNum with ParentHash + // set to testBlockHash(number-1), matching the invariant that block N's + // ParentHash equals hash(N-1). + makeParents := func(startNum uint64, count int) []*blockdb.Block { + blocks := make([]*blockdb.Block, count) + for i := range blocks { + n := startNum + uint64(i) + blocks[i] = &blockdb.Block{ + Number: n, + ParentHash: testBlockHash(n - 1).Bytes(), + } + } + return blocks + } + // expectedHashes returns the map of block-number → seeded hash for the + // inclusive range [first, last]. + expectedHashes := func(first, last uint64) map[uint64]common.Hash { + m := map[uint64]common.Hash{} + for n := first; n <= last; n++ { + m[n] = testBlockHash(n) + } + return m + } + + const chainID uint64 = 42 + + cases := map[string]struct { + firstBlockToApply uint64 + dbBlocks []*blockdb.Block // nil → GetRange must not be called + wantStart uint64 + wantEnd uint64 + wantHashes map[uint64]common.Hash + wantErr bool + }{ + "NoSeedingWhenStartBlockIsZero": { + firstBlockToApply: 0, + dbBlocks: nil, + wantHashes: map[uint64]common.Hash{}, + }, + "SeedsGenesisHashWhenStartingAtBlockOne": { + firstBlockToApply: 1, + dbBlocks: makeParents(1, 1), + wantStart: 1, + wantEnd: 1, + wantHashes: expectedHashes(0, 0), + }, + "SeedsPartialWindowForLowStartBlock": { + firstBlockToApply: 100, + dbBlocks: makeParents(1, 100), + wantStart: 1, + wantEnd: 100, + wantHashes: expectedHashes(0, 99), + }, + "SeedsPartialWindowFor255thBlock": { + firstBlockToApply: 255, + dbBlocks: makeParents(1, 255), + wantStart: 1, + wantEnd: 255, + wantHashes: expectedHashes(0, 254), + }, + "SeedsFullWindowFor256thBlock": { + firstBlockToApply: 256, + dbBlocks: makeParents(1, 256), + wantStart: 1, + wantEnd: 256, + wantHashes: expectedHashes(0, 255), + }, + "SeedsFullWindowForLaterBlock": { + firstBlockToApply: 1000, + dbBlocks: makeParents(745, 256), + wantStart: 745, + wantEnd: 1000, + wantHashes: expectedHashes(744, 999), + }, + "ErrorsWhenNoBlocksInRange": { + firstBlockToApply: 100, + dbBlocks: []*blockdb.Block{}, + wantStart: 1, + wantEnd: 100, + wantErr: true, + }, + "ErrorsWhenFewerBlocksThanExpected": { + firstBlockToApply: 1000, + dbBlocks: makeParents(745, 100), + wantStart: 745, + wantEnd: 1000, + wantErr: true, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + db := blockdb.NewMockBlockDB(ctrl) + if tc.dbBlocks != nil { + db.EXPECT(). + GetRange(chainID, tc.wantStart, tc.wantEnd). + Return(utils.NewIter(tc.dbBlocks)) + } + history := &blockHashHistory{} + err := seedBlockHashHistory(db, chainID, tc.firstBlockToApply, history) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + for num, expected := range tc.wantHashes { + require.Equal(t, expected, history.GetBlockHash(num)) + } + }) + } +} + +func TestSeedBlockHashHistory_PropagatesBlockDBError(t *testing.T) { + ctrl := gomock.NewController(t) + db := blockdb.NewMockBlockDB(ctrl) + + var errIter iter.Seq2[*blockdb.Block, error] = func(yield func(*blockdb.Block, error) bool) { + yield(nil, fmt.Errorf("boom")) + } + db.EXPECT(). + GetRange(uint64(42), uint64(1), uint64(100)). + Return(errIter) + + history := &blockHashHistory{} + err := seedBlockHashHistory(db, 42, 100, history) + require.ErrorContains(t, err, "boom") +} + func Test_getExpectedStateRoot_ReturnsCorrectStateRoot(t *testing.T) { require := require.New(t)