Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions go/replay/replay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -351,14 +351,17 @@ 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.
if err := chain.state.ApplyGenesis(genesis); err != nil {
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)
}
Comment thread
LorenzSchueler marked this conversation as resolved.
stateRoot, err := chain.state.GetStateRoot().Await().Get()
if err != nil {
return fmt.Errorf("failed to get state root: %w", err)
Expand All @@ -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++
}
Comment thread
LorenzSchueler marked this conversation as resolved.
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,
)
}
Comment thread
Copilot marked this conversation as resolved.
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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
137 changes: 137 additions & 0 deletions go/replay/replay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading