From c55168ea46a8cad97f948256099c536e4904dd0b Mon Sep 17 00:00:00 2001 From: Vui-Chee Date: Sat, 28 Feb 2026 20:43:22 +0800 Subject: [PATCH 1/2] feat(builder): port incremental trie cache optimization for flashblocks state root Port flashbots/op-rbuilder#385 - caches TrieUpdates after each flashblock state root calculation so subsequent flashblocks reuse cached trie nodes instead of recomputing from the database each time, achieving ~2.4-2.5x speedup in state root calculation. Changes: - Add `prev_trie_updates: Option>` field to FlashblocksState - Use `state_root_from_nodes_with_updates` with cached trie on subsequent flashblocks - Cache trie updates after each calculation and propagate to next flashblock - Add criterion benchmark: bench_flashblocks_state_root - Add reth-trie-db and criterion workspace dependencies Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 143 ++++++++ Cargo.toml | 2 + crates/builder/Cargo.toml | 6 + .../benches/bench_flashblocks_state_root.rs | 310 ++++++++++++++++++ .../src/payload/flashblocks/payload.rs | 58 +++- 5 files changed, 515 insertions(+), 4 deletions(-) create mode 100644 crates/builder/benches/bench_flashblocks_state_root.rs diff --git a/Cargo.lock b/Cargo.lock index 986152df..8ea862a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,6 +969,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" @@ -2139,6 +2145,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" @@ -2234,6 +2246,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" @@ -2553,6 +2592,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" @@ -4013,6 +4088,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" @@ -4777,6 +4863,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" @@ -6333,6 +6430,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" @@ -6871,6 +6974,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" @@ -12235,6 +12366,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" @@ -13966,6 +14107,7 @@ dependencies = [ "async-trait", "chrono", "clap", + "criterion", "ctor", "dashmap 6.1.0", "derive_more", @@ -14029,6 +14171,7 @@ dependencies = [ "reth-testing-utils", "reth-transaction-pool", "reth-trie", + "reth-trie-db", "revm", "rlimit 0.10.2", "secp256k1 0.30.0", diff --git a/Cargo.toml b/Cargo.toml index d171ef3f..0977e999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,7 @@ reth-tracing = { git = "https://github.com/okx/reth", rev = "b6a31f31af91abdecb4 reth-transaction-pool = { git = "https://github.com/okx/reth", rev = "b6a31f31af91abdecb475f2a991906bff9bbef7f" } reth-optimism-flashblocks = { git = "https://github.com/okx/reth", rev = "b6a31f31af91abdecb475f2a991906bff9bbef7f" } reth-trie = { git = "https://github.com/okx/reth", rev = "b6a31f31af91abdecb475f2a991906bff9bbef7f" } +reth-trie-db = { git = "https://github.com/okx/reth", rev = "b6a31f31af91abdecb475f2a991906bff9bbef7f" } # ============================================================================== # Revm Dependencies (follows upstream reth) @@ -199,6 +200,7 @@ eyre = { version = "0.6.12" } hex = "0.4" metrics = "0.24.1" moka = { version = "0.12.11", features = ["sync"] } +criterion = { version = "0.5", features = ["html_reports"] } once_cell = "1.19" parking_lot = { version = "0.12.3" } secp256k1 = { version = "0.30" } diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml index 14ae6e84..6298e3b0 100644 --- a/crates/builder/Cargo.toml +++ b/crates/builder/Cargo.toml @@ -39,6 +39,7 @@ reth-metrics.workspace = true reth-provider.workspace = true reth-revm.workspace = true reth-trie.workspace = true +reth-trie-db.workspace = true reth-rpc-layer.workspace = true reth-payload-util.workspace = true reth-transaction-pool.workspace = true @@ -121,6 +122,7 @@ hyper = { version = "1.7.0", features = ["http1"], optional = true } hyper-util = { version = "0.1.11", optional = true } macros = { path = "src/tests/framework/macros", optional = true } nanoid = { version = "0.4", optional = true } +criterion.workspace = true rand = "0.9.0" rlimit = { version = "0.10", optional = true } sha3 = "0.10" @@ -157,3 +159,7 @@ testing = [ ] interop = [] + +[[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..b73d215b --- /dev/null +++ b/crates/builder/benches/bench_flashblocks_state_root.rs @@ -0,0 +1,310 @@ +//! 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 xlayer-builder --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 + 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/payload/flashblocks/payload.rs b/crates/builder/src/payload/flashblocks/payload.rs index 9d0e38f4..b41185e2 100644 --- a/crates/builder/src/payload/flashblocks/payload.rs +++ b/crates/builder/src/payload/flashblocks/payload.rs @@ -52,7 +52,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; @@ -111,6 +111,9 @@ pub(super) struct FlashblocksState { /// Index into `ExecutionInfo` tracking the last consumed flashblock. /// Used for slicing transactions/receipts per flashblock. last_flashblock_tx_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>, } impl FlashblocksState { @@ -130,6 +133,7 @@ impl FlashblocksState { target_gas_for_batch, target_da_for_batch, target_da_footprint_for_batch, + prev_trie_updates: self.prev_trie_updates.clone(), ..*self } } @@ -980,24 +984,67 @@ where let mut state_root = B256::ZERO; let mut trie_output = TrieUpdates::default(); let mut hashed_state = HashedPostState::default(); + let mut trie_updates_to_cache: Option = None; if calculate_state_root { let state_provider = state.database.as_ref(); + + // prev_trie_updates is None for the first flashblock. + let prev_trie = fb_state.as_deref().and_then(|s| s.prev_trie_updates.clone()); + let flashblock_index = fb_state.as_deref().map(|s| s.flashblock_index()).unwrap_or(0); + hashed_state = state_provider.hashed_post_state(&state.bundle_state); - (state_root, trie_output) = { + + (state_root, trie_output) = if let Some(prev_trie) = prev_trie { + // Incremental path: Use cached trie from previous flashblock + debug!( + target: "payload_builder", + flashblock_index, + "Using incremental state root calculation with cached trie" + ); + + let trie_input = TrieInput::new( + prev_trie.as_ref().clone(), + hashed_state.clone(), + hashed_state.construct_prefix_sets(), + ); + + state_provider + .state_root_from_nodes_with_updates(trie_input) + .map_err(PayloadBuilderError::other)? + } else { + debug!( + target: "payload_builder", + flashblock_index, + "Using full state root calculation" + ); + state.database.as_ref().state_root_with_updates(hashed_state.clone()).inspect_err( |err| { - warn!(target: "payload_builder", - parent_header=%ctx.parent().hash(), + warn!( + target: "payload_builder", + parent_header=%ctx.parent().hash(), %err, "failed to calculate state root for payload" ); }, )? }; + + // Cache trie updates to apply in fb_state later (avoids mut on fb_state parameter). + trie_updates_to_cache = Some(trie_output.clone()); + let state_root_calculation_time = state_root_start_time.elapsed(); ctx.metrics.state_root_calculation_duration.record(state_root_calculation_time); ctx.metrics.state_root_calculation_gauge.set(state_root_calculation_time); + + debug!( + target: "payload_builder", + flashblock_index, + state_root = %state_root, + duration_ms = state_root_calculation_time.as_millis(), + "State root calculation completed" + ); } let mut requests_hash = None; @@ -1115,6 +1162,9 @@ where let new_receipts = info.receipts[last_idx..].to_vec(); if let Some(fb) = fb_state { + if let Some(updates) = trie_updates_to_cache.take() { + fb.prev_trie_updates = Some(Arc::new(updates)); + } fb.set_last_flashblock_tx_index(info.executed_transactions.len()); } let receipts_with_hash = new_transactions From 5327f2e45bced27b45458ab13023d25c76231aa4 Mon Sep 17 00:00:00 2001 From: Vui-Chee Date: Mon, 9 Mar 2026 09:43:24 +0800 Subject: [PATCH 2/2] perf: remove trie updates clone and allocation, remove benchmarks Align with upstream flashbots/op-rbuilder#427: - Wrap trie_output in Arc once upfront instead of cloning then wrapping - Reuse same Arc allocation for both executed block and fb_state cache - Remove trie cache benchmark and related dev dependencies (criterion, reth-trie-db) --- Cargo.lock | 145 -------- crates/builder/Cargo.toml | 6 - .../benches/bench_flashblocks_state_root.rs | 310 ------------------ crates/builder/src/flashblocks/builder.rs | 17 +- 4 files changed, 11 insertions(+), 467 deletions(-) delete mode 100644 crates/builder/benches/bench_flashblocks_state_root.rs diff --git a/Cargo.lock b/Cargo.lock index 915fae09..4e8f3491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,12 +969,6 @@ 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" @@ -2228,12 +2222,6 @@ 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" @@ -2329,33 +2317,6 @@ 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" @@ -2675,42 +2636,6 @@ 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" @@ -4181,17 +4106,6 @@ 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" @@ -4956,17 +4870,6 @@ 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" @@ -6530,12 +6433,6 @@ 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" @@ -7080,34 +6977,6 @@ 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" @@ -12465,16 +12334,6 @@ 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" @@ -14221,7 +14080,6 @@ dependencies = [ "async-trait", "chrono", "clap", - "criterion", "ctor", "dashmap 6.1.0", "derive_more", @@ -14285,7 +14143,6 @@ dependencies = [ "reth-testing-utils", "reth-transaction-pool", "reth-trie", - "reth-trie-db", "revm", "rlimit 0.10.2", "secp256k1 0.30.0", @@ -14491,11 +14348,9 @@ name = "xlayer-rpc" version = "0.1.0" dependencies = [ "jsonrpsee", - "reth-chainspec", "reth-optimism-rpc", "reth-rpc", "reth-rpc-eth-api", - "reth-storage-api", "serde", "tokio", ] diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml index a8b40553..0e183e64 100644 --- a/crates/builder/Cargo.toml +++ b/crates/builder/Cargo.toml @@ -39,7 +39,6 @@ reth-metrics.workspace = true reth-provider.workspace = true reth-revm.workspace = true reth-trie.workspace = true -reth-trie-db.workspace = true reth-rpc-layer.workspace = true reth-payload-util.workspace = true reth-transaction-pool.workspace = true @@ -122,7 +121,6 @@ hyper = { version = "1.7.0", features = ["http1"], optional = true } hyper-util = { version = "0.1.11", optional = true } macros = { path = "src/tests/framework/macros", optional = true } nanoid = { version = "0.4", optional = true } -criterion.workspace = true rand = "0.9.0" rlimit = { version = "0.10", optional = true } sha3 = "0.10" @@ -160,7 +158,3 @@ testing = [ ] interop = [] - -[[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 deleted file mode 100644 index b73d215b..00000000 --- a/crates/builder/benches/bench_flashblocks_state_root.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! 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 xlayer-builder --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 - 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/flashblocks/builder.rs b/crates/builder/src/flashblocks/builder.rs index 2398c6fc..d89dcfde 100644 --- a/crates/builder/src/flashblocks/builder.rs +++ b/crates/builder/src/flashblocks/builder.rs @@ -988,9 +988,8 @@ where // calculate the state root let state_root_start_time = Instant::now(); let mut state_root = B256::ZERO; - let mut trie_output = TrieUpdates::default(); let mut hashed_state = HashedPostState::default(); - let mut trie_updates_to_cache: Option = None; + let mut trie_updates_to_cache: Option> = None; if calculate_state_root { let state_provider = state.database.as_ref(); @@ -1001,6 +1000,7 @@ where hashed_state = state_provider.hashed_post_state(&state.bundle_state); + let trie_output; (state_root, trie_output) = if let Some(prev_trie) = prev_trie { // Incremental path: Use cached trie from previous flashblock debug!( @@ -1010,7 +1010,7 @@ where ); let trie_input = TrieInput::new( - prev_trie.as_ref().clone(), + (*prev_trie).clone(), hashed_state.clone(), hashed_state.construct_prefix_sets(), ); @@ -1038,7 +1038,8 @@ where }; // Cache trie updates to apply in fb_state later (avoids mut on fb_state parameter). - trie_updates_to_cache = Some(trie_output.clone()); + // Wrap in Arc once so the same allocation is reused for both `executed` and fb_state. + trie_updates_to_cache = Some(Arc::new(trie_output)); let state_root_calculation_time = state_root_start_time.elapsed(); ctx.metrics.state_root_calculation_duration.record(state_root_calculation_time); @@ -1137,7 +1138,11 @@ where let executed = BuiltPayloadExecutedBlock { recovered_block: Arc::new(recovered_block), execution_output: Arc::new(execution_output), - trie_updates: either::Either::Left(Arc::new(trie_output)), + trie_updates: either::Either::Left( + trie_updates_to_cache + .clone() + .unwrap_or_else(|| Arc::new(TrieUpdates::default())), + ), hashed_state: either::Either::Left(Arc::new(hashed_state)), }; debug!( @@ -1169,7 +1174,7 @@ where let new_receipts = info.receipts[last_idx..].to_vec(); if let Some(fb) = fb_state { if let Some(updates) = trie_updates_to_cache.take() { - fb.prev_trie_updates = Some(Arc::new(updates)); + fb.prev_trie_updates = Some(updates); } fb.set_last_flashblock_tx_index(info.executed_transactions.len()); }