From fa93a66bf8a58742b0f9bfe8291e2dea6a80f9ed Mon Sep 17 00:00:00 2001 From: KyrinCode Date: Mon, 22 Sep 2025 08:36:23 +0800 Subject: [PATCH] add op-replay-seq & op-replay cmd line tools --- op-chain-ops/cmd/op-replay-seq/README.md | 412 +++++++++ op-chain-ops/cmd/op-replay-seq/hybrid_db.go | 155 ++++ op-chain-ops/cmd/op-replay-seq/main.go | 873 ++++++++++++++++++++ op-chain-ops/cmd/op-replay/README.md | 331 ++++++++ op-chain-ops/cmd/op-replay/hybrid_db.go | 155 ++++ op-chain-ops/cmd/op-replay/main.go | 738 +++++++++++++++++ 6 files changed, 2664 insertions(+) create mode 100644 op-chain-ops/cmd/op-replay-seq/README.md create mode 100644 op-chain-ops/cmd/op-replay-seq/hybrid_db.go create mode 100644 op-chain-ops/cmd/op-replay-seq/main.go create mode 100644 op-chain-ops/cmd/op-replay/README.md create mode 100644 op-chain-ops/cmd/op-replay/hybrid_db.go create mode 100644 op-chain-ops/cmd/op-replay/main.go diff --git a/op-chain-ops/cmd/op-replay-seq/README.md b/op-chain-ops/cmd/op-replay-seq/README.md new file mode 100644 index 0000000000000..a9971d44afc08 --- /dev/null +++ b/op-chain-ops/cmd/op-replay-seq/README.md @@ -0,0 +1,412 @@ +# op-replay-seq + +`op-replay-seq` is a tool that uses sequencer logic to replay blocks, verifying block execution consistency by reusing the block building logic from the op-geth miner package. + +## Purpose + +The main objectives of this tool are: + +1. **Verify Sequencer Logic Upgrades**: When upgrading sequencer logic, use this tool to verify that new sequencer logic doesn't affect state when replaying old blocks, preventing forks. + +2. **State Consistency Verification**: Ensure block execution consistency by replaying blocks and comparing state roots. + +3. **Debugging and Testing**: Help developers debug block execution issues and verify state transition correctness. + +## How It Works + +Unlike `op-replay`, `op-replay-seq` completely reuses sequencer's block building logic: + +### Method Correspondence + +The methods in `op-replay-seq` correspond one-to-one with miner methods: + +| op-replay-seq Method | Miner Method | Purpose | +|----------------------|---------------|---------| +| `processBlockWithSequencerLogic()` | `buildPayload` | Block building entry point (skipped during replay) | +| `extractGenerateParams()` | - | Parameter extraction (replay-specific) | +| `ReplayMiner.generateWork()` | `Miner.generateWork()` | Main block generation logic | +| `ReplayMiner.prepareWork()` | `Miner.prepareWork()` | Prepare block building environment | +| `ReplayMiner.makeEnv()` | `Miner.makeEnv()` | Create execution environment | +| `ReplayMiner.commitTransaction()` | `Miner.commitTransaction()` | Commit individual transactions | +| `ReplayMiner.applyTransaction()` | `Miner.applyTransaction()` | Apply transactions to state | + +### Call Chain + +The tool follows the same call chain as the miner: + +#### Miner Complete Call Chain +``` +forkchoiceUpdated() -> buildPayload() -> generateWork() -> prepareWork() -> makeEnv() -> commitTransaction() -> applyTransaction() +``` + +#### op-replay-seq Call Chain +``` +processBlockWithSequencerLogic() -> extractGenerateParams() -> generateWork() -> prepareWork() -> makeEnv() -> commitTransaction() -> applyTransaction() +``` + +**Note**: +- `op-replay-seq` starts from `processBlockWithSequencerLogic`, skipping the miner's API layer and `buildPayload` stage +- It directly extracts parameters from `extractGenerateParams`, then enters the same `generateWork` call chain as the miner +- This ensures that the replay process uses identical core logic while adapting to replay-specific requirements + +### Key Features + +1. **Parameter Extraction**: Extract `generateParams` from existing blocks, including timestamp, transaction list, gas limit, etc. +2. **State Reuse**: Use the same state processing logic as the miner +3. **Transaction Processing**: Reuse miner's transaction validation and application logic +4. **Consensus Logic**: Use the same consensus engine and FinalizeAndAssemble logic +5. **Object Reuse**: `ReplayMiner` and `remoteChainCtx` are created outside the block processing loop and reused for each block, improving performance + +## Code Structure + +### Core Data Structures + +#### generateParams +- **Purpose**: Wraps various settings for block generation +- **Relationship with Miner**: Completely mirrors the miner's `generateParams` structure +- **Replay-Specific**: Extracted from existing blocks through `extractGenerateParams()` + +#### environment +- **Purpose**: Contains all information needed for block generation +- **Relationship with Miner**: Completely mirrors the miner's `environment` structure +- **Replay-Specific**: State is passed in externally, not fetched from the chain + +#### ReplayMiner +- **Purpose**: Simulates the miner structure for replaying blocks +- **Relationship with Miner**: Provides the same method interface as the miner +- **Replay-Specific**: State management adapted for replay scenarios + +### Method Implementation Details + +#### 1. generateWork Method + +**Correspondence**: `ReplayMiner.generateWork()` ↔ `Miner.generateWork()` + +**Reused Logic**: +- Calls `prepareWork` to prepare environment +- Sets up gas pool +- Processes forced-included transactions +- Collects consensus layer requests (EIP-6110, EIP-7002, EIP-7251) +- Calls `FinalizeAndAssemble` + +**Differences from Miner**: +- **Skip `fillTransactions`**: Transactions are already determined during replay, no need to fill from transaction pool +- **Skip Interrupt Checks**: Replay doesn't need interruption mechanism +- **Reconstruct Block Header**: Reconstruct block header based on existing block information, then call FinalizeAndAssemble +- **Witness Export**: Records witness creation status at the end of the method for subsequent export + +**Reason**: The purpose of replay tools is to verify execution of existing blocks, not to dynamically build transaction lists or handle interruptions. Witness export logic is now in the correct position, maintaining consistency with miner logic structure. + +#### 2. prepareWork Method + +**Correspondence**: `ReplayMiner.prepareWork()` ↔ `Miner.prepareWork()` + +**Reused Logic**: +- Timestamp validation +- Block header construction +- EIP-1559 base fee processing +- EIP-4844 blob gas processing +- Consensus engine preparation +- Calls `makeEnv` + +**Differences from Miner**: +- **Use External Parent Block**: Don't fetch parent block from chain, use the passed-in one +- **Use Existing Block's Extra**: Don't calculate Extra data, directly use the one in the block +- **Use Existing Block's BaseFee**: Don't calculate BaseFee, directly use the one in the block +- **Simplified Gas Limit Handling**: Directly use parent block's gas limit + +**Reason**: During replay, we already have complete block information and can use it directly without recalculation. This ensures replay results are completely consistent with the original block. + +#### 3. makeEnv Method + +**Correspondence**: `ReplayMiner.makeEnv()` ↔ `Miner.makeEnv()` + +**Reused Logic**: +- Witness data collection +- EVM environment creation +- State database configuration +- Prefetcher management + +**Differences from Miner**: +- **Use External State**: Don't fetch state from chain, use the passed-in state +- **Simplified State Access**: Don't need complex historical state fetching logic +- **Witness Management**: Unified management of witness creation and prefetcher startup, avoiding duplicate logic + +**Reason**: During replay, state is managed externally, not needing the miner's complex state fetching mechanism. Now witness creation and prefetcher management are all in `makeEnv`, completely consistent with miner logic. + +#### 4. commitTransaction Method + +**Correspondence**: `ReplayMiner.commitTransaction()` ↔ `Miner.commitTransaction()` + +**Reused Logic**: +- Conditional transaction checks +- Calls `applyTransaction` +- Updates environment state + +**Differences from Miner**: +- **Skip Interop Transaction Checks**: Don't need to check interop transactions during replay +- **Simplified Conditional Transaction Handling**: Retain core conditional check logic + +**Reason**: During replay, miner's interop transaction validation isn't needed, but conditional transaction validation needs to be retained to ensure correctness. + +#### 5. applyTransaction Method + +**Correspondence**: `ReplayMiner.applyTransaction()` ↔ `Miner.applyTransaction()` + +**Reused Logic**: +- State snapshot management +- Calls `core.ApplyTransaction` +- Error rollback handling + +**Differences from Miner**: +- **Completely Identical**: No differences, logic is exactly the same + +**Reason**: This is the lowest-level transaction application logic, which should be consistent between miner and replay. + +## Refactored Logic Structure + +### Witness and Prefetcher Management Optimization + +**Problems Before Refactoring**: +- **Logic Duplication**: Both `processBlockWithSequencerLogic` and `makeEnv` created witness and started prefetcher +- **Unclear Responsibilities**: Witness export logic was in the wrong position, inconsistent with miner logic structure + +**Structure After Refactoring**: +- **Unified Management**: Witness creation and prefetcher startup are all in the `makeEnv` method, completely consistent with miner logic +- **Correct Export**: Witness export logic is in `processBlockWithSequencerLogic`, processed after calling `generateWork` +- **Avoid Duplication**: Eliminated duplicate logic for witness creation and prefetcher startup + +**Refactoring Advantages**: +1. **Logic Consistency**: Completely consistent with miner's `makeEnv` method +2. **Clear Responsibilities**: Each method's responsibilities are more clear +3. **Maintenance Friendly**: When miner logic updates, easy to identify code that needs synchronization +4. **Performance Optimization**: Avoid duplicate witness creation and prefetcher startup + +## Replay-Specific Methods + +### extractGenerateParams + +**Purpose**: Extract miner-required parameters from existing blocks + +**Relationship with Miner**: No corresponding method in miner, this is replay tool-specific + +**Implementation Details**: +```go +func extractGenerateParams(block *types.Block) *generateParams { + return &generateParams{ + timestamp: block.Time(), + forceTime: true, // Timestamp is fixed during replay + parentHash: block.ParentHash(), + coinbase: block.Coinbase(), + random: block.MixDigest(), + withdrawals: block.Withdrawals(), + beaconRoot: block.BeaconRoot(), + noTxs: true, // Transactions are forced-included during replay + txs: block.Transactions(), + gasLimit: func() *uint64 { limit := block.GasLimit(); return &limit }(), + // ... other fields + } +} +``` + +**Note**: All parameters are extracted from the current block to be processed, no parent block information needed + +## Usage + +### Basic Usage + +```bash +# Replay blocks 1000-1100 +op-replay-seq --rpc --start 1000 --end 1100 + +# Replay blocks with trace output +op-replay-seq --rpc --start 1000 --end 1100 --out ./trace_output +``` + +### Parameter Description + +- `--rpc`: RPC endpoint address (required) +- `--start`: Start block number (required) +- `--end`: End block number (required) +- `--out`: Output directory path (optional, for saving trace files) + +### Environment Variables + +You can set parameters via environment variables: + +```bash +export OP_REPLAY_SEQ_RPC="http://localhost:8545" +export OP_REPLAY_SEQ_START=1000 +export OP_REPLAY_SEQ_END=1100 +export OP_REPLAY_SEQ_OUT="./trace_output" + +op-replay-seq +``` + +## Output + +### Console Output + +The tool displays processing progress and results in the console: + +``` +INFO Starting block range replay using sequencer logic start=1000 end=1100 +INFO Initialized state from parent block parent_number=999 parent_root=0x1234... +INFO Processing block number=1000 +INFO Block processed successfully block_number=1000 state_root=0x5678... gas_used=15000000 tx_count=150 +... +INFO Replay completed total_blocks=101 success_blocks=101 mismatch_blocks=0 success_rate=100.00% +``` + +### Trace Files (Optional) + +If `--out` parameter is specified, the tool generates: + +- `block__trace.txt`: Transaction execution traces +- `block__witness.json`: State access witness data + +## Differences from op-replay + +| Feature | op-replay | op-replay-seq | +|---------|-----------|---------------| +| **Execution Logic** | Verifier node logic | Sequencer node logic (reusing miner) | +| **Transaction Source** | From block parsing | From block parsing (reusing miner logic) | +| **State Processing** | Direct state transition | Reusing miner's state processing logic | +| **Method Correspondence** | Independent implementation | One-to-one correspondence with miner methods | +| **Purpose** | Verify block execution | Verify sequencer logic consistency | + +## Code Reuse Analysis + +### Completely Reused Logic + +The following logic is completely reused from the miner: + +1. **Block Header Preparation** (`prepareWork`) + - Timestamp validation + - EIP-1559 base fee calculation + - EIP-4844 blob gas processing + - Consensus engine preparation + +2. **Environment Creation** (`makeEnv`) + - EVM environment setup + - State database configuration + - Witness data collection + +3. **Transaction Processing** (`commitTransaction` + `applyTransaction`) + - Conditional transaction checks + - Transaction application and rollback + - Gas pool management + +4. **Consensus Layer Requests** (`generateWork`) + - EIP-6110 deposits + - EIP-7002 withdrawals + - EIP-7251 consolidations + +### Replay-Specific Logic + +The following logic is specific to the replay tool: + +1. **Parameter Extraction** (`extractGenerateParams`) + - Extract build parameters from existing blocks + - Skip transaction pool logic + +2. **State Management** + - Use externally passed-in state + - Skip miner's state fetching logic + +3. **Output Processing** + - Trace file generation + - Witness data export + +4. **Skipped Logic** + - Transaction pool filling, interrupt checks, etc. + +5. **Object Reuse Optimization** + - `ReplayMiner` created outside the loop, reused for each block + - `remoteChainCtx` created outside the loop, updated for each block + - Avoid duplicate object creation, improve performance + - Unified state management: `currentState` and `hdr` are both updated in the main loop, ensuring state consistency + - Avoid duplicate parent block information fetching in functions, improve efficiency + - Optimized state update timing: update state uniformly after processing blocks, prepare for the next block + +### Skipped Miner Logic + +1. **API Layer** (`forkchoiceUpdated`) + - Don't need to handle external API calls during replay + - Directly process existing blocks + +2. **Block Building Entry** (`buildPayload`) + - Don't need to build new blocks during replay + - Directly use existing blocks for replay verification + +## Use Cases + +### 1. Sequencer Upgrade Verification + +After upgrading sequencer logic, use this tool to replay historical blocks: + +```bash +# Replay recent 1000 blocks to verify upgrade +op-replay-seq --rpc --start --end +``` + +### 2. Fork Detection + +If state root mismatches are found during replay, it indicates potential fork risk: + +``` +ERROR State root mismatch block_number=1050 expected_root=0x1234... computed_root=0x5678... +``` + +### 3. Performance Testing + +Test the performance and stability of new sequencer logic by replaying large numbers of blocks. + +## Notes + +1. **RPC Performance**: The tool needs to frequently call RPC interfaces, ensure the RPC endpoint has sufficient performance +2. **Memory Usage**: Pay attention to memory usage when replaying large numbers of blocks +3. **Network Stability**: Ensure stable network connection with the RPC endpoint +4. **Block Range**: Start with small ranges for testing, then gradually expand + +## Troubleshooting + +### Common Errors + +1. **RPC Connection Failure**: Check RPC endpoint address and network connectivity +2. **Block Fetch Failure**: Confirm block number range is valid and RPC endpoint has corresponding data +3. **State Root Mismatch**: May indicate sequencer logic issues, need further investigation + +### Debugging Tips + +1. Use `--out` parameter to generate detailed trace files +2. Start with small block ranges for testing +3. Check log output for detailed error information +4. Verify chain configuration and hardfork settings + +## Maintenance Guide + +### When Miner Logic Updates + +1. **Check Method Signatures**: Confirm that corresponding method signatures haven't changed +2. **Check Reused Logic**: Confirm that reused logic sections don't need updates +3. **Check Difference Logic**: Confirm that replay-specific logic doesn't need adjustment +4. **Test Validation**: Run tests to ensure functionality works correctly + +### Adding New Features + +1. **Determine Reusability**: Judge whether new features should exist in both miner and replay +2. **Method Correspondence**: If reusing, ensure method names and logic correspond +3. **Documentation Updates**: Update method correspondence table + +## Advantages Summary + +1. **Code Consistency**: Maintains consistency with miner logic +2. **Maintenance Convenience**: When miner updates, easy to identify code that needs synchronization +3. **Complete Functionality**: Reuses all core miner logic +4. **Debugging Friendly**: Can be compared directly with miner code for debugging +5. **Clear Structure**: Clearly distinguishes reused code from replay-specific code + +## Contributing + +Welcome to submit Issues and Pull Requests to improve this tool. diff --git a/op-chain-ops/cmd/op-replay-seq/hybrid_db.go b/op-chain-ops/cmd/op-replay-seq/hybrid_db.go new file mode 100644 index 0000000000000..761735d8b743c --- /dev/null +++ b/op-chain-ops/cmd/op-replay-seq/hybrid_db.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/ethdb/remotedb" + "github.com/ethereum/go-ethereum/rpc" +) + +// ============================================================================= +// Hybrid Database Implementation +// ============================================================================= + +// hybridRemoteDB combines remotedb (read-only) with memdb (write cache) +// This solves the NewBatch issue by writing to memdb while reading from both sources +type hybridRemoteDB struct { + ethdb.Database // Embed the interface to inherit all methods + memDB ethdb.Database // Write cache for all modifications + mu sync.RWMutex // Protect concurrent access +} + +// NewHybridRemoteDB creates a new hybrid database that combines remotedb and memdb +func NewHybridRemoteDB(cl *rpc.Client) *hybridRemoteDB { + remoteDB := remotedb.New(cl) + return &hybridRemoteDB{ + Database: remoteDB, // Embed the remote database + memDB: rawdb.NewDatabase(memorydb.New()), + } +} + +// NewBatch returns a batch that writes to memdb +func (h *hybridRemoteDB) NewBatch() ethdb.Batch { + return h.memDB.NewBatch() +} + +// Get implements layered read strategy: memdb first, then remotedb +func (h *hybridRemoteDB) Get(key []byte) ([]byte, error) { + h.mu.RLock() + defer h.mu.RUnlock() + + // First try memdb (most recent data) + if val, err := h.memDB.Get(key); err == nil { + return val, nil + } + + // If not in memdb, try remotedb (initial state) + return h.Database.Get(key) +} + +// Has implements layered read strategy +func (h *hybridRemoteDB) Has(key []byte) (bool, error) { + h.mu.RLock() + defer h.mu.RUnlock() + + // First check memdb + if has, err := h.memDB.Has(key); err == nil && has { + return true, nil + } + + // Then check remotedb + return h.Database.Has(key) +} + +// Put writes to memdb only +func (h *hybridRemoteDB) Put(key, value []byte) error { + h.mu.Lock() + defer h.mu.Unlock() + return h.memDB.Put(key, value) +} + +// Delete removes from memdb only +func (h *hybridRemoteDB) Delete(key []byte) error { + h.mu.Lock() + defer h.mu.Unlock() + return h.memDB.Delete(key) +} + +// Close closes both databases +func (h *hybridRemoteDB) Close() error { + h.mu.Lock() + defer h.mu.Unlock() + + // Close memdb first + if err := h.memDB.Close(); err != nil { + return err + } + + // Then close remotedb + return h.Database.Close() +} + +// For all other methods, delegate to remoteDB (read-only operations) +func (h *hybridRemoteDB) Stat() (string, error) { return h.Database.Stat() } + +func (h *hybridRemoteDB) Compact(start []byte, limit []byte) error { + return h.Database.Compact(start, limit) +} + +func (h *hybridRemoteDB) NewIterator(prefix []byte, start []byte) ethdb.Iterator { + return h.Database.NewIterator(prefix, start) +} + +func (h *hybridRemoteDB) Ancient(kind string, number uint64) ([]byte, error) { + return h.Database.Ancient(kind, number) +} + +func (h *hybridRemoteDB) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) { + return h.Database.AncientRange(kind, start, count, maxBytes) +} + +func (h *hybridRemoteDB) Ancients() (uint64, error) { return h.Database.Ancients() } + +func (h *hybridRemoteDB) Tail() (uint64, error) { return h.Database.Tail() } + +func (h *hybridRemoteDB) AncientSize(kind string) (uint64, error) { + return h.Database.AncientSize(kind) +} + +func (h *hybridRemoteDB) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) { + // 使用类型断言来调用正确的方法 + if reader, ok := h.Database.(interface { + ReadAncients(func(ethdb.AncientReaderOp) error) error + }); ok { + return reader.ReadAncients(fn) + } + // 如果底层数据库不支持,返回错误 + return fmt.Errorf("underlying database does not support ReadAncients") +} + +func (h *hybridRemoteDB) Sync() error { return h.Database.Sync() } + +func (h *hybridRemoteDB) AncientDatadir() (string, error) { return h.Database.AncientDatadir() } + +func (h *hybridRemoteDB) DeleteRange(start []byte, limit []byte) error { + return h.Database.DeleteRange(start, limit) +} + +func (h *hybridRemoteDB) HasAncient(kind string, number uint64) (bool, error) { + return h.Database.HasAncient(kind, number) +} + +func (h *hybridRemoteDB) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (int64, error) { + // 检查底层数据库是否支持 ModifyAncients + if modifier, ok := h.Database.(interface { + ModifyAncients(func(ethdb.AncientWriteOp) error) (int64, error) + }); ok { + return modifier.ModifyAncients(fn) + } + // 如果不支持,返回默认值 + return 0, fmt.Errorf("underlying database does not support ModifyAncients") +} diff --git a/op-chain-ops/cmd/op-replay-seq/main.go b/op-chain-ops/cmd/op-replay-seq/main.go new file mode 100644 index 0000000000000..1450b4a01b17e --- /dev/null +++ b/op-chain-ops/cmd/op-replay-seq/main.go @@ -0,0 +1,873 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "os" + "time" + + "github.com/urfave/cli/v2" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/core" + gstate "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + logger2 "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/triedb" + + op_service "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/cliapp" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum-optimism/optimism/op-service/retry" +) + +// ============================================================================= +// CLI Flags and Configuration +// ============================================================================= + +var EnvPrefix = "OP_REPLAY_SEQ" + +var ( + RPCFlag = &cli.StringFlag{ + Name: "rpc", + Usage: "RPC endpoint to fetch data from", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "RPC"), + Required: true, + } + StartFlag = &cli.IntFlag{ + Name: "start", + Usage: "Start block number", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "START"), + Required: true, + } + EndFlag = &cli.IntFlag{ + Name: "end", + Usage: "End block number", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "END"), + Required: true, + } + OutPathFlag = &cli.PathFlag{ + Name: "out", + Usage: "Path to directory to write trace data files (optional, if not specified no files will be written)", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "OUT"), + Value: "", + } +) + +// ============================================================================= +// Core Data Structures (mimicking miner structures) +// ============================================================================= + +// generateParams wraps various settings for generating sealing task. +// This structure mirrors the miner's generateParams exactly. +type generateParams struct { + timestamp uint64 // The timestamp for sealing task + forceTime bool // Flag whether the given timestamp is immutable or not + parentHash common.Hash // Parent block hash, empty means the latest chain head + coinbase common.Address // The fee recipient address for including transaction + random common.Hash // The randomness generated by beacon chain, empty before the merge + withdrawals types.Withdrawals // List of withdrawals to include in block (shanghai field) + beaconRoot *common.Hash // The beacon root (cancun field). + noTxs bool // Flag whether an empty block without any transaction is expected + + txs types.Transactions // Deposit transactions to include at the start of the block + gasLimit *uint64 // Optional gas limit override + eip1559Params []byte // Optional EIP-1559 parameters + interrupt *int32 // Optional interruption signal to pass down to worker.generateWork + isUpdate bool // Optional flag indicating that this is building a discardable update + + rpcCtx context.Context // context to control block-building RPC work. No RPC allowed if nil. +} + +// environment contains all the information needed for block generation. +// This structure mirrors the miner's environment exactly. +type environment struct { + signer types.Signer + state *gstate.StateDB // apply state changes here + tcount int // tx count in cycle + gasPool *core.GasPool // available gas used to pack transactions + coinbase common.Address + evm *vm.EVM + + header *types.Header + txs []*types.Transaction + receipts []*types.Receipt + sidecars []*types.BlobTxSidecar + blobs int + + witness *stateless.Witness + + noTxs bool // true if we are reproducing a block, and do not have to check interop txs + rpcCtx context.Context // context to control block-building RPC work. No RPC allowed if nil. +} + +// newPayloadResult is the result of payload generation. +// This structure mirrors the miner's newPayloadResult exactly. +type newPayloadResult struct { + err error + block *types.Block + fees *big.Int + sidecars []*types.BlobTxSidecar + stateDB *gstate.StateDB + receipts []*types.Receipt + requests [][]byte + witness *stateless.Witness +} + +// ReplayMiner simulates the miner structure for replay purposes. +// This allows us to reuse miner logic while adapting it for replay scenarios. +type ReplayMiner struct { + chainConfig *params.ChainConfig + chain *remoteChainCtx + logger log.Logger +} + +// remoteChainCtx provides access to block-headers, for usage by the state-transition, +// such as basefee computation (based on prior block) and EVM block-hash opcode. +type remoteChainCtx struct { + consensusEng consensus.Engine + hdr *types.Header + cfg *params.ChainConfig + cl *ethclient.Client + logger log.Logger + currentState *gstate.StateDB // Added to support state access in replay mode +} + +// ============================================================================= +// Error Constants +// ============================================================================= + +var ( + errTxConditionalInvalid = errors.New("transaction conditional invalid") +) + +// ============================================================================= +// Main Application Entry Point +// ============================================================================= + +func main() { + flags := []cli.Flag{ + RPCFlag, StartFlag, EndFlag, OutPathFlag, + } + flags = append(flags, oplog.CLIFlags(EnvPrefix)...) + + app := cli.NewApp() + app.Name = "op-replay-seq" + app.Usage = "Replay a range of blocks using sequencer logic." + app.Description = "Replay a range of blocks using sequencer logic and verify state roots." + app.Flags = cliapp.ProtectFlags(flags) + app.Action = mainAction + app.Writer = os.Stdout + app.ErrWriter = os.Stderr + err := app.Run(os.Args) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Application failed: %v", err) + os.Exit(1) + } +} + +func mainAction(c *cli.Context) error { + ctx := ctxinterrupt.WithCancelOnInterrupt(c.Context) + logCfg := oplog.ReadCLIConfig(c) + logger := oplog.NewLogger(c.App.Writer, logCfg) + + rpcEndpoint := c.String(RPCFlag.Name) + start := c.Int(StartFlag.Name) + end := c.Int(EndFlag.Name) + outDir := c.Path(OutPathFlag.Name) + + if start > end { + return fmt.Errorf("start block (%d) must be less than or equal to end block (%d)", start, end) + } + + // Create output directory if it doesn't exist + if outDir != "" { + if err := os.MkdirAll(outDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + } + + cl, err := rpc.DialContext(ctx, rpcEndpoint) + if err != nil { + return fmt.Errorf("failed to dial RPC: %w", err) + } + defer cl.Close() + + ethCl := ethclient.NewClient(cl) + db := NewHybridRemoteDB(cl) + + var config *params.ChainConfig + if err := cl.CallContext(ctx, &config, "debug_chainConfig"); err != nil { + return fmt.Errorf("failed to fetch chain config: %w", err) + } + + // Print chain configuration details + logger.Info("Chain configuration loaded", + "chain_id", config.ChainID, + "homestead_block", config.HomesteadBlock, + "dao_fork_block", config.DAOForkBlock, + "dao_fork_support", config.DAOForkSupport, + "eip150_block", config.EIP150Block, + "eip155_block", config.EIP155Block, + "eip158_block", config.EIP158Block, + "byzantium_block", config.ByzantiumBlock, + "constantinople_block", config.ConstantinopleBlock, + "petersburg_block", config.PetersburgBlock, + "istanbul_block", config.IstanbulBlock, + "muir_glacier_block", config.MuirGlacierBlock, + "berlin_block", config.BerlinBlock, + "london_block", config.LondonBlock, + "arrow_glacier_block", config.ArrowGlacierBlock, + "gray_glacier_block", config.GrayGlacierBlock, + "shanghai_time", config.ShanghaiTime, + "cancun_time", config.CancunTime, + "prague_time", config.PragueTime, + "terminal_total_difficulty", config.TerminalTotalDifficulty, + "optimism_config", config.Optimism != nil, + "clique_config", config.Clique != nil, + ) + + // Print detailed Optimism configuration if available + if config.Optimism != nil { + logger.Info("Optimism configuration", + "eip1559_elasticity", config.Optimism.EIP1559Elasticity, + "eip1559_denominator", config.Optimism.EIP1559Denominator, + "eip1559_denominator_canyon", config.Optimism.EIP1559DenominatorCanyon, + ) + } + + // Print detailed Clique configuration if available + if config.Clique != nil { + logger.Info("Clique configuration", + "epoch", config.Clique.Epoch, + "period", config.Clique.Period, + ) + } + + // Print additional hardfork timestamps if available + if config.RegolithTime != nil { + logger.Info("Regolith hardfork", "time", *config.RegolithTime) + } + if config.CanyonTime != nil { + logger.Info("Canyon hardfork", "time", *config.CanyonTime) + } + if config.EcotoneTime != nil { + logger.Info("Ecotone hardfork", "time", *config.EcotoneTime) + } + if config.FjordTime != nil { + logger.Info("Fjord hardfork", "time", *config.FjordTime) + } + if config.InteropTime != nil { + logger.Info("Interop hardfork", "time", *config.InteropTime) + } + + logger.Info("Starting block range replay using sequencer logic", "start", start, "end", end) + + // Get parent block of the start block + parentBlock, err := ethCl.HeaderByNumber(ctx, big.NewInt(int64(start-1))) + if err != nil { + return fmt.Errorf("failed to fetch parent block %d: %w", start-1, err) + } + + stateDB := gstate.NewDatabase(triedb.NewDatabase(db, &triedb.Config{ + Preimages: true, + }), nil) + + currentState, err := gstate.New(parentBlock.Root, stateDB) + if err != nil { + return fmt.Errorf("failed to create initial state from block %d: %w", start-1, err) + } + logger.Info("Initialized state from parent block", "parent_number", start-1, "parent_root", parentBlock.Root) + + // Create ReplayMiner and remoteChainCtx once, outside the block processing loop + // This matches the miner pattern where these objects are created once and reused + consensusEng := beacon.New(&beacon.OpLegacy{}) + chCtx := &remoteChainCtx{ + consensusEng: consensusEng, + hdr: parentBlock, // Will be updated for each block + cfg: config, + cl: ethCl, + logger: logger, + currentState: currentState, // Will be updated for each block + } + replayMiner := NewReplayMiner(config, chCtx, logger) + + // Stats tracking + var ( + totalBlocks = 0 + successBlocks = 0 + mismatchBlocks = 0 + prevBlockHash common.Hash // Track previous block hash for validation + ) + + // Process each block in the range + for i := start; i <= end; i++ { + logger.Info("Processing block", "number", i) + + // Fetch the block + block, err := ethCl.BlockByNumber(ctx, big.NewInt(int64(i))) + if err != nil { + return fmt.Errorf("failed to fetch block %d: %w", i, err) + } + + // Basic block validation + if block.NumberU64() != uint64(i) { + return fmt.Errorf("block number mismatch: expected %d, got %d", i, block.NumberU64()) + } + + // Verify parent block connection (except for the first block) + if i > start { + expectedParentHash := prevBlockHash + if block.ParentHash() != expectedParentHash { + return fmt.Errorf("parent hash mismatch at block %d: expected %s, got %s", + i, expectedParentHash, block.ParentHash()) + } + } + + // Create output file for this block's trace + var outW io.Writer = io.Discard // Default to discard if no output directory + if outDir != "" { + outPath := fmt.Sprintf("%s/block_%d_trace.txt", outDir, i) + file, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to create trace file for block %d: %w", i, err) + } + outW = file + } + + // Process the block using sequencer logic + result, newStateRoot, err := processBlockWithSequencerLogic(logger, config, chCtx, replayMiner, block, outW, outDir) + + // Close the file immediately after processing the block + if outDir != "" { + if file, ok := outW.(*os.File); ok { + file.Close() + } + } + + if err != nil { + logger.Error("Failed to process block", "number", i, "error", err) + return fmt.Errorf("failed to process block %d: %w", i, err) + } + + totalBlocks++ + + // Compare state roots + expectedRoot := block.Root() + if newStateRoot != expectedRoot { + mismatchBlocks++ + logger.Error("State root mismatch", + "block_number", i, + "expected_root", expectedRoot, + "computed_root", newStateRoot, + "gas_used", result.GasUsed) + } else { + successBlocks++ + logger.Info("Block processed successfully", + "block_number", i, + "state_root", newStateRoot, + "gas_used", result.GasUsed, + "tx_count", len(block.Transactions())) + } + + // Update current state to the new state for next block + // Note: We need to create a new StateDB instance because the current one is already committed + currentState, err = gstate.New(newStateRoot, stateDB) + if err != nil { + return fmt.Errorf("failed to create state for next block: %w", err) + } + + // Update the remoteChainCtx for the next block processing + chCtx.currentState = currentState // Update state for next block + chCtx.hdr = block.Header() // Update header to current block (will be parent for next block) + + // Update previous block hash for next iteration + prevBlockHash = block.Hash() + } + + // Print summary + logger.Info("Replay completed", + "total_blocks", totalBlocks, + "success_blocks", successBlocks, + "mismatch_blocks", mismatchBlocks, + "success_rate", fmt.Sprintf("%.2f%%", float64(successBlocks)/float64(totalBlocks)*100)) + + if mismatchBlocks > 0 { + return fmt.Errorf("state root mismatches detected in %d out of %d blocks", mismatchBlocks, totalBlocks) + } + + return nil +} + +// ============================================================================= +// Block Processing Logic (Core Replay Functionality) +// ============================================================================= + +// processBlockWithSequencerLogic processes a single block using sequencer logic and returns the new state root +func processBlockWithSequencerLogic(logger log.Logger, config *params.ChainConfig, chCtx *remoteChainCtx, replayMiner *ReplayMiner, block *types.Block, + outW io.Writer, outDir string) (*core.ProcessResult, common.Hash, error) { + + header := block.Header() + + vmCfg := vm.Config{Tracer: nil} + + // Set up tracing only if output is requested + if outW != nil && outW != io.Discard { + vmCfg.Tracer = logger2.NewJSONLogger(&logger2.Config{ + EnableMemory: false, + DisableStack: false, + DisableStorage: false, + EnableReturnData: false, + Limit: 0, + Overrides: nil, + }, outW) + } + + // Witness creation and prefetcher management is now handled in makeEnv method + // to align with miner logic and avoid duplication + + params := extractGenerateParams(block) + + // Use miner's call chain: generateWork -> prepareWork -> makeEnv -> commitTransaction -> applyTransaction + // Pass vmCfg to enable tracing + result := replayMiner.generateWork(params, outDir != "", block, vmCfg) + if result.err != nil { + return nil, common.Hash{}, fmt.Errorf("failed to generate work: %w", result.err) + } + + // Export witness data for debugging if output directory is specified (replay-specific logic) + // This is now handled after generateWork to align with miner logic structure + if outDir != "" && result.witness != nil { + witnessDump := result.witness.ToExecutionWitness() + out, err := json.MarshalIndent(witnessDump, "", " ") + if err != nil { + logger.Error("failed to encode witness", "err", err) + } else { + witnessPath := fmt.Sprintf("%s/block_%d_witness.json", outDir, block.NumberU64()) + if err := os.WriteFile(witnessPath, out, 0644); err != nil { + logger.Debug("Failed to write witness", "err", err, "path", witnessPath) + } else { + logger.Info("Witness data exported", "path", witnessPath) + } + } + } + + // Commit the state changes and get the new state root + newStateRoot, err := chCtx.currentState.Commit(uint64(block.NumberU64()), config.IsEIP158(header.Number), config.IsCancun(header.Number, header.Time)) + if err != nil { + return nil, common.Hash{}, fmt.Errorf("failed to commit state: %w", err) + } + + return &core.ProcessResult{ + Receipts: result.receipts, + Requests: result.requests, + Logs: func() []*types.Log { + var allLogs []*types.Log + for _, r := range result.receipts { + allLogs = append(allLogs, r.Logs...) + } + return allLogs + }(), + GasUsed: result.block.GasUsed(), + }, newStateRoot, nil +} + +// ============================================================================= +// ReplayMiner Implementation (Mimicking Miner Logic) +// ============================================================================= + +// NewReplayMiner creates a new replay miner instance +func NewReplayMiner(config *params.ChainConfig, chain *remoteChainCtx, logger log.Logger) *ReplayMiner { + return &ReplayMiner{ + chainConfig: config, + chain: chain, + logger: logger, + } +} + +// extractGenerateParams extracts miner parameters from an existing block +// This is replay-specific logic that miner doesn't have +func extractGenerateParams(block *types.Block) *generateParams { + return &generateParams{ + timestamp: block.Time(), + forceTime: true, // Timestamp is fixed in replay mode + parentHash: block.ParentHash(), + coinbase: block.Coinbase(), + random: block.MixDigest(), + withdrawals: block.Withdrawals(), + beaconRoot: block.BeaconRoot(), + noTxs: true, // Transactions are forced-included in replay mode + txs: block.Transactions(), + gasLimit: func() *uint64 { limit := block.GasLimit(); return &limit }(), + eip1559Params: nil, + interrupt: nil, + isUpdate: false, + rpcCtx: nil, + } +} + +// generateWork mimics miner's generateWork method +// DIFFERENCES FROM MINER: +// - Skips fillTransactions logic (transactions are already determined) +// - Skips interrupt checks (no interruption needed in replay) +// - Uses existing block for FinalizeAndAssemble +// - Receives vmCfg for tracing support +func (rm *ReplayMiner) generateWork(params *generateParams, witness bool, block *types.Block, vmCfg vm.Config) *newPayloadResult { + work, err := rm.prepareWork(params, witness, block, vmCfg) + if err != nil { + return &newPayloadResult{err: err} + } + + if work.gasPool == nil { + gasLimit := work.header.GasLimit + work.gasPool = new(core.GasPool).AddGas(gasLimit) + } + + misc.EnsureCreate2Deployer(rm.chainConfig, work.header.Time, work.state) + + // Process forced transactions (from block) + for i, tx := range params.txs { + rm.logger.Info("Processing tx", "i", i, "hash", tx.Hash().Hex()) + from, _ := types.Sender(work.signer, tx) + work.state.SetTxContext(tx.Hash(), work.tcount) + err = rm.commitTransaction(work, tx) + if err != nil { + return &newPayloadResult{err: fmt.Errorf("failed to force-include tx: %s type: %d sender: %s nonce: %d, err: %w", tx.Hash(), tx.Type(), from, tx.Nonce(), err)} + } + } + + // SKIPPED: fillTransactions logic (not needed in replay mode) + // SKIPPED: interrupt checks (no interruption needed in replay mode) + + body := types.Body{Transactions: work.txs, Withdrawals: params.withdrawals} + allLogs := make([]*types.Log, 0) + for _, r := range work.receipts { + allLogs = append(allLogs, r.Logs...) + } + + isIsthmus := rm.chainConfig.IsIsthmus(work.header.Time) + + // Collect consensus-layer requests if Prague is enabled + var requests [][]byte + if rm.chainConfig.IsPrague(work.header.Number, work.header.Time) && !isIsthmus { + requests = [][]byte{} + // EIP-6110 deposits + if err := core.ParseDepositLogs(&requests, allLogs, rm.chainConfig); err != nil { + return &newPayloadResult{err: err} + } + // EIP-7002 + if err := core.ProcessWithdrawalQueue(&requests, work.evm); err != nil { + return &newPayloadResult{err: err} + } + // EIP-7251 consolidations + if err := core.ProcessConsolidationQueue(&requests, work.evm); err != nil { + return &newPayloadResult{err: err} + } + } + + if isIsthmus { + requests = [][]byte{} + } + + if requests != nil { + reqHash := types.CalcRequestsHash(requests) + work.header.RequestsHash = &reqHash + } + + // Use FinalizeAndAssemble to match miner logic exactly + newBlock, err := rm.chain.Engine().FinalizeAndAssemble(rm.chain, work.header, work.state, &body, work.receipts) + if err != nil { + return &newPayloadResult{err: err} + } + + return &newPayloadResult{ + block: newBlock, + fees: totalFees(newBlock, work.receipts), + sidecars: work.sidecars, + stateDB: work.state, + receipts: work.receipts, + requests: requests, + witness: work.witness, + } +} + +// prepareWork mimics miner's prepareWork method +// DIFFERENCES FROM MINER: +// - Uses external parent block instead of fetching from chain +// - Uses existing block's Extra and BaseFee instead of computing them +// - Simplified gas limit handling +// - Receives vmCfg for tracing support +func (rm *ReplayMiner) prepareWork(genParams *generateParams, witness bool, block *types.Block, vmCfg vm.Config) (*environment, error) { + // In replay mode, we don't need to check parent block since we already have the complete block + parent := rm.chain.CurrentHeader() + + // Timestamp validation + timestamp := genParams.timestamp + if parent.Time >= timestamp { + if genParams.forceTime { + return nil, fmt.Errorf("invalid timestamp, parent %d given %d", parent.Time, timestamp) + } + timestamp = parent.Time + 1 + } + + // Construct block header + header := &types.Header{ + ParentHash: parent.Hash(), + Number: new(big.Int).Add(parent.Number, common.Big1), + GasLimit: parent.GasLimit, // Use parent's gas limit instead of computing + Time: timestamp, + Coinbase: genParams.coinbase, + } + + // Use existing block's Extra data instead of computing + header.Extra = block.Extra() + + if genParams.random != (common.Hash{}) { + header.MixDigest = genParams.random + } + + // Set EIP-1559 related fields + if rm.chainConfig.IsLondon(header.Number) { + // Use existing block's BaseFee instead of computing + header.BaseFee = block.BaseFee() + } + + // Run consensus preparation + if err := rm.chain.Engine().Prepare(rm.chain, header); err != nil { + rm.logger.Error("Failed to prepare header for sealing", "err", err) + return nil, err + } + + // Apply EIP-4844, EIP-4788 + if rm.chainConfig.IsCancun(header.Number, header.Time) { + var excessBlobGas uint64 + if rm.chainConfig.IsCancun(parent.Number, parent.Time) { + excessBlobGas = eip4844.CalcExcessBlobGas(rm.chainConfig, parent, timestamp) + } + header.BlobGasUsed = new(uint64) + header.ExcessBlobGas = &excessBlobGas + header.ParentBeaconRoot = genParams.beaconRoot + } + + // Create environment + env, err := rm.makeEnv(header, genParams.coinbase, witness, genParams.rpcCtx, vmCfg) + if err != nil { + rm.logger.Error("Failed to create sealing context", "err", err) + return nil, err + } + + env.noTxs = genParams.noTxs + + // Process beacon root + if header.ParentBeaconRoot != nil { + core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, env.evm) + } + + // Process Prague related logic + if rm.chainConfig.IsPrague(header.Number, header.Time) { + core.ProcessParentBlockHash(header.ParentHash, env.evm) + } + + return env, nil +} + +// makeEnv mimics miner's makeEnv method +// DIFFERENCES FROM MINER: +// - Uses external state instead of fetching from chain +// - Simplified state access logic +// - Handles witness export for replay debugging +// - Receives vmCfg for tracing support +func (rm *ReplayMiner) makeEnv(header *types.Header, coinbase common.Address, witness bool, rpcCtx context.Context, vmCfg vm.Config) (*environment, error) { + // In replay mode, we use externally provided state + state := rm.chain.currentState + + if witness { + bundle, err := stateless.NewWitness(header, rm.chain) + if err != nil { + return nil, err + } + state.StartPrefetcher("replay", bundle) + + // Export witness data for debugging (replay-specific logic) + // This is moved from processBlockWithSequencerLogic to align with miner logic + if rm.chain.logger != nil { + rm.chain.logger.Info("Witness created and prefetcher started for replay debugging") + } + } + + return &environment{ + signer: types.MakeSigner(rm.chainConfig, header.Number, header.Time), + state: state, + coinbase: coinbase, + header: header, + witness: state.Witness(), + evm: vm.NewEVM(core.NewEVMBlockContext(header, rm.chain, &coinbase, rm.chainConfig, state), state, rm.chainConfig, vmCfg), + rpcCtx: rpcCtx, + }, nil +} + +// commitTransaction mimics miner's commitTransaction method +// DIFFERENCES FROM MINER: +// - Skips interop transaction checks (not needed in replay mode) +// - Simplified conditional transaction handling +func (rm *ReplayMiner) commitTransaction(env *environment, tx *types.Transaction) error { + // Check conditional transaction if present + if conditional := tx.Conditional(); conditional != nil { + // Check header conditional + if err := env.header.CheckTransactionConditional(conditional); err != nil { + return fmt.Errorf("failed header check: %s: %w", err, errTxConditionalInvalid) + } + // Check state conditional + if err := env.state.CheckTransactionConditional(conditional); err != nil { + return fmt.Errorf("failed state check: %s: %w", err, errTxConditionalInvalid) + } + } + + receipt, err := rm.applyTransaction(env, tx) + if err != nil { + return err + } + env.txs = append(env.txs, tx) + env.receipts = append(env.receipts, receipt) + env.tcount++ + return nil +} + +// applyTransaction mimics miner's applyTransaction method +// DIFFERENCES FROM MINER: +// - None (completely identical logic) +func (rm *ReplayMiner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { + var ( + snap = env.state.Snapshot() + gp = env.gasPool.Gas() + ) + receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx, &env.header.GasUsed) + if err != nil { + env.state.RevertToSnapshot(snap) + env.gasPool.SetGas(gp) + } + return receipt, err +} + +// ============================================================================= +// Utility Functions +// ============================================================================= + +// totalFees computes total consumed miner fees in Wei. Block transactions and receipts have to have the same order. +func totalFees(block *types.Block, receipts []*types.Receipt) *big.Int { + feesWei := new(big.Int) + for i, tx := range block.Transactions() { + minerFee, _ := tx.EffectiveGasTip(block.BaseFee()) + feesWei.Add(feesWei, new(big.Int).Mul(new(big.Int).SetUint64(receipts[i].GasUsed), minerFee)) + // TODO (MariusVanDerWijden) add blob fees + } + return feesWei +} + +// ============================================================================= +// Chain Context Implementation (Implementing Required Interfaces) +// ============================================================================= + +var _ core.ChainContext = (*remoteChainCtx)(nil) +var _ consensus.ChainHeaderReader = (*remoteChainCtx)(nil) + +// Config is part of consensus.ChainHeaderReader +func (r *remoteChainCtx) Config() *params.ChainConfig { + return r.cfg +} + +// CurrentHeader is part of consensus.ChainHeaderReader +func (r remoteChainCtx) CurrentHeader() *types.Header { + return r.hdr +} + +// GetHeaderByNumber is part of consensus.ChainHeaderReader +func (r remoteChainCtx) GetHeaderByNumber(u uint64) *types.Header { + if r.hdr.Number.Uint64() == u { + return r.hdr + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + hdr, err := retry.Do[*types.Header](ctx, 10, retry.Exponential(), func() (*types.Header, error) { + r.logger.Info("fetching block header", "num", u) + return r.cl.HeaderByNumber(ctx, new(big.Int).SetUint64(u)) + }) + if err != nil { + r.logger.Error("failed to get block header", "err", err, "num", u) + return nil + } + if hdr == nil { + r.logger.Warn("header not found", "num", u) + } + return hdr +} + +// GetHeaderByHash is part of consensus.ChainHeaderReader +func (r remoteChainCtx) GetHeaderByHash(hash common.Hash) *types.Header { + if r.hdr.Hash() == hash { + return r.hdr + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + hdr, err := retry.Do[*types.Header](ctx, 10, retry.Exponential(), func() (*types.Header, error) { + r.logger.Info("fetching block header", "hash", hash) + return r.cl.HeaderByHash(ctx, hash) + }) + if err != nil { + r.logger.Error("failed to get block header", "err", err, "hash", hash) + return nil + } + if hdr == nil { + r.logger.Warn("header not found", "hash", hash) + } + return hdr +} + +// GetTd is part of consensus.ChainHeaderReader +func (r remoteChainCtx) GetTd(hash common.Hash, number uint64) *big.Int { + return big.NewInt(1) +} + +// Engine is part of core.ChainContext +func (r remoteChainCtx) Engine() consensus.Engine { + return r.consensusEng +} + +// GetHeader is part of both consensus.ChainHeaderReader and core.ChainContext +func (r remoteChainCtx) GetHeader(hash common.Hash, u uint64) *types.Header { + if r.hdr.Hash() == hash { + return r.hdr + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + hdr, err := retry.Do[*types.Header](ctx, 10, retry.Exponential(), func() (*types.Header, error) { + r.logger.Info("fetching block header", "hash", hash, "num", u) + return r.cl.HeaderByNumber(ctx, new(big.Int).SetUint64(u)) + }) + if err != nil { + r.logger.Error("failed to get block header", "err", err, "hash", hash, "num", u) + return nil + } + if hdr == nil { + r.logger.Warn("header not found", "hash", hash, "num", u) + } + if got := hdr.Hash(); got != hash { + r.logger.Error("fetched incompatible header", "expectedHash", hash, "fetchedHash", got, "num", u) + } + return hdr +} diff --git a/op-chain-ops/cmd/op-replay/README.md b/op-chain-ops/cmd/op-replay/README.md new file mode 100644 index 0000000000000..e86f4c9f8df7e --- /dev/null +++ b/op-chain-ops/cmd/op-replay/README.md @@ -0,0 +1,331 @@ +# op-replay + +`op-replay` is a tool for local op-geth EVM debugging that replays existing blocks in a controlled local environment. It simulates the RPC node's block processing logic to verify state consistency and enable debugging with arbitrary tracers. + +This tool helps debug if existing blocks may have forks due to new client logic by replaying them using the same processing chain as a standard Ethereum RPC node. + +## Purpose + +The main objectives of this tool are: + +1. **State Root Verification**: Replay blocks and compare computed state roots with expected values to ensure execution consistency +2. **RPC Logic Validation**: Verify that the replay logic matches the standard RPC node processing chain +3. **Debugging and Testing**: Enable detailed EVM execution tracing and witness data collection for debugging +4. **Fork Detection**: Identify potential state inconsistencies that could lead to blockchain forks + +## How It Works + +`op-replay` completely mimics the RPC node's block processing logic: + +### Method Correspondence + +The methods in `op-replay` correspond one-to-one with RPC node methods: + +| op-replay Method | RPC Node Method | Purpose | +|------------------|-----------------|---------| +| `processBlockWithRpcLogic()` | `ConsensusAPI.newPayload` | Entry point for new block processing | +| `insertChain()` | `BlockChain.insertChain` | Core method for inserting a chain of blocks | +| `processBlock()` | `BlockChain.processBlock` | Process a single block, including transaction execution | +| `Process()` | `StateProcessor.Process` | Execute transactions and finalize the block | + +### Call Chain + +The tool follows the same call chain as the RPC node: + +#### RPC Node Complete Call Chain +``` +ConsensusAPI.newPayload() -> BlockChain.insertChain() -> BlockChain.processBlock() -> StateProcessor.Process() +``` + +#### op-replay Call Chain +``` +processBlockWithRpcLogic() -> insertChain() -> processBlock() -> Process() +``` + +**Note**: +- `op-replay` starts from `processBlockWithRpcLogic`, which mimics the RPC node's `newPayload` entry point +- It directly processes blocks using the same core logic as the RPC node +- This ensures that the replay process uses identical processing logic, while adapting to replay-specific requirements + +### Key Features + +1. **RPC Logic Simulation**: Completely mimics the RPC node's block processing flow +2. **State Management**: Uses the same state transition logic as the RPC node +3. **Transaction Processing**: Reuses the RPC node's transaction validation and execution logic +4. **Consensus Logic**: Uses the same consensus engine and finalization logic +5. **Object Reuse**: `remoteChainCtx` is created outside the block processing loop and reused for each block, improving performance + +## Code Structure + +### Core Data Structures + +#### remoteChainCtx +- **Purpose**: Provides access to block headers and state for the state transition +- **RPC Correspondence**: Implements `core.ChainContext` and `consensus.ChainHeaderReader` interfaces +- **Replay Specific**: Manages state access in replay mode with external state management + +### Method Implementation Details + +#### 1. processBlockWithRpcLogic Method + +**Correspondence**: `processBlockWithRpcLogic()` ↔ `ConsensusAPI.newPayload()` + +**Reused Logic**: +- Calls `insertChain` to process the block +- Handles state commitment and state root computation +- Returns processing results and witness data + +**Differences from RPC**: +- **No API Parameters**: Skips `versionedHashes`, `beaconRoot`, `requests` parameters (not needed for replay) +- **Direct Processing**: Directly processes the block instead of going through consensus API +- **Witness Handling**: Witness generation is handled in `insertChain`, not directly here + +**Reason**: Replay tools don't need the full consensus API interface, but require the same core processing logic. + +#### 2. insertChain Method + +**Correspondence**: `insertChain()` ↔ `BlockChain.insertChain()` + +**Reused Logic**: +- Parallel signature recovery using `core.SenderCacher().RecoverFromBlocks()` +- Witness creation and prefetcher management +- Calls `processBlock` for core processing + +**Differences from RPC**: +- **Conditional Witness**: Witness generation is conditional based on the `witness` parameter +- **Direct Processing**: Directly processes the block without blockchain validation +- **Simplified Logic**: Focuses on core processing without RPC-specific overhead + +**Reason**: Replay tools need the same core logic but don't require full blockchain validation. + +#### 3. processBlock Method + +**Correspondence**: `processBlock()` ↔ `BlockChain.processBlock()` + +**Reused Logic**: +- Calls `Process` method for transaction execution +- Handles block processing results + +**Differences from RPC**: +- **No Block Validation**: Block validation is already done in replay context +- **Witness Generation**: Witness generation is handled in `insertChain` +- **Direct Processing**: Directly processes the block without blockchain context + +**Reason**: Replay tools focus on core processing and validation, not blockchain management. + +#### 4. Process Method + +**Correspondence**: `Process()` ↔ `StateProcessor.Process()` + +**Reused Logic**: +- Hardfork specifications application (DAO fork, Create2 deployer) +- EVM block context and environment creation +- Transaction processing and receipt generation +- Consensus layer requests processing (EIP-6110, EIP-7002, EIP-7251) +- Block finalization using consensus engine + +**Differences from RPC**: +- **No Witness Generation**: Witness generation is handled in `insertChain` +- **Direct Processing**: Directly processes consensus requests without blockchain context + +**Reason**: This is the core transaction processing logic that should be identical between RPC and replay. + +## Code Reuse Analysis + +### Completely Reused Logic + +The following logic is completely reused from the RPC node: + +1. **Block Processing** (`processBlock`) + - Transaction execution flow + - Receipt generation + - Gas management + +2. **State Processing** (`Process`) + - Hardfork specifications + - EVM environment setup + - Transaction application + +3. **Transaction Handling** (`insertChain`) + - Parallel signature recovery + - Witness creation and prefetcher management + +4. **Consensus Logic** + - Consensus engine finalization + - EIP-6110, EIP-7002, EIP-7251 processing + +### Replay-Specific Logic + +The following logic is specific to the replay tool: + +1. **State Management** + - External state initialization and updates + - StateDB lifecycle management + - State root comparison and validation + +2. **Output Processing** + - Trace file generation + - Witness data export + - Performance mode (no file output) + +3. **Skipped Logic** + - Full blockchain validation + - RPC-specific overhead + - Consensus API interface + +4. **Object Reuse Optimization** + - `remoteChainCtx` created outside the loop and reused for each block + - Efficient state database handling + - Conditional witness generation and tracing + +### Skipped RPC Logic + +1. **API Layer** (`ConsensusAPI.newPayload`) + - Replay tools don't need to handle external API calls + - Directly process existing blocks + +2. **Blockchain Management** + - No need for full blockchain validation + - Focus on core processing logic + +## Usage + +### Basic Usage + +```bash +# Replay blocks 1000-1100 +op-replay --rpc --start 1000 --end 1100 + +# Replay blocks with trace and witness generation +op-replay --rpc --start 1000 --end 1100 --out ./trace_output +``` + +### Parameter Description + +- `--rpc`: RPC endpoint address (required) +- `--start`: Start block number (required) +- `--end`: End block number (required) +- `--out`: Output directory path (optional, for saving trace files) + +### Environment Variables + +You can set parameters via environment variables: + +```bash +export OP_REPLAY_RPC="http://localhost:8545" +export OP_REPLAY_START=1000 +export OP_REPLAY_END=1100 +export OP_REPLAY_OUT="./trace_output" + +op-replay +``` + +## Output + +### Console Output + +The tool displays processing progress and results in the console: + +``` +INFO Starting block range replay using RPC logic start=1000 end=1100 +INFO Initialized state from parent block parent_number=999 parent_root=0x1234... +INFO Processing block number=1000 +INFO Block processed successfully block_number=1000 state_root=0x5678... gas_used=15000000 tx_count=150 +... +INFO Replay completed total_blocks=101 success_blocks=101 mismatch_blocks=0 success_rate=100.00% +``` + +### Trace Files (Optional) + +If `--out` parameter is specified, the tool generates: + +- `block_{N}_trace.txt`: Transaction execution traces +- `block_{N}_witness.json`: State access witness data + +## Differences from op-replay-seq + +| Feature | op-replay | op-replay-seq | +|---------|-----------|---------------| +| **Execution Logic** | RPC node logic | Sequencer logic (reusing miner) | +| **Transaction Source** | From block parsing | From block parsing (reusing miner logic) | +| **State Processing** | Direct state transition | Reusing miner's state processing logic | +| **Method Correspondence** | Independent implementation | One-to-one correspondence with miner methods | +| **Purpose** | Verify block execution | Verify sequencer logic consistency | + +## Use Cases + +### 1. State Root Verification + +Verify that replayed blocks produce the same state roots as the original blockchain: + +```bash +# Replay recent 1000 blocks to verify state consistency +op-replay --rpc --start --end +``` + +### 2. RPC Logic Testing + +Test new RPC node logic by replaying historical blocks: + +```bash +# Test new logic on historical blocks +op-replay --rpc --start 1000000 --end 1000100 +``` + +### 3. Performance Testing + +Test RPC node performance improvements: + +```bash +# Performance test without file output +op-replay --rpc --start 1000000 --end 1000100 +``` + +## Notes + +1. **RPC Performance**: The tool needs to frequently call RPC interfaces, ensure the RPC endpoint has sufficient performance +2. **Memory Usage**: Pay attention to memory usage when replaying large numbers of blocks +3. **Network Stability**: Ensure stable network connection with the RPC endpoint +4. **Block Range**: Start with small ranges for testing, then gradually expand + +## Troubleshooting + +### Common Errors + +1. **RPC Connection Failure**: Check RPC endpoint address and network connectivity +2. **Block Fetch Failure**: Confirm block number range is valid and RPC endpoint has corresponding data +3. **State Root Mismatch**: May indicate RPC logic issues, need further investigation + +### Debugging Tips + +1. Use `--out` parameter to generate detailed trace files +2. Start with small block ranges for testing +3. Check log output for detailed error information +4. Verify chain configuration and hardfork settings + +## Maintenance Guide + +### When RPC Logic Updates + +1. **Check Method Signatures**: Confirm that corresponding method signatures haven't changed +2. **Review Reused Logic**: Ensure reused logic sections don't need updates +3. **Check Difference Logic**: Confirm replay-specific logic doesn't need adjustment +4. **Test Validation**: Run tests to ensure functionality works correctly + +### Adding New Features + +1. **Determine Reusability**: Judge whether new features should exist in both RPC and replay +2. **Method Correspondence**: If reusing, ensure method names and logic correspond +3. **Documentation Updates**: Update method correspondence table + +## Advantages Summary + +1. **Code Consistency**: Maintains consistency with RPC node logic +2. **Maintenance Convenience**: Easy to identify code that needs synchronization when RPC updates +3. **Complete Functionality**: Reuses all core RPC processing logic +4. **Debugging Friendly**: Can be compared directly with RPC node code for debugging +5. **Clear Structure**: Clearly distinguishes reused code from replay-specific code + +## Contributing + +Welcome to submit Issues and Pull Requests to improve this tool. diff --git a/op-chain-ops/cmd/op-replay/hybrid_db.go b/op-chain-ops/cmd/op-replay/hybrid_db.go new file mode 100644 index 0000000000000..761735d8b743c --- /dev/null +++ b/op-chain-ops/cmd/op-replay/hybrid_db.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/ethdb/remotedb" + "github.com/ethereum/go-ethereum/rpc" +) + +// ============================================================================= +// Hybrid Database Implementation +// ============================================================================= + +// hybridRemoteDB combines remotedb (read-only) with memdb (write cache) +// This solves the NewBatch issue by writing to memdb while reading from both sources +type hybridRemoteDB struct { + ethdb.Database // Embed the interface to inherit all methods + memDB ethdb.Database // Write cache for all modifications + mu sync.RWMutex // Protect concurrent access +} + +// NewHybridRemoteDB creates a new hybrid database that combines remotedb and memdb +func NewHybridRemoteDB(cl *rpc.Client) *hybridRemoteDB { + remoteDB := remotedb.New(cl) + return &hybridRemoteDB{ + Database: remoteDB, // Embed the remote database + memDB: rawdb.NewDatabase(memorydb.New()), + } +} + +// NewBatch returns a batch that writes to memdb +func (h *hybridRemoteDB) NewBatch() ethdb.Batch { + return h.memDB.NewBatch() +} + +// Get implements layered read strategy: memdb first, then remotedb +func (h *hybridRemoteDB) Get(key []byte) ([]byte, error) { + h.mu.RLock() + defer h.mu.RUnlock() + + // First try memdb (most recent data) + if val, err := h.memDB.Get(key); err == nil { + return val, nil + } + + // If not in memdb, try remotedb (initial state) + return h.Database.Get(key) +} + +// Has implements layered read strategy +func (h *hybridRemoteDB) Has(key []byte) (bool, error) { + h.mu.RLock() + defer h.mu.RUnlock() + + // First check memdb + if has, err := h.memDB.Has(key); err == nil && has { + return true, nil + } + + // Then check remotedb + return h.Database.Has(key) +} + +// Put writes to memdb only +func (h *hybridRemoteDB) Put(key, value []byte) error { + h.mu.Lock() + defer h.mu.Unlock() + return h.memDB.Put(key, value) +} + +// Delete removes from memdb only +func (h *hybridRemoteDB) Delete(key []byte) error { + h.mu.Lock() + defer h.mu.Unlock() + return h.memDB.Delete(key) +} + +// Close closes both databases +func (h *hybridRemoteDB) Close() error { + h.mu.Lock() + defer h.mu.Unlock() + + // Close memdb first + if err := h.memDB.Close(); err != nil { + return err + } + + // Then close remotedb + return h.Database.Close() +} + +// For all other methods, delegate to remoteDB (read-only operations) +func (h *hybridRemoteDB) Stat() (string, error) { return h.Database.Stat() } + +func (h *hybridRemoteDB) Compact(start []byte, limit []byte) error { + return h.Database.Compact(start, limit) +} + +func (h *hybridRemoteDB) NewIterator(prefix []byte, start []byte) ethdb.Iterator { + return h.Database.NewIterator(prefix, start) +} + +func (h *hybridRemoteDB) Ancient(kind string, number uint64) ([]byte, error) { + return h.Database.Ancient(kind, number) +} + +func (h *hybridRemoteDB) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) { + return h.Database.AncientRange(kind, start, count, maxBytes) +} + +func (h *hybridRemoteDB) Ancients() (uint64, error) { return h.Database.Ancients() } + +func (h *hybridRemoteDB) Tail() (uint64, error) { return h.Database.Tail() } + +func (h *hybridRemoteDB) AncientSize(kind string) (uint64, error) { + return h.Database.AncientSize(kind) +} + +func (h *hybridRemoteDB) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) { + // 使用类型断言来调用正确的方法 + if reader, ok := h.Database.(interface { + ReadAncients(func(ethdb.AncientReaderOp) error) error + }); ok { + return reader.ReadAncients(fn) + } + // 如果底层数据库不支持,返回错误 + return fmt.Errorf("underlying database does not support ReadAncients") +} + +func (h *hybridRemoteDB) Sync() error { return h.Database.Sync() } + +func (h *hybridRemoteDB) AncientDatadir() (string, error) { return h.Database.AncientDatadir() } + +func (h *hybridRemoteDB) DeleteRange(start []byte, limit []byte) error { + return h.Database.DeleteRange(start, limit) +} + +func (h *hybridRemoteDB) HasAncient(kind string, number uint64) (bool, error) { + return h.Database.HasAncient(kind, number) +} + +func (h *hybridRemoteDB) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (int64, error) { + // 检查底层数据库是否支持 ModifyAncients + if modifier, ok := h.Database.(interface { + ModifyAncients(func(ethdb.AncientWriteOp) error) (int64, error) + }); ok { + return modifier.ModifyAncients(fn) + } + // 如果不支持,返回默认值 + return 0, fmt.Errorf("underlying database does not support ModifyAncients") +} diff --git a/op-chain-ops/cmd/op-replay/main.go b/op-chain-ops/cmd/op-replay/main.go new file mode 100644 index 0000000000000..33a4015bf6d03 --- /dev/null +++ b/op-chain-ops/cmd/op-replay/main.go @@ -0,0 +1,738 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "math/big" + "os" + "time" + + "github.com/urfave/cli/v2" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core" + gstate "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/stateless" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + logger2 "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/triedb" + + op_service "github.com/ethereum-optimism/optimism/op-service" + "github.com/ethereum-optimism/optimism/op-service/cliapp" + "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" + "github.com/ethereum-optimism/optimism/op-service/eth" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum-optimism/optimism/op-service/retry" + "github.com/ethereum-optimism/optimism/op-service/sources" +) + +// ============================================================================= +// Constants and Global Variables +// ============================================================================= + +const EnvPrefix = "OP_REPLAY" + +var ( + RPCFlag = &cli.StringFlag{ + Name: "rpc", + Usage: "RPC endpoint to fetch data from", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "RPC"), + Required: true, + } + StartFlag = &cli.IntFlag{ + Name: "start", + Usage: "Start block number", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "START"), + Required: true, + } + EndFlag = &cli.IntFlag{ + Name: "end", + Usage: "End block number", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "END"), + Required: true, + } + OutPathFlag = &cli.PathFlag{ + Name: "out", + Usage: "Path to directory to write trace data files (optional, if not specified no files will be written)", + EnvVars: op_service.PrefixEnvVar(EnvPrefix, "OUT"), + Value: "", + } +) + +// ============================================================================= +// Core Data Structures +// ============================================================================= + +// remoteChainCtx provides access to block-headers, for usage by the state-transition, +// such as basefee computation (based on prior block) and EVM block-hash opcode. +type remoteChainCtx struct { + consensusEng consensus.Engine + hdr *types.Header + cfg *params.ChainConfig + cl *ethclient.Client + logger log.Logger + currentState *gstate.StateDB // Added to support state access in replay mode +} + +// ============================================================================= +// Interface Implementation +// ============================================================================= + +var _ core.ChainContext = (*remoteChainCtx)(nil) +var _ consensus.ChainHeaderReader = (*remoteChainCtx)(nil) + +// Config is part of consensus.ChainHeaderReader +func (r *remoteChainCtx) Config() *params.ChainConfig { + return r.cfg +} + +// CurrentHeader is part of consensus.ChainHeaderReader +func (r remoteChainCtx) CurrentHeader() *types.Header { + return r.hdr +} + +// GetHeaderByNumber is part of consensus.ChainHeaderReader +func (r remoteChainCtx) GetHeaderByNumber(u uint64) *types.Header { + if r.hdr.Number.Uint64() == u { + return r.hdr + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + hdr, err := retry.Do[*types.Header](ctx, 10, retry.Exponential(), func() (*types.Header, error) { + r.logger.Info("fetching block header", "num", u) + return r.cl.HeaderByNumber(ctx, new(big.Int).SetUint64(u)) + }) + if err != nil { + r.logger.Error("failed to get block header", "err", err, "num", u) + return nil + } + if hdr == nil { + r.logger.Warn("header not found", "num", u) + } + return hdr +} + +// GetHeaderByHash is part of consensus.ChainHeaderReader +func (r remoteChainCtx) GetHeaderByHash(hash common.Hash) *types.Header { + if r.hdr.Hash() == hash { + return r.hdr + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + hdr, err := retry.Do[*types.Header](ctx, 10, retry.Exponential(), func() (*types.Header, error) { + r.logger.Info("fetching block header", "hash", hash) + return r.cl.HeaderByHash(ctx, hash) + }) + if err != nil { + r.logger.Error("failed to get block header", "err", err, "hash", hash) + return nil + } + if hdr == nil { + r.logger.Warn("header not found", "hash", hash) + } + return hdr +} + +// GetTd is part of consensus.ChainHeaderReader +func (r remoteChainCtx) GetTd(hash common.Hash, number uint64) *big.Int { + return big.NewInt(1) +} + +// Engine is part of core.ChainContext +func (r remoteChainCtx) Engine() consensus.Engine { + return r.consensusEng +} + +// GetHeader is part of both consensus.ChainHeaderReader and core.ChainContext +func (r remoteChainCtx) GetHeader(hash common.Hash, u uint64) *types.Header { + if r.hdr.Hash() == hash { + return r.hdr + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + hdr, err := retry.Do[*types.Header](ctx, 10, retry.Exponential(), func() (*types.Header, error) { + r.logger.Info("fetching block header", "hash", hash, "num", u) + return r.cl.HeaderByNumber(ctx, new(big.Int).SetUint64(u)) + }) + if err != nil { + r.logger.Error("failed to get block header", "err", err, "hash", hash, "num", u) + return nil + } + if hdr == nil { + r.logger.Warn("header not found", "hash", hash, "num", u) + } + if got := hdr.Hash(); got != hash { + r.logger.Error("fetched incompatible header", "expectedHash", hash, "fetchedHash", got, "num", u) + } + return hdr +} + +// ============================================================================= +// Main Application Entry Point +// ============================================================================= + +func main() { + flags := []cli.Flag{ + RPCFlag, StartFlag, EndFlag, OutPathFlag, + } + flags = append(flags, oplog.CLIFlags(EnvPrefix)...) + + app := cli.NewApp() + app.Name = "op-replay" + app.Usage = "Replay a range of blocks locally." + app.Description = "Replay a range of blocks locally and verify state roots." + app.Flags = cliapp.ProtectFlags(flags) + app.Action = mainAction + app.Writer = os.Stdout + app.ErrWriter = os.Stderr + err := app.Run(os.Args) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Application failed: %v", err) + os.Exit(1) + } +} + +// ============================================================================= +// Core Business Logic +// ============================================================================= + +// mainAction is the main application logic that orchestrates the block replay process +func mainAction(c *cli.Context) error { + ctx := ctxinterrupt.WithCancelOnInterrupt(c.Context) + logCfg := oplog.ReadCLIConfig(c) + logger := oplog.NewLogger(c.App.Writer, logCfg) + + rpcEndpoint := c.String(RPCFlag.Name) + start := c.Int(StartFlag.Name) + end := c.Int(EndFlag.Name) + outDir := c.Path(OutPathFlag.Name) + + if start > end { + return fmt.Errorf("start block (%d) must be less than or equal to end block (%d)", start, end) + } + + // Create output directory if it doesn't exist + if outDir != "" { + if err := os.MkdirAll(outDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + } + + cl, err := rpc.DialContext(ctx, rpcEndpoint) + if err != nil { + return fmt.Errorf("failed to dial RPC: %w", err) + } + defer cl.Close() + + ethCl := ethclient.NewClient(cl) + db := NewHybridRemoteDB(cl) + + var config *params.ChainConfig + if err := cl.CallContext(ctx, &config, "debug_chainConfig"); err != nil { + return fmt.Errorf("failed to fetch chain config: %w", err) + } + + // Print chain configuration details + logger.Info("Chain configuration loaded", + "chain_id", config.ChainID, + "homestead_block", config.HomesteadBlock, + "dao_fork_block", config.DAOForkBlock, + "dao_fork_support", config.DAOForkSupport, + "eip150_block", config.EIP150Block, + "eip155_block", config.EIP155Block, + "eip158_block", config.EIP158Block, + "byzantium_block", config.ByzantiumBlock, + "constantinople_block", config.ConstantinopleBlock, + "petersburg_block", config.PetersburgBlock, + "istanbul_block", config.IstanbulBlock, + "muir_glacier_block", config.MuirGlacierBlock, + "berlin_block", config.BerlinBlock, + "london_block", config.LondonBlock, + "arrow_glacier_block", config.ArrowGlacierBlock, + "gray_glacier_block", config.GrayGlacierBlock, + "shanghai_time", config.ShanghaiTime, + "cancun_time", config.CancunTime, + "prague_time", config.PragueTime, + "terminal_total_difficulty", config.TerminalTotalDifficulty, + "optimism_config", config.Optimism != nil, + "clique_config", config.Clique != nil, + ) + + // Print detailed Optimism configuration if available + if config.Optimism != nil { + logger.Info("Optimism configuration", + "eip1559_elasticity", config.Optimism.EIP1559Elasticity, + "eip1559_denominator", config.Optimism.EIP1559Denominator, + "eip1559_denominator_canyon", config.Optimism.EIP1559DenominatorCanyon, + ) + } + + // Print detailed Clique configuration if available + if config.Clique != nil { + logger.Info("Clique configuration", + "epoch", config.Clique.Epoch, + "period", config.Clique.Period, + ) + } + + // Print additional hardfork timestamps if available + if config.RegolithTime != nil { + logger.Info("Regolith hardfork", "time", *config.RegolithTime) + } + if config.CanyonTime != nil { + logger.Info("Canyon hardfork", "time", *config.CanyonTime) + } + if config.EcotoneTime != nil { + logger.Info("Ecotone hardfork", "time", *config.EcotoneTime) + } + if config.FjordTime != nil { + logger.Info("Fjord hardfork", "time", *config.FjordTime) + } + if config.InteropTime != nil { + logger.Info("Interop hardfork", "time", *config.InteropTime) + } + + logger.Info("Starting block range replay using RPC logic", "start", start, "end", end) + + // Get parent block of the start block + parentBlock, err := ethCl.HeaderByNumber(ctx, big.NewInt(int64(start-1))) + if err != nil { + return fmt.Errorf("failed to fetch parent block %d: %w", start-1, err) + } + + stateDB := gstate.NewDatabase(triedb.NewDatabase(db, &triedb.Config{ + Preimages: true, + }), nil) + + currentState, err := gstate.New(parentBlock.Root, stateDB) + if err != nil { + return fmt.Errorf("failed to create initial state from block %d: %w", start-1, err) + } + logger.Info("Initialized state from parent block", "parent_number", start-1, "parent_root", parentBlock.Root) + + // Create remoteChainCtx once, outside the block processing loop + // This matches the RPC pattern where these objects are created once and reused + consensusEng := beacon.New(&beacon.OpLegacy{}) + chCtx := &remoteChainCtx{ + consensusEng: consensusEng, + hdr: parentBlock, // Will be updated for each block + cfg: config, + cl: ethCl, + logger: logger, + currentState: currentState, // Will be updated for each block + } + + // Stats tracking + var ( + totalBlocks = 0 + successBlocks = 0 + mismatchBlocks = 0 + prevBlockHash common.Hash // Track previous block hash for validation + ) + + // Process each block in the range + for i := start; i <= end; i++ { + logger.Info("Processing block", "number", i) + + // Fetch the block + block, err := ethCl.BlockByNumber(ctx, big.NewInt(int64(i))) + if err != nil { + return fmt.Errorf("failed to fetch block %d: %w", i, err) + } + + // Basic block validation + if block.NumberU64() != uint64(i) { + return fmt.Errorf("block number mismatch: expected %d, got %d", i, block.NumberU64()) + } + + // Verify parent block connection (except for the first block) + if i > start { + expectedParentHash := prevBlockHash + if block.ParentHash() != expectedParentHash { + return fmt.Errorf("parent hash mismatch at block %d: expected %s, got %s", + i, expectedParentHash, block.ParentHash()) + } + } + + // Create output file for this block's trace + var outW io.Writer = io.Discard // Default to discard if no output directory + if outDir != "" { + outPath := fmt.Sprintf("%s/block_%d_trace.txt", outDir, i) + file, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to create trace file for block %d: %w", i, err) + } + outW = file + } + + // Convert types.Block to sources.RPCBlock for processing + rpcBlock, err := convertToRPCBlock(block) + if err != nil { + return fmt.Errorf("failed to convert block %d to RPC format: %w", i, err) + } + + // Process the block using the same logic as RPC node + // This mimics the call chain: processBlockWithRpcLogic -> insertChain -> processBlock -> Process + result, witness, newStateRoot, err := processBlockWithRpcLogic(logger, config, chCtx, rpcBlock, outW, outDir != "") + + // Export witness data for debugging if output directory is specified + if outDir != "" && witness != nil { + witnessDump := witness.ToExecutionWitness() + out, err := json.MarshalIndent(witnessDump, "", " ") + if err != nil { + logger.Error("failed to encode witness", "err", err) + } else { + witnessPath := fmt.Sprintf("%s/block_%d_witness.json", outDir, i) + if err := os.WriteFile(witnessPath, out, 0644); err != nil { + logger.Debug("Failed to write witness", "err", err, "path", witnessPath) + } else { + logger.Info("Witness data exported", "path", witnessPath) + } + } + } + + // Close the file immediately after processing the block + if outDir != "" { + if file, ok := outW.(*os.File); ok { + file.Close() + } + } + + if err != nil { + logger.Error("Failed to process block", "number", i, "error", err) + return fmt.Errorf("failed to process block %d: %w", i, err) + } + + totalBlocks++ + + // Compare state roots + expectedRoot := block.Root() + if newStateRoot != expectedRoot { + mismatchBlocks++ + logger.Error("State root mismatch", + "block_number", i, + "expected_root", expectedRoot, + "computed_root", newStateRoot, + "gas_used", result.GasUsed) + } else { + successBlocks++ + logger.Info("Block processed successfully", + "block_number", i, + "state_root", newStateRoot, + "gas_used", result.GasUsed, + "tx_count", len(block.Transactions())) + } + + // Update current state to the new state for next block + // Note: We need to create a new StateDB instance because the current one is already committed + currentState, err = gstate.New(newStateRoot, stateDB) + if err != nil { + return fmt.Errorf("failed to create state for next block: %w", err) + } + + // Update the remoteChainCtx for the next block processing + chCtx.currentState = currentState // Update state for next block + chCtx.hdr = block.Header() // Update header to current block (will be parent for next block) + + // Update previous block hash for next iteration + prevBlockHash = block.Hash() + } + + // Print summary + logger.Info("Replay completed", + "total_blocks", totalBlocks, + "success_blocks", successBlocks, + "mismatch_blocks", mismatchBlocks, + "success_rate", fmt.Sprintf("%.2f%%", float64(successBlocks)/float64(totalBlocks)*100)) + + if mismatchBlocks > 0 { + return fmt.Errorf("state root mismatches detected in %d out of %d blocks", mismatchBlocks, totalBlocks) + } + + return nil +} + +// ============================================================================= +// Block Processing Methods (RPC Logic Simulation) +// ============================================================================= + +// processBlockWithRpcLogic mimics ConsensusAPI.newPayload - the entry point for processing new blocks +// DIFFERENCES FROM RPC: +// - No versionedHashes, beaconRoot, requests parameters (not needed for replay) +// - Witness generation is handled in insertChain method (not directly here) +// - Directly processes the block instead of going through consensus API +func processBlockWithRpcLogic(logger log.Logger, config *params.ChainConfig, chCtx *remoteChainCtx, block *sources.RPCBlock, + outW io.Writer, witness bool) (*core.ProcessResult, *stateless.Witness, common.Hash, error) { + + header := block.CreateGethHeader() + + vmCfg := vm.Config{Tracer: nil} + + // Set up tracing only if output is requested + if outW != nil && outW != io.Discard { + vmCfg.Tracer = logger2.NewJSONLogger(&logger2.Config{ + EnableMemory: false, + DisableStack: false, + DisableStorage: false, + EnableReturnData: false, + Limit: 0, + Overrides: nil, + }, outW) + } + + // Witness creation and prefetcher management is now handled in insertChain method + // to align with RPC logic and avoid duplication + + // insertChain mimics BlockChain.insertChain + // DIFFERENCES FROM RPC: + // - Witness generation is conditional based on the witness parameter + // - Directly processes the block without blockchain validation + result, witnessData, err := insertChain(logger, config, block, chCtx.currentState, vmCfg, chCtx, outW, witness) + if err != nil { + return nil, nil, common.Hash{}, err + } + + // Commit the state changes and get the new state root + newStateRoot, err := chCtx.currentState.Commit(uint64(block.Number), config.IsEIP158(header.Number), config.IsCancun(header.Number, header.Time)) + if err != nil { + return nil, nil, common.Hash{}, fmt.Errorf("failed to commit state: %w", err) + } + + return result, witnessData, newStateRoot, nil +} + +// insertChain mimics BlockChain.insertChain - inserts a block into the chain +// DIFFERENCES FROM RPC: +// - Witness generation is conditional based on the witness parameter +// - Directly processes the block without blockchain validation +func insertChain(logger log.Logger, config *params.ChainConfig, + block *sources.RPCBlock, + statedb *gstate.StateDB, cfg vm.Config, + chainCtx *remoteChainCtx, outW io.Writer, witness bool) (*core.ProcessResult, *stateless.Witness, error) { + + // Start a parallel signature recovery (matching RPC insertChain lines 1685-1686) + // This is important for transaction signature validation + gethBlock, err := convertRPCBlockToGethBlock(block) + if err != nil { + logger.Error("failed to convert RPC block to geth block", "err", err) + } else { + core.SenderCacher().RecoverFromBlocks(types.MakeSigner(config, new(big.Int).SetUint64(uint64(block.Number)), uint64(block.Time)), []*types.Block{gethBlock}) + } + + // Witness creation and prefetcher management (matching RPC insertChain lines 1850-1860) + // DIFFERENCES FROM RPC: + // - Witness generation is conditional and simplified for replay purposes + // - Simplified prefetcher management + var createdWitness *stateless.Witness + if witness { + // Create witness for debugging purposes + header := block.CreateGethHeader() + var err error + createdWitness, err = stateless.NewWitness(header, chainCtx) + if err != nil { + logger.Error("failed to prepare witness data collector", "err", err) + } else { + // Start prefetcher for trie node path optimization + statedb.StartPrefetcher("replay", createdWitness) + } + } else if config.IsByzantium(new(big.Int).SetUint64(uint64(block.Number))) { + // Start prefetcher without witness for trie node path optimization + statedb.StartPrefetcher("replay", nil) + } + + // processBlock mimics BlockChain.processBlock - processes the block using StateProcessor.Process + // DIFFERENCES FROM RPC: + // - No block validation (already validated in replay context) + // - Witness generation is handled in insertChain, not here + // - Directly processes the block without blockchain context + result, err := processBlock(logger, config, block, statedb, cfg, chainCtx, outW) + if err != nil { + return nil, nil, err + } + + logger.Info("Completed block processing") + if outW != nil { + _, _ = fmt.Fprintf(outW, "# Completed block processing\n") + } + + return result, createdWitness, nil +} + +// processBlock mimics BlockChain.processBlock - processes the block using StateProcessor.Process +// DIFFERENCES FROM RPC: +// - No logging hooks (OnBlockStart/OnBlockEnd not needed in replay) +// - No stateless self-validation (not needed in replay) +// - No metrics collection (not needed in replay) +// - No database writes (not needed in replay) +// - Focus only on core processing and validation +func processBlock(logger log.Logger, config *params.ChainConfig, block *sources.RPCBlock, statedb *gstate.StateDB, cfg vm.Config, chainCtx *remoteChainCtx, outW io.Writer) (*core.ProcessResult, error) { + + // Process mimics StateProcessor.Process - processes all transactions and finalizes the block + // DIFFERENCES FROM RPC: + // - Witness generation is handled in insertChain, not here + // - Directly processes consensus requests without blockchain context + result, err := Process(logger, config, block, statedb, cfg, chainCtx) + if err != nil { + return nil, err + } + + // SKIPPED: validateState logic (not needed for replay) + // REASON: Replay tool's purpose is to replay blocks and verify state roots, + // not to validate other block properties like gas usage, bloom filters, etc. + + return result, nil +} + +// Process mimics StateProcessor.Process - processes all transactions and finalizes the block +// DIFFERENCES FROM RPC: +// - Witness generation is handled in insertChain, not here +// - Directly processes consensus requests without blockchain context +func Process(logger log.Logger, config *params.ChainConfig, block *sources.RPCBlock, statedb *gstate.StateDB, cfg vm.Config, chainCtx *remoteChainCtx) (*core.ProcessResult, error) { + + var ( + receipts types.Receipts + usedGas = new(uint64) + header = block.CreateGethHeader() + blockHash = block.Hash + blockNumber = new(big.Int).SetUint64(uint64(block.Number)) + allLogs []*types.Log + gp = new(core.GasPool).AddGas(uint64(block.GasLimit)) + signer = types.MakeSigner(config, header.Number, header.Time) + ) + + // Mutate the block and state according to any hard-fork specs (matching RPC StateProcessor.Process lines 68-70) + if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(blockNumber) == 0 { + misc.ApplyDAOHardFork(statedb) + } + misc.EnsureCreate2Deployer(config, uint64(block.Time), statedb) + + // Create EVM block context and environment (matching RPC StateProcessor.Process) + blockContext := core.NewEVMBlockContext(header, chainCtx, nil, config, statedb) + vmenv := vm.NewEVM(blockContext, statedb, config, cfg) + + // Process beacon root and parent block hash + if beaconRoot := block.ParentBeaconRoot; beaconRoot != nil { + core.ProcessBeaconBlockRoot(*beaconRoot, vmenv) + } + if config.IsPrague(blockNumber, uint64(block.Time)) { + core.ProcessParentBlockHash(block.ParentHash, vmenv) + } + + logger.Info("Prepared EVM state") + logger.Info("Processing transactions", "count", len(block.Transactions)) + + // Iterate over and process the individual transactions + for i, tx := range block.Transactions { + logger.Info("Processing tx", "i", i, "hash", tx.Hash().Hex()) + msg, err := core.TransactionToMessage(tx, signer, header.BaseFee) + if err != nil { + return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + statedb.SetTxContext(tx.Hash(), i) + + receipt, err := core.ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv) + if err != nil { + return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + receipts = append(receipts, receipt) + allLogs = append(allLogs, receipt.Logs...) + } + + isIsthmus := config.IsIsthmus(uint64(block.Time)) + + // Read requests if Prague is enabled. + var requests [][]byte + if config.IsPrague(blockNumber, uint64(block.Time)) && !isIsthmus { + requests = [][]byte{} + // EIP-6110 + if err := core.ParseDepositLogs(&requests, allLogs, config); err != nil { + return nil, err + } + // EIP-7002 + if err := core.ProcessWithdrawalQueue(&requests, vmenv); err != nil { + return nil, err + } + // EIP-7251 + if err := core.ProcessConsolidationQueue(&requests, vmenv); err != nil { + return nil, err + } + } + + if isIsthmus { + requests = [][]byte{} + } + + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) + chainCtx.Engine().Finalize(chainCtx, header, statedb, &types.Body{Transactions: block.Transactions, Withdrawals: *block.Withdrawals}) + + return &core.ProcessResult{ + Receipts: receipts, + Requests: requests, + Logs: allLogs, + GasUsed: *usedGas, + }, nil +} + +// ============================================================================= +// Utility Functions +// ============================================================================= + +// convertToRPCBlock converts a standard geth block to RPC block format +func convertToRPCBlock(block *types.Block) (*sources.RPCBlock, error) { + withdrawals := block.Withdrawals() + return &sources.RPCBlock{ + RPCHeader: sources.RPCHeader{ + ParentHash: block.ParentHash(), + UncleHash: block.UncleHash(), + Coinbase: block.Coinbase(), + Root: block.Root(), + TxHash: block.TxHash(), + ReceiptHash: block.ReceiptHash(), + Bloom: eth.Bytes256(block.Bloom()), + Difficulty: hexutil.Big(*block.Difficulty()), + Number: hexutil.Uint64(block.NumberU64()), + GasLimit: hexutil.Uint64(block.GasLimit()), + GasUsed: hexutil.Uint64(block.GasUsed()), + Time: hexutil.Uint64(block.Time()), + Extra: hexutil.Bytes(block.Extra()), + MixDigest: block.MixDigest(), + Nonce: types.EncodeNonce(block.Nonce()), + BaseFee: (*hexutil.Big)(block.BaseFee()), + WithdrawalsRoot: block.Header().WithdrawalsHash, + BlobGasUsed: (*hexutil.Uint64)(block.BlobGasUsed()), + ExcessBlobGas: (*hexutil.Uint64)(block.ExcessBlobGas()), + ParentBeaconRoot: block.BeaconRoot(), + Hash: block.Hash(), + }, + Transactions: block.Transactions(), + Withdrawals: &withdrawals, + }, nil +} + +// convertRPCBlockToGethBlock converts an RPCBlock back to a standard geth block +// This is the reverse operation of convertToRPCBlock +func convertRPCBlockToGethBlock(rpcBlock *sources.RPCBlock) (*types.Block, error) { + header := rpcBlock.CreateGethHeader() + + // Create the block with header and transactions + block := types.NewBlockWithHeader(header) + var withdrawals []*types.Withdrawal + if rpcBlock.Withdrawals != nil { + withdrawals = *rpcBlock.Withdrawals + } + body := types.Body{Transactions: rpcBlock.Transactions, Withdrawals: withdrawals} + block = block.WithBody(body) + + return block, nil +}