Bug Found: RocksDB pending batch writes are not visible to concurrent history index reads, causing EVM execution divergence during engine_newPayload processing.
When RocksDB is enabled for history indices (account_history_in_rocksdb, storages_history_in_rocksdb), there is a race condition between writes and reads:
- Write Path:
save_blocks()→write_blocks_data()→ pushes topending_batches - Read Path:
engine_newPayload→HistoricalStateProvider→with_rocksdb_tx()→ creates new transaction - Problem: The new read transaction only sees committed data, not pending batches
- Block N is processed via Engine API
- Block N's history indices are written to
pending_batches(not yet committed) - Block N+1 arrives via Engine API before block N's
DatabaseProvider::commit()is called - Block N+1 needs historical state from block N (parent's state)
HistoricalStateProviderlooks up history indices in RocksDB- RocksDB returns stale data (missing block N's indices)
- Lookup returns
HistoryInfo::NotYetWrittenorHistoryInfo::InPlainStateincorrectly - EVM reads wrong state values → gas divergence
- Block 24263292 failed with gas mismatch: expected 40,020,183, got 39,935,121 (difference: ~85,062 gas)
- This indicates one or more transactions executed with incorrect state
- The benchmark ran 901 blocks successfully before the divergence
- MDBX-only run did not have this issue (MDBX uses a single transaction with read-your-writes semantics)
Write side (crates/storage/provider/src/providers/rocksdb/provider.rs):
// Lines 517-560: write_blocks_data pushes to pending_batches
fn write_blocks_data<N: reth_node_types::NodePrimitives>(
&self,
blocks: &[ExecutedBlock<N>],
tx_nums: &[TxNumber],
ctx: RocksDBWriteCtx,
) -> ProviderResult<()> {
// ... writes to pending_batches, NOT committed yet
}Read side (crates/storage/provider/src/traits/rocksdb_provider.rs):
// Lines 21-33: with_rocksdb_tx creates NEW transaction
fn with_rocksdb_tx<F, R>(&self, f: F) -> ProviderResult<R> {
let rocksdb = self.rocksdb_provider();
let tx = rocksdb.tx(); // NEW transaction, only sees committed data
f(&tx)
}-
Immediate commit: Commit RocksDB batches immediately in
write_blocks_datainstead of deferring toDatabaseProvider::commit()- Pro: Simple fix
- Con: Loses atomicity with MDBX/static file commits
-
Shared transaction: Use a single RocksDB transaction per
DatabaseProviderthat is shared between reads and writes- Pro: Maintains MDBX-like semantics (read-your-writes)
- Con: Requires refactoring RocksDB provider architecture
-
Memory overlay: Cache pending history writes in memory and check memory before RocksDB reads
- Pro: No RocksDB changes needed
- Con: Additional complexity and memory usage
-
Flush before read: Ensure pending batches are committed before any historical state read
- Pro: Conceptually simple
- Con: May introduce performance overhead and ordering complexity
-
Binary builds:
reth-rocks: Built fromrocksdb-benchmark-combinedbranch with--features 'jemalloc,asm-keccak,rocksdb,edge'reth-mdbx: Built frommainwith--features 'jemalloc,asm-keccak,edge'
-
Environment:
export DATADIR=~/.local/share/reth/mainnet export JWT_SECRET=~/.local/share/reth/mainnet/jwt.hex
-
RocksDB storage settings (for
reth-rocks):reth --storage.rocksdb.transaction-hash-numbers \ --storage.rocksdb.account-history \ --storage.rocksdb.storages-history
./reth-rocks-bench new-payload-fcu \
--rpc-url https://reth-ethereum.ithaca.xyz/rpc \
--engine-rpc-url http://127.0.0.1:8551 \
--jwt-secret $JWT_SECRET \
--from <start_block> \
--to <end_block> \
--output ./benchmark-resultsnewPayloadlatency: Time forengine_newPayloadV4to returnVALID- FCU latency: Time for
engine_forkchoiceUpdatedV3to returnVALID - Ggas/s: Gigagas processed per second (gas_used / execution_time)
- Block count: Number of blocks successfully replayed before failure
| Metric | MDBX (baseline) | RocksDB |
|---|---|---|
| Blocks replayed | 767 (connection reset) | 901 (execution divergence) |
| Failure type | Network error | Invalid block (gas mismatch) |
| Execution correctness | Correct | Incorrect |
crates/storage/provider/src/providers/rocksdb/provider.rs- RocksDB write/read implementationcrates/storage/provider/src/traits/rocksdb_provider.rs-with_rocksdb_txtraitcrates/storage/provider/src/providers/state/historical.rs- Historical state lookupscrates/storage/provider/src/either_writer.rs- MDBX/RocksDB routingcrates/storage/provider/src/providers/database/provider.rs- Pending batch commit logicbin/reth-bench/src/bench/new_payload_fcu.rs- Benchmark command
The RocksDB integration has a fundamental consistency issue where pending history index writes are not visible to concurrent historical state reads. This causes EVM execution to diverge when processing blocks faster than they can be committed. A fix is required before RocksDB can be used for history indices in production.