Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
9883b71
feat(trie): add disjoint_by_keys for sorted overlays
mediocregopher Apr 16, 2026
037828f
feat(stages): add partial finish checkpoint field
mediocregopher Apr 16, 2026
b97544a
feat(stages): move partial trie progress into finish checkpoint
mediocregopher Apr 16, 2026
761acad
fix(node): unwind startup to partial trie checkpoint
mediocregopher Apr 16, 2026
e2bd518
refactor(provider): simplify save_blocks trie masking API
mediocregopher Apr 16, 2026
84b5205
docs(provider): explain save_blocks trie masking range
mediocregopher Apr 16, 2026
ca35283
fix(engine): raise persistence defaults
mediocregopher Apr 17, 2026
9e38dde
fix(provider): persist partial trie finish checkpoint
mediocregopher Apr 17, 2026
ca20cc1
fix(engine): retain partial trie suffix after persistence
mediocregopher Apr 17, 2026
c757a31
feat(engine): add dual-frontier persistence planning
mediocregopher Apr 17, 2026
be4e8cd
refactor(chain-state): derive lazy overlay anchor from blocks
mediocregopher Apr 20, 2026
cacb69a
refactor(chain-state): address lazy overlay review feedback
mediocregopher Apr 20, 2026
ebfaa6f
refactor(chain-state): cache lazy overlays by anchor
mediocregopher Apr 20, 2026
5041d55
refactor(provider): resolve lazy overlay anchors at use time
mediocregopher Apr 20, 2026
812e479
refactor(provider): separate overlay anchors from revert state
mediocregopher Apr 20, 2026
5036eb5
refactor(provider): infer overlay anchors from sources
mediocregopher Apr 20, 2026
b5ad001
refactor(provider): thread explicit requested anchors
mediocregopher Apr 20, 2026
d92ad5a
fix(provider): anchor overlay state providers by hash
mediocregopher Apr 21, 2026
134a7f3
fix(provider): pass overlay anchors via constructor
mediocregopher Apr 21, 2026
7db14d0
fix(engine): anchor state-root test overlay factory
mediocregopher Apr 21, 2026
45db5e0
fix(trie): initialize test overlay anchors
mediocregopher Apr 21, 2026
ffb0587
fix(provider): satisfy overlay lint checks
mediocregopher Apr 21, 2026
87b5240
Merge branch 'main' into mediocregopher/lazyoverlay-refactor
mediocregopher Apr 22, 2026
d5169ed
fix(engine): update sparse trie overlay factory test
mediocregopher Apr 22, 2026
c4d0949
style(engine): format sparse trie overlay test
mediocregopher Apr 22, 2026
6e8dbe3
Merge branch 'main' into mediocregopher/lazyoverlay-refactor
mediocregopher Apr 23, 2026
b60758e
fix(trie): remove unused parallel test dependency
mediocregopher Apr 24, 2026
31d0c78
fix(ci): clean bench checkouts and lock cargo builds
mediocregopher Apr 24, 2026
b6eec2e
refactor(provider): require overlay builder anchor hash
mediocregopher Apr 24, 2026
843b5a8
merge(engine): bring lazy overlay refactor into partial persistence
mediocregopher Apr 27, 2026
743d42f
fix(provider): anchor overlay state providers to trie frontier
mediocregopher Apr 27, 2026
5d40190
fix(provider): stop masking trie writes with in-memory suffix
mediocregopher Apr 27, 2026
0d19b17
fix(provider): preserve partial trie frontier across overlay and unwind
mediocregopher Apr 27, 2026
8248aa2
chore: merge origin/main
mediocregopher Apr 27, 2026
d9d3f69
fix(engine): wire deferred trie persistence config
mediocregopher Apr 27, 2026
adf2930
refactor(engine): split partial persistence frontiers
mediocregopher Apr 28, 2026
e71cf30
fix(provider): preserve masked persistence frontier state
mediocregopher Apr 28, 2026
baf6ef9
Revert "fix(provider): preserve masked persistence frontier state"
mediocregopher Apr 28, 2026
5ab335d
refactor(provider): drive save_blocks from plan steps
mediocregopher Apr 28, 2026
4b4a1b8
fix(trie): mask storage entries in disjoint merges
mediocregopher Apr 28, 2026
c07e228
fix(provider): clamp partial trie unwind during reorgs
mediocregopher Apr 29, 2026
f3e4ad7
chore(provider): add save_blocks persistence debug logging
mediocregopher Apr 30, 2026
b5c4e04
fix(trie): keep masked ancestors of unmasked nodes
mediocregopher Apr 30, 2026
411e65c
fix(trie): keep masked storage ancestors of unmasked nodes
mediocregopher Apr 30, 2026
0891a45
Merge remote-tracking branch 'origin/main' into mediocregopher/partia…
mediocregopher May 1, 2026
1598784
test: add hoodi partial persistence repro script
mediocregopher May 1, 2026
d11b02b
test: harden hoodi partial persistence repro script
mediocregopher May 1, 2026
c0a8e82
test: capture unwind trace logs in hoodi repro
mediocregopher May 1, 2026
f680db7
fix(scripts): pipe hoodi unwind traces to artifacts
mediocregopher May 1, 2026
b810c77
feat(scripts): add merkle trace touch extractor
mediocregopher May 1, 2026
02044b5
fix(scripts): use profiling binaries for hoodi repro
mediocregopher May 2, 2026
858e6c2
fix(scripts): capture full merkle trace logs
mediocregopher May 4, 2026
231b4f4
fix(trie): simplify merkle trace DB comparer
mediocregopher May 4, 2026
c7d9fd6
fix(trie): stop tracing post-drop merkle rebuild
mediocregopher May 4, 2026
3c667b1
chore(provider): improve save_blocks trie debug logging
mediocregopher May 4, 2026
ecbf6ba
fix(trie): preserve equivalent masked trie nodes
mediocregopher May 4, 2026
9040aae
fix(scripts): make Hoodi persistence repro marker detection robust
mediocregopher May 4, 2026
72b0666
fix(scripts): randomize Hoodi crash target block
mediocregopher May 4, 2026
f9e3da0
feat(trie): add walker option for matching branch children
mediocregopher May 5, 2026
3fab9bf
refactor(trie): rename walker branch child option
mediocregopher May 5, 2026
91c25f4
feat(stages): enable full changed-branch walks during merkle unwind
mediocregopher May 5, 2026
8db3ac6
fix(trie): simplify disjointed merge masking
mediocregopher May 5, 2026
eb2c8b8
Merge remote-tracking branch 'origin/main' into mediocregopher/partia…
mediocregopher May 5, 2026
a0dc39c
test(scripts): add hoodi reorg repro
mediocregopher May 5, 2026
636d8f7
chore(engine): log overlay construction details
mediocregopher May 5, 2026
92e4a15
fix(provider): revert overlays from finish frontier
mediocregopher May 6, 2026
6e597b5
fix(provider): reject overlays ahead of partial trie
mediocregopher May 6, 2026
33f55b9
chore(engine): log payload builder overlay frontiers
mediocregopher May 6, 2026
53a6b9f
fix(provider): allow lazy overlays from partial trie
mediocregopher May 6, 2026
a27db17
chore(engine): log payload builder state providers
mediocregopher May 6, 2026
4f1f8e3
chore(scripts): capture provider overlay logs
mediocregopher May 6, 2026
08e61b1
fix(scripts): narrow reorg mismatch detection
mediocregopher May 6, 2026
ad06261
chore: expand overlay repro logging
mediocregopher May 6, 2026
6e574bf
chore: add Hoodi reorg diagnostics
mediocregopher May 7, 2026
70f157c
Merge origin/main into pr-23592
mediocregopher May 7, 2026
eb3f527
fix(reth-bench): preserve replay payload topology
mediocregopher May 8, 2026
3327f0d
chore: replay Hoodi reorg artifacts in repro script
mediocregopher May 8, 2026
694b6b4
fix(engine): catch up trie state before disk reorg removal
mediocregopher May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bin/reth-bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Both `new-payload-fcu` and `new-payload-only` support `--rpc-block-fetch-retries
to control how many times block fetches are retried after an RPC failure. The default is `10`.
Use `--rpc-block-fetch-retries forever` to keep retrying indefinitely.

When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold (2). This can be customized with `--persistence-threshold <N>`.
When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold. This can be customized with `--persistence-threshold <N>`.

By default, the WebSocket URL for persistence subscriptions is derived from `--engine-rpc-url` (converting to ws:// on port 8546). Use `--ws-rpc-url` to override this.

Expand Down
104 changes: 94 additions & 10 deletions bin/reth-bench/src/bench/new_payload_fcu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,17 @@ use alloy_provider::{
network::{AnyNetwork, AnyRpcBlock},
Provider, RootProvider,
};
use alloy_rpc_types_engine::{
ExecutionData, ExecutionPayloadEnvelopeV5, ForkchoiceState, PayloadAttributes,
};
use alloy_rpc_types_engine::{ExecutionData, ForkchoiceState, PayloadAttributes};
use clap::Parser;
use eyre::{bail, ensure, Context, OptionExt};
use futures::{stream, Stream, StreamExt, TryStreamExt};
use reth_cli_runner::CliContext;
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
use reth_node_api::{EngineApiMessageVersion, ExecutionPayload};
use reth_node_core::args::{BenchmarkArgs, WaitForPersistence};
use reth_rpc_api::{RethNewPayloadInput, TestingBuildBlockRequestV1};
use reth_rpc_api::{RethNewPayloadInput, TestingBuildBlockRequestV1, TestingBuildBlockResponseV1};
use std::{
path::Path,
pin::Pin,
time::{Duration, Instant},
};
Expand Down Expand Up @@ -189,6 +188,7 @@ impl Command {
if let Some(depth) = self.reorg {
info!(target: "reth-bench", depth, "Using testing_buildBlockV1 reorg mode");
}
let output_dir = self.benchmark.output.clone();

let BenchContext {
benchmark_mode,
Expand All @@ -213,13 +213,15 @@ impl Command {
let buffer_size = self.rpc_block_buffer_size;
let provider = block_provider.clone();
let bench_mode = benchmark_mode.clone();
let artifact_output_dir = output_dir.clone();
let mut blocks: Pin<Box<dyn Stream<Item = eyre::Result<Payload>> + Send>> = Box::pin(
stream::iter((next_block..)
.take_while(move |next_block| {
bench_mode.contains(*next_block)
}))
.map(move |next_block| {
let block_provider = provider.clone();
let artifact_output_dir = artifact_output_dir.clone();
async move {
let block_res = block_provider
.get_block_by_number(next_block.into())
Expand All @@ -238,10 +240,47 @@ impl Command {
};


let bal = if !rlp_blocks &&
(block.header.block_access_list_hash.is_some() || self.enable_bal)
{
Some(fetch_block_access_list(&block_provider, block.header.number).await?)
let fetched_bal = if !rlp_blocks {
match fetch_block_access_list(&block_provider, block.header.number).await {
Ok(bal) => {
write_bal_artifact(
artifact_output_dir.as_deref(),
"real",
block.header.number,
block.header.hash,
Some(&bal),
)?;
Some(bal)
}
Err(err) => {
warn!(
target: "reth-bench",
block_number = block.header.number,
block_hash = %block.header.hash,
%err,
"Failed to fetch real block BAL artifact"
);
if is_unsupported_bal_rpc_error(&err) {
warn!(
target: "reth-bench",
"Remote RPC does not support BAL fetching; writing null real-block BAL artifact"
);
}
write_bal_artifact(
artifact_output_dir.as_deref(),
"real",
block.header.number,
block.header.hash,
None,
)?;
None
}
}
} else {
None
};
let bal = if block.header.block_access_list_hash.is_some() || self.enable_bal {
fetched_bal
} else {
None
};
Expand Down Expand Up @@ -404,6 +443,7 @@ impl Command {
.ok_or_eyre("missing deferred fork block for reorg branch start")?,
canonical_parent_hash,
no_wait_for_caches,
output_dir.as_deref(),
)
.await?,
});
Expand Down Expand Up @@ -448,6 +488,7 @@ impl Command {
next_fork_block_number,
Some(prepared.block_hash),
no_wait_for_caches,
output_dir.as_deref(),
)
.await?;
} else {
Expand Down Expand Up @@ -534,12 +575,13 @@ async fn prepare_built_block(
block: &AnyRpcBlock,
parent_block_hash: B256,
no_wait_for_caches: bool,
output_dir: Option<&Path>,
) -> eyre::Result<PreparedBuiltBlock> {
const MAX_BUILD_ATTEMPTS: usize = 10;
const BUILD_RETRY_INTERVAL: Duration = Duration::from_millis(100);

let request = build_block_request(block, parent_block_hash)?;
let built_payload: ExecutionPayloadEnvelopeV5 = {
let built_response: TestingBuildBlockResponseV1 = {
let mut attempts_remaining = MAX_BUILD_ATTEMPTS;

loop {
Expand Down Expand Up @@ -569,8 +611,16 @@ async fn prepare_built_block(
}
};

let built_payload = built_response.execution_payload_envelope;
let payload = &built_payload.execution_payload.payload_inner.payload_inner;
let block_hash = payload.block_hash;
write_bal_artifact(
output_dir,
"fork",
payload.block_number,
block_hash,
built_response.block_access_list.as_ref(),
)?;
let (payload, sidecar) = built_payload
.into_payload_and_sidecar(block.header.parent_beacon_block_root.unwrap_or_default());
// Fork payloads are built immediately before the next `testing_buildBlockV1` call. Leaving
Expand All @@ -593,9 +643,10 @@ async fn queue_fork_block(
block_number: u64,
parent_block_hash: Option<B256>,
no_wait_for_caches: bool,
output_dir: Option<&Path>,
) -> eyre::Result<Option<QueuedForkBlock>> {
if !benchmark_mode.contains(block_number) {
return Ok(None)
return Ok(None);
}

let future_block = block_provider
Expand All @@ -613,17 +664,50 @@ async fn queue_fork_block(
&future_block,
parent_block_hash,
no_wait_for_caches,
output_dir,
)
.await?,
}))
}

fn write_bal_artifact(
output_dir: Option<&Path>,
kind: &str,
block_number: u64,
block_hash: B256,
block_access_list: Option<&BlockAccessList>,
) -> eyre::Result<()> {
let Some(output_dir) = output_dir else { return Ok(()) };

let bal_dir = output_dir.join("block-access-lists");
std::fs::create_dir_all(&bal_dir)?;
let path = bal_dir.join(format!("bal-{kind}-{block_number}-{block_hash}.json"));
let value = serde_json::json!({
"kind": kind,
"blockNumber": block_number,
"blockHash": block_hash,
"blockAccessList": block_access_list,
});
let file = std::fs::File::create(&path)?;
serde_json::to_writer_pretty(file, &value)?;
debug!(target: "reth-bench", %kind, block_number, %block_hash, path = %path.display(), "Wrote BAL artifact");
Ok(())
}

fn is_retryable_build_block_error(err: &alloy_transport::TransportError) -> bool {
let message = err.to_string();
message.contains("block not found: hash") ||
message.contains("block hash not found for block number")
}

fn is_unsupported_bal_rpc_error(err: &eyre::Report) -> bool {
let message = err.to_string();
message.contains("method ignored") ||
message.contains("Method not found") ||
message.contains("method not found") ||
message.contains("-32601")
}

fn build_block_request(
block: &AnyRpcBlock,
parent_block_hash: B256,
Expand Down
31 changes: 18 additions & 13 deletions bin/reth-bench/src/bench/replay_payloads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use reth_node_api::EngineApiMessageVersion;
use reth_node_core::args::WaitForPersistence;
use reth_rpc_api::RethNewPayloadInput;
use std::{
collections::HashMap,
path::PathBuf,
time::{Duration, Instant},
};
Expand Down Expand Up @@ -228,14 +229,15 @@ impl Command {
);
}

let mut parent_hash = initial_parent_hash;
let mut replayed_hashes = HashMap::from([(initial_parent_hash, initial_parent_hash)]);

let mut results = Vec::new();
let total_benchmark_duration = Instant::now();

for (i, payload) in payloads.iter().enumerate() {
let execution_data = &payload.execution_data;
let mut block_hash = payload.block_hash;
let original_block_hash = block_hash;
let v1 = execution_data.payload.as_v1();

let gas_used = v1.gas_used;
Expand Down Expand Up @@ -274,11 +276,16 @@ impl Command {
.unwrap_or(WaitForPersistence::Never)
.rpc_value(block_number);

// Inject sidecar BAL into the inline V4 payload field when --bal is set.
// If the payload is not already V4 we upgrade it (V3→V4) so the BAL
// can be carried inline. This changes the block hash, so we recompute
// it and patch parent_hash to maintain the chain.
let mut execution_data = execution_data.clone();
let original_parent_hash = execution_data.payload.as_v1().parent_hash;
let mut payload_modified = false;
if let Some(remapped_parent_hash) = replayed_hashes.get(&original_parent_hash) {
if *remapped_parent_hash != original_parent_hash {
execution_data.payload.as_v1_mut().parent_hash = *remapped_parent_hash;
payload_modified = true;
}
}

if self.bal &&
let Some(bal) = &payload.block_access_list
{
Expand All @@ -292,12 +299,10 @@ impl Command {
execution_data.payload.as_v4_mut().unwrap().block_access_list = encoded_bal;
}

// Patch parent_hash so this block chains off the (possibly
// rehashed) previous block.
execution_data.payload.as_v1_mut().parent_hash = parent_hash;
payload_modified = true;
}

// Recompute block hash after payload modification and update
// the hash stored in the payload itself.
if payload_modified {
block_hash = compute_payload_block_hash(&execution_data)?;
execution_data.payload.as_v1_mut().block_hash = block_hash;
}
Expand Down Expand Up @@ -349,8 +354,8 @@ impl Command {

let fcu_state = ForkchoiceState {
head_block_hash: block_hash,
safe_block_hash: parent_hash,
finalized_block_hash: parent_hash,
safe_block_hash: initial_parent_hash,
finalized_block_hash: initial_parent_hash,
};

let fcu_start = Instant::now();
Expand Down Expand Up @@ -390,7 +395,7 @@ impl Command {
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
results.push((gas_row, combined_result));

parent_hash = block_hash;
replayed_hashes.insert(original_block_hash, block_hash);
}

let (gas_output_results, combined_results): (Vec<TotalGasRow>, Vec<CombinedResult>) =
Expand Down
20 changes: 16 additions & 4 deletions crates/chain-state/src/in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,19 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
/// This will update the links between blocks and remove all blocks that are [..
/// `persisted_height`].
pub fn remove_persisted_blocks(&self, persisted_num_hash: BlockNumHash) {
self.remove_persisted_blocks_until(persisted_num_hash, persisted_num_hash.number);
}

/// Removes blocks from the in-memory state through `remove_until` while still reporting the
/// provided block as the persisted tip.
///
/// This is used when block bodies/plain state have been persisted further than trie data, so a
/// suffix still needs to remain in memory for trie-backed operations.
pub fn remove_persisted_blocks_until(
&self,
persisted_num_hash: BlockNumHash,
remove_until: BlockNumber,
) {
self.set_persisted(persisted_num_hash);
Comment thread
mediocregopher marked this conversation as resolved.
// if the persisted hash is not in the canonical in memory state, do nothing, because it
// means canonical blocks were not actually persisted.
Expand All @@ -337,16 +350,15 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
let mut numbers = self.inner.in_memory_state.numbers.write();
let mut blocks = self.inner.in_memory_state.blocks.write();

let BlockNumHash { number: persisted_height, hash: _ } = persisted_num_hash;
let remove_until = remove_until.min(persisted_num_hash.number);

// clear all numbers
numbers.clear();

// drain all blocks and only keep the ones that are not persisted (below the persisted
// height)
// Drain all blocks and keep only the suffix that still has to stay in memory.
let mut old_blocks = blocks
.drain()
.filter(|(_, b)| b.block_ref().recovered_block().number() > persisted_height)
.filter(|(_, b)| b.block_ref().recovered_block().number() > remove_until)
.map(|(_, b)| b.block.clone())
.collect::<Vec<_>>();

Expand Down
Loading