diff --git a/Cargo.lock b/Cargo.lock
index 17fc34ea..eb8944df 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -972,6 +972,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
[[package]]
name = "anstream"
version = "0.6.21"
@@ -2142,6 +2148,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
[[package]]
name = "castaway"
version = "0.2.4"
@@ -2237,6 +2249,33 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
[[package]]
name = "cipher"
version = "0.4.4"
@@ -2556,6 +2595,42 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools 0.10.5",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools 0.10.5",
+]
+
[[package]]
name = "critical-section"
version = "1.2.0"
@@ -4016,6 +4091,17 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
[[package]]
name = "hash-db"
version = "0.15.2"
@@ -4780,6 +4866,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -6337,6 +6434,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
[[package]]
name = "op-alloy"
version = "0.23.1"
@@ -6875,6 +6978,34 @@ dependencies = [
"crunchy",
]
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
[[package]]
name = "polling"
version = "3.11.0"
@@ -12252,6 +12383,16 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "tinyvec"
version = "1.10.0"
@@ -13996,6 +14137,7 @@ dependencies = [
"async-trait",
"chrono",
"clap",
+ "criterion",
"ctor",
"dashmap 6.1.0",
"derive_more",
diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml
index ad452548..39f4e8a6 100644
--- a/crates/builder/Cargo.toml
+++ b/crates/builder/Cargo.toml
@@ -154,6 +154,7 @@ ctor = "0.4.2"
hyper = { version = "1.7.0", features = ["http1"] }
hyper-util = { version = "0.1.11" }
http-body-util = { version = "0.1.3" }
+criterion = { version = "0.5", features = ["html_reports"] }
macros = { path = "src/tests/framework/macros" }
nanoid = { version = "0.4" }
reth-ipc.workspace = true
@@ -178,3 +179,7 @@ testing = [
interop = []
telemetry = ["reth-tracing-otlp", "opentelemetry"]
+
+[[bench]]
+name = "bench_flashblocks_state_root"
+harness = false
diff --git a/crates/builder/benches/bench_flashblocks_state_root.rs b/crates/builder/benches/bench_flashblocks_state_root.rs
new file mode 100644
index 00000000..f2c21243
--- /dev/null
+++ b/crates/builder/benches/bench_flashblocks_state_root.rs
@@ -0,0 +1,311 @@
+//! Benchmark comparing flashblocks state root calculation with and without incremental trie caching.
+//!
+//! This benchmark simulates building 10 sequential flashblocks, measuring the total time
+//! spent in state root calculation. It compares:
+//! - Without cache: Full state root calculation from database each time
+//! - With cache: Incremental state root using cached trie nodes from previous flashblock
+//!
+//! Run with:
+//! ```
+//! cargo bench -p op-rbuilder --bench bench_flashblocks_state_root
+//! ```
+
+use alloy_primitives::{Address, B256, U256, keccak256};
+use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
+use rand::{Rng, SeedableRng, rngs::StdRng};
+use reth_chainspec::MAINNET;
+use reth_primitives_traits::Account;
+use reth_provider::{
+ DatabaseProviderFactory, HashingWriter, StateRootProvider,
+ test_utils::create_test_provider_factory_with_chain_spec,
+};
+use reth_trie::{HashedPostState, HashedStorage, TrieInput};
+use std::{collections::HashMap, time::Instant};
+
+const SEED: u64 = 42;
+
+/// Generate random accounts and storage for initial database state
+fn generate_test_data(
+ num_accounts: usize,
+ storage_per_account: usize,
+ seed: u64,
+) -> (Vec<(Address, Account)>, HashMap
>) {
+ let mut rng = StdRng::seed_from_u64(seed);
+ let mut accounts = Vec::with_capacity(num_accounts);
+ let mut storage = HashMap::new();
+
+ for _ in 0..num_accounts {
+ let mut addr_bytes = [0u8; 20];
+ rng.fill(&mut addr_bytes);
+ let address = Address::from_slice(&addr_bytes);
+
+ let account = Account {
+ nonce: rng.random_range(0..1000),
+ balance: U256::from(rng.random_range(0u64..1_000_000)),
+ bytecode_hash: if rng.random_bool(0.3) {
+ let mut hash = [0u8; 32];
+ rng.fill(&mut hash);
+ Some(B256::from(hash))
+ } else {
+ None
+ },
+ };
+ accounts.push((address, account));
+
+ // Generate storage for accounts
+ if storage_per_account > 0 && rng.random_bool(0.5) {
+ let mut slots = Vec::with_capacity(storage_per_account);
+ for _ in 0..storage_per_account {
+ let mut key = [0u8; 32];
+ rng.fill(&mut key);
+ let value = U256::from(rng.random_range(1u64..1_000_000));
+ slots.push((B256::from(key), value));
+ }
+ storage.insert(address, slots);
+ }
+ }
+
+ (accounts, storage)
+}
+
+/// Setup test database with initial state
+fn setup_database(
+ accounts: &[(Address, Account)],
+ storage: &HashMap>,
+) -> reth_provider::providers::ProviderFactory {
+ let provider_factory = create_test_provider_factory_with_chain_spec(MAINNET.clone());
+
+ {
+ let provider_rw = provider_factory.provider_rw().unwrap();
+
+ // Insert accounts
+ let accounts_iter = accounts.iter().map(|(addr, acc)| (*addr, Some(*acc)));
+ provider_rw
+ .insert_account_for_hashing(accounts_iter)
+ .unwrap();
+
+ // Insert storage
+ let storage_entries: Vec<_> = storage
+ .iter()
+ .map(|(addr, slots)| {
+ let entries: Vec<_> = slots
+ .iter()
+ .map(|(key, value)| reth_primitives_traits::StorageEntry {
+ key: *key,
+ value: *value,
+ })
+ .collect();
+ (*addr, entries)
+ })
+ .collect();
+ provider_rw
+ .insert_storage_for_hashing(storage_entries)
+ .unwrap();
+
+ provider_rw.commit().unwrap();
+ }
+
+ provider_factory
+}
+
+/// Generate a flashblock's worth of state changes
+fn generate_flashblock_changes(
+ base_accounts: &[(Address, Account)],
+ change_size: usize,
+ seed: u64,
+) -> (Vec<(Address, Account)>, HashMap>) {
+ let mut rng = StdRng::seed_from_u64(seed);
+ let mut accounts = Vec::with_capacity(change_size);
+ let mut storage = HashMap::new();
+
+ for i in 0..change_size {
+ // Mix of existing and new addresses (70% existing, 30% new)
+ let address = if i < base_accounts.len() && rng.random_bool(0.7) {
+ base_accounts[rng.random_range(0..base_accounts.len())].0
+ } else {
+ let mut addr_bytes = [0u8; 20];
+ rng.fill(&mut addr_bytes);
+ Address::from_slice(&addr_bytes)
+ };
+
+ let account = Account {
+ nonce: rng.random_range(1000..2000),
+ balance: U256::from(rng.random_range(1_000_000u64..2_000_000)),
+ bytecode_hash: None,
+ };
+ accounts.push((address, account));
+
+ // Add some storage updates (30% of accounts)
+ if rng.random_bool(0.3) {
+ let mut slots = Vec::new();
+ for _ in 0..rng.random_range(1..10) {
+ let mut key = [0u8; 32];
+ rng.fill(&mut key);
+ let value = U256::from(rng.random_range(1u64..1_000_000));
+ slots.push((B256::from(key), value));
+ }
+ storage.insert(address, slots);
+ }
+ }
+
+ (accounts, storage)
+}
+
+/// Convert to HashedPostState for state root calculation
+fn to_hashed_post_state(
+ accounts: &[(Address, Account)],
+ storage: &HashMap>,
+) -> HashedPostState {
+ let hashed_accounts: Vec<_> = accounts
+ .iter()
+ .map(|(addr, acc)| (keccak256(addr), Some(*acc)))
+ .collect();
+
+ let mut hashed_storages = alloy_primitives::map::HashMap::default();
+ for (addr, slots) in storage {
+ let hashed_addr = keccak256(addr);
+ let hashed_storage = HashedStorage::from_iter(
+ false,
+ slots.iter().map(|(key, value)| (keccak256(key), *value)),
+ );
+ hashed_storages.insert(hashed_addr, hashed_storage);
+ }
+
+ HashedPostState {
+ accounts: hashed_accounts.into_iter().collect(),
+ storages: hashed_storages,
+ }
+}
+
+/// Benchmark without incremental trie cache (baseline)
+fn bench_without_cache(
+ provider_factory: &reth_provider::providers::ProviderFactory<
+ reth_provider::test_utils::MockNodeTypesWithDB,
+ >,
+ flashblock_changes: &[HashedPostState],
+) -> (u128, Vec) {
+ let mut individual_times = Vec::new();
+ let total_start = Instant::now();
+
+ for hashed_state in flashblock_changes {
+ let fb_start = Instant::now();
+ let provider = provider_factory.database_provider_ro().unwrap();
+ let latest = reth_provider::LatestStateProvider::new(provider);
+ let _ = black_box(
+ latest
+ .state_root_with_updates(hashed_state.clone())
+ .unwrap(),
+ );
+ individual_times.push(fb_start.elapsed().as_micros());
+ }
+
+ (total_start.elapsed().as_micros(), individual_times)
+}
+
+/// Benchmark with incremental trie cache (optimized)
+fn bench_with_cache(
+ provider_factory: &reth_provider::providers::ProviderFactory<
+ reth_provider::test_utils::MockNodeTypesWithDB,
+ >,
+ flashblock_changes: &[HashedPostState],
+) -> (u128, Vec) {
+ let mut individual_times = Vec::new();
+ let mut prev_trie_updates = None;
+ let total_start = Instant::now();
+
+ for (i, hashed_state) in flashblock_changes.iter().enumerate() {
+ let fb_start = Instant::now();
+ let provider = provider_factory.database_provider_ro().unwrap();
+
+ let (state_root, trie_output) = if i == 0 || prev_trie_updates.is_none() {
+ // First flashblock: full calculation
+ let latest = reth_provider::LatestStateProvider::new(provider);
+ latest
+ .state_root_with_updates(hashed_state.clone())
+ .unwrap()
+ } else {
+ // Subsequent flashblocks: incremental calculation
+ // Use state_root_from_nodes_with_updates from StateRootProvider trait
+ let trie_input = TrieInput::new(
+ prev_trie_updates.clone().unwrap(),
+ hashed_state.clone(),
+ hashed_state.construct_prefix_sets(),
+ );
+
+ let latest = reth_provider::LatestStateProvider::new(provider);
+ latest
+ .state_root_from_nodes_with_updates(trie_input)
+ .unwrap()
+ };
+
+ prev_trie_updates = Some(trie_output);
+ individual_times.push(fb_start.elapsed().as_micros());
+
+ // Use the result
+ black_box(state_root);
+ }
+
+ (total_start.elapsed().as_micros(), individual_times)
+}
+
+fn bench_flashblocks_state_root(c: &mut Criterion) {
+ // Setup: Create a large database with 50k accounts, 10 storage slots each
+ println!("\n=== Setting up database with 50,000 accounts...");
+ let (base_accounts, base_storage) = generate_test_data(50_000, 10, SEED);
+ let provider_factory = setup_database(&base_accounts, &base_storage);
+ println!("â
Database setup complete\n");
+
+ // Test different flashblock sizes (transactions per flashblock)
+ for txs_per_flashblock in [50, 100, 200] {
+ let mut group = c.benchmark_group(format!("flashblocks_{}_txs", txs_per_flashblock));
+ group.sample_size(10);
+
+ println!(
+ "--- Testing with {} transactions per flashblock ---",
+ txs_per_flashblock
+ );
+
+ // Generate 10 flashblocks worth of changes
+ let mut flashblock_changes = Vec::new();
+ for i in 0..10 {
+ let (accounts, storage) =
+ generate_flashblock_changes(&base_accounts, txs_per_flashblock, SEED + i + 1);
+ let hashed_state = to_hashed_post_state(&accounts, &storage);
+ flashblock_changes.push(hashed_state);
+ }
+
+ // Benchmark without cache (baseline)
+ group.bench_function(BenchmarkId::new("without_cache", "10_flashblocks"), |b| {
+ b.iter(|| bench_without_cache(&provider_factory, &flashblock_changes))
+ });
+
+ // Benchmark with cache (optimized)
+ group.bench_function(BenchmarkId::new("with_cache", "10_flashblocks"), |b| {
+ b.iter(|| bench_with_cache(&provider_factory, &flashblock_changes))
+ });
+
+ // Manual comparison run for detailed output
+ println!("\nð Manual timing comparison:");
+ let (total_without, times_without) =
+ bench_without_cache(&provider_factory, &flashblock_changes);
+ println!(" WITHOUT cache: {} Ξs total", total_without);
+ println!(" Per-flashblock: {:?} Ξs", times_without);
+
+ let (total_with, times_with) = bench_with_cache(&provider_factory, &flashblock_changes);
+ println!(" WITH cache: {} Ξs total", total_with);
+ println!(" Per-flashblock: {:?} Ξs", times_with);
+
+ let speedup = total_without as f64 / total_with as f64;
+ let improvement = ((total_without - total_with) as f64 / total_without as f64) * 100.0;
+ println!(" ⥠Speedup: {:.2}x ({:.1}% faster)", speedup, improvement);
+ println!();
+
+ group.finish();
+ }
+
+ println!("\n=== Benchmark complete! ===");
+ println!("Results saved to target/criterion/");
+}
+
+criterion_group!(benches, bench_flashblocks_state_root);
+criterion_main!(benches);
diff --git a/crates/builder/src/args/op.rs b/crates/builder/src/args/op.rs
index 722dcf87..0d80af29 100644
--- a/crates/builder/src/args/op.rs
+++ b/crates/builder/src/args/op.rs
@@ -136,6 +136,26 @@ pub struct FlashblocksArgs {
)]
pub flashblocks_disable_async_calculate_state_root: bool,
+ /// Enable async trie precalculation during flashblock building.
+ /// When enabled and disable_state_root is true, background trie calculations
+ /// are spawned after each flashblock to speed up final state root resolution.
+ #[arg(
+ long = "flashblocks.enable-async-trie-precalc",
+ default_value = "false",
+ env = "FLASHBLOCKS_ENABLE_ASYNC_TRIE_PRECALC"
+ )]
+ pub flashblocks_enable_async_trie_precalc: bool,
+
+ /// Which flashblock index to start async trie precalculation from (0-indexed).
+ /// For example, with 5 flashblocks and start=2, precalculation begins after
+ /// flashblock 2 (skipping 0 and 1).
+ #[arg(
+ long = "flashblocks.async-trie-precalc-start-flashblock",
+ default_value = "1",
+ env = "FLASHBLOCKS_ASYNC_TRIE_PRECALC_START_FLASHBLOCK"
+ )]
+ pub flashblocks_async_trie_precalc_start_flashblock: u64,
+
/// Flashblocks number contract address
///
/// This is the address of the contract that will be used to increment the flashblock number.
@@ -187,6 +207,7 @@ pub struct FlashblocksArgs {
default_value = "256"
)]
pub ws_subscriber_limit: Option,
+
}
impl Default for FlashblocksArgs {
diff --git a/crates/builder/src/builders/flashblocks/config.rs b/crates/builder/src/builders/flashblocks/config.rs
index 8fe2ad49..0ae88030 100644
--- a/crates/builder/src/builders/flashblocks/config.rs
+++ b/crates/builder/src/builders/flashblocks/config.rs
@@ -66,6 +66,14 @@ pub struct FlashblocksConfig {
/// Maximum number of concurrent WebSocket subscribers
pub ws_subscriber_limit: Option,
+
+ /// Enable async trie precalculation during flashblock building.
+ /// When enabled and disable_state_root is true, background trie calculations
+ /// are spawned after each flashblock to speed up final state root resolution.
+ pub enable_async_trie_precalc: bool,
+
+ /// Which flashblock index to start async trie precalculation from (0-indexed).
+ pub async_trie_precalc_start_flashblock: u64,
}
impl Default for FlashblocksConfig {
@@ -88,6 +96,8 @@ impl Default for FlashblocksConfig {
p2p_send_full_payload: false,
p2p_process_full_payload: false,
ws_subscriber_limit: None,
+ enable_async_trie_precalc: false,
+ async_trie_precalc_start_flashblock: 1,
}
}
}
@@ -132,6 +142,10 @@ impl TryFrom for FlashblocksConfig {
p2p_send_full_payload: args.flashblocks.p2p.p2p_send_full_payload,
p2p_process_full_payload: args.flashblocks.p2p.p2p_process_full_payload,
ws_subscriber_limit: args.flashblocks.ws_subscriber_limit,
+ enable_async_trie_precalc: args.flashblocks.flashblocks_enable_async_trie_precalc,
+ async_trie_precalc_start_flashblock: args
+ .flashblocks
+ .flashblocks_async_trie_precalc_start_flashblock,
})
}
}
diff --git a/crates/builder/src/builders/flashblocks/payload.rs b/crates/builder/src/builders/flashblocks/payload.rs
index 637d6b4f..a5560562 100644
--- a/crates/builder/src/builders/flashblocks/payload.rs
+++ b/crates/builder/src/builders/flashblocks/payload.rs
@@ -51,7 +51,7 @@ use reth_revm::{
State,
};
use reth_transaction_pool::TransactionPool;
-use reth_trie::{updates::TrieUpdates, HashedPostState};
+use reth_trie::{updates::TrieUpdates, HashedPostState, TrieInput};
use revm::Database;
use std::{collections::BTreeMap, sync::Arc, time::Instant};
use tokio::sync::mpsc;
@@ -91,6 +91,10 @@ type NextBestFlashblocksTxs = BestFlashblocksTxs<
pub(super) struct FlashblocksExecutionInfo {
/// Index of the last consumed flashblock
last_flashblock_index: usize,
+
+ /// Cached trie updates from previous flashblock for incremental state root calculation.
+ /// None only for the first flashblock; populated after each subsequent state root calculation.
+ prev_trie_updates: Option>,
}
#[derive(Debug, Default, Clone)]
@@ -134,6 +138,39 @@ impl FlashblocksExtraCtx {
}
}
+/// Result of an async trie precalculation for a single flashblock.
+#[derive(Debug)]
+struct TriePrecalcResult {
+ /// The flashblock index this result corresponds to.
+ flashblock_index: u64,
+ /// The computed state root for this flashblock's cumulative state.
+ state_root: B256,
+ /// The computed trie updates.
+ trie_updates: Arc,
+ /// The hashed post state at the time of precalculation.
+ hashed_state: HashedPostState,
+}
+
+/// Work item sent from the main flashblock loop to the background trie worker.
+#[derive(Debug)]
+struct TriePrecalcWorkItem {
+ flashblock_index: u64,
+ bundle_state: Arc,
+}
+
+/// Manages the async trie precalculation pipeline.
+///
+/// A background worker computes incremental trie updates sequentially.
+/// Each computation uses the previous one's `TrieUpdates` to maintain
+/// an incremental chain. Results are collected here and used during
+/// final state root resolution.
+struct AsyncTriePrecalcPipeline {
+ /// Receiver for completed precalculation results from the background worker.
+ result_rx: std::sync::mpsc::Receiver,
+ /// Sender for providing BundleState snapshots to the background worker.
+ work_tx: std::sync::mpsc::SyncSender,
+}
+
impl OpPayloadBuilderCtx {
/// Returns the current flashblock index
pub(crate) fn flashblock_index(&self) -> u64 {
@@ -416,7 +453,7 @@ where
.try_send(fb_payload.clone())
.map_err(PayloadBuilderError::other)?;
}
- let mut best_payload = (fallback_payload.clone(), bundle_state);
+ let mut best_payload = (fallback_payload.clone(), Arc::new(bundle_state));
info!(
target: "payload_builder",
@@ -452,7 +489,7 @@ where
ctx.metrics.payload_num_tx_gauge.set(info.executed_transactions.len() as f64);
// return early since we don't need to build a block with transactions from the pool
- self.resolve_best_payload(&ctx, best_payload, fallback_payload, &resolve_payload);
+ self.resolve_best_payload(&ctx, best_payload, fallback_payload, &resolve_payload, None);
return Ok(());
}
@@ -527,6 +564,39 @@ where
fb_payload.payload_id,
)));
+ // Initialize async trie precalculation pipeline if enabled
+ let mut precalc_pipeline: Option = if disable_state_root
+ && self.config.specific.enable_async_trie_precalc
+ {
+ match self.client.state_by_block_hash(ctx.parent().hash()) {
+ Ok(worker_state_provider) => {
+ let (work_tx, work_rx) =
+ std::sync::mpsc::sync_channel((expected_flashblocks + 1) as usize);
+ let (result_tx, result_rx) =
+ std::sync::mpsc::sync_channel((expected_flashblocks + 1) as usize);
+ let metrics = self.metrics.clone();
+ self.task_executor.spawn_blocking(Box::pin(async move {
+ run_trie_precalc_worker(work_rx, result_tx, worker_state_provider, metrics);
+ }));
+ info!(
+ target: "payload_builder",
+ "Async trie precalculation pipeline started"
+ );
+ Some(AsyncTriePrecalcPipeline { result_rx, work_tx })
+ }
+ Err(err) => {
+ warn!(
+ target: "payload_builder",
+ error = %err,
+ "Failed to create state provider for async trie precalc, disabling"
+ );
+ None
+ }
+ }
+ } else {
+ None
+ };
+
// Process flashblocks - block on async channel receive
loop {
// Wait for signal before building flashblock.
@@ -541,7 +611,13 @@ where
ctx = ctx.with_cancel(new_fb_cancel);
} else {
// Channel closed - block building cancelled
- self.resolve_best_payload(&ctx, best_payload, fallback_payload, &resolve_payload);
+ self.resolve_best_payload(
+ &ctx,
+ best_payload,
+ fallback_payload,
+ &resolve_payload,
+ precalc_pipeline.take(),
+ );
self.record_flashblocks_metrics(&ctx, &info, target_flashblocks, &span);
return Ok(());
}
@@ -574,6 +650,7 @@ where
best_payload,
fallback_payload,
&resolve_payload,
+ precalc_pipeline.take(),
);
self.record_flashblocks_metrics(&ctx, &info, target_flashblocks, &span);
return Ok(());
@@ -592,11 +669,46 @@ where
best_payload,
fallback_payload,
&resolve_payload,
+ precalc_pipeline.take(),
);
return Err(PayloadBuilderError::Other(err.into()));
}
};
+ // Feed work item to async trie precalc pipeline
+ if let Some(pipeline) = &precalc_pipeline {
+ let fb_index = ctx.flashblock_index();
+ if fb_index >= self.config.specific.async_trie_precalc_start_flashblock {
+ match pipeline.work_tx.try_send(TriePrecalcWorkItem {
+ flashblock_index: fb_index,
+ bundle_state: best_payload.1.clone(),
+ }) {
+ Ok(()) => {
+ debug!(
+ target: "payload_builder",
+ flashblock_index = fb_index,
+ "Sent work item to async trie precalc pipeline"
+ );
+ }
+ Err(std::sync::mpsc::TrySendError::Full(_)) => {
+ warn!(
+ target: "payload_builder",
+ flashblock_index = fb_index,
+ "Async trie precalc pipeline full, skipping"
+ );
+ }
+ Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {
+ warn!(
+ target: "payload_builder",
+ flashblock_index = fb_index,
+ "Async trie precalc worker disconnected"
+ );
+ precalc_pipeline = None;
+ }
+ }
+ }
+ }
+
ctx = ctx.with_extra_ctx(next_flashblocks_ctx);
}
}
@@ -613,7 +725,7 @@ where
state_provider: impl reth::providers::StateProvider + Clone,
best_txs: &mut NextBestFlashblocksTxs,
block_cancel: &CancellationToken,
- best_payload: &mut (OpBuiltPayload, BundleState),
+ best_payload: &mut (OpBuiltPayload, Arc),
) -> eyre::Result