diff --git a/.gitignore b/.gitignore index cf5014a4810..5067373be6a 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,6 @@ __pycache__/ # direnv .envrc .direnv/ + +# Local spec / design docs +*_SPEC.md diff --git a/Cargo.lock b/Cargo.lock index 39aacfb04d9..e06da745414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8704,6 +8704,7 @@ dependencies = [ "reth-engine-primitives", "reth-ethereum-engine-primitives", "reth-optimism-chainspec", + "reth-optimism-forks", "reth-payload-builder", "reth-payload-primitives", "reth-primitives-traits", @@ -10243,6 +10244,7 @@ dependencies = [ "alloy-rpc-types-engine", "derive_more", "either", + "metrics", "op-alloy-consensus", "op-alloy-rpc-types-engine", "reth-basic-payload-builder", diff --git a/crates/engine/local/Cargo.toml b/crates/engine/local/Cargo.toml index 8bf9e28bcbf..3d56184d5bb 100644 --- a/crates/engine/local/Cargo.toml +++ b/crates/engine/local/Cargo.toml @@ -35,6 +35,7 @@ tracing.workspace = true op-alloy-rpc-types-engine = { workspace = true, optional = true } reth-optimism-chainspec = { workspace = true, optional = true } +reth-optimism-forks = { workspace = true, optional = true } [lints] workspace = true @@ -43,6 +44,7 @@ workspace = true op = [ "dep:op-alloy-rpc-types-engine", "dep:reth-optimism-chainspec", + "dep:reth-optimism-forks", "reth-payload-primitives/op", "reth-primitives-traits/op", ] diff --git a/crates/engine/local/src/payload.rs b/crates/engine/local/src/payload.rs index dc3be02f17e..76ef8c92520 100644 --- a/crates/engine/local/src/payload.rs +++ b/crates/engine/local/src/payload.rs @@ -2,7 +2,7 @@ //! [`LocalMiner`](super::LocalMiner). use alloy_consensus::BlockHeader; -use alloy_primitives::{Address, B256}; +use alloy_primitives::{Address, B256, B64}; use reth_chainspec::{EthChainSpec, EthereumHardforks}; use reth_ethereum_engine_primitives::EthPayloadAttributes; use reth_payload_primitives::PayloadAttributesBuilder; @@ -66,14 +66,27 @@ impl PayloadAttributesBuilder for LocalPayloadAttributesBuilder where - ChainSpec: EthChainSpec + EthereumHardforks + 'static, + ChainSpec: EthChainSpec + EthereumHardforks + reth_optimism_forks::OpHardforks + 'static, { fn build( &self, parent: &SealedHeader, ) -> op_alloy_rpc_types_engine::OpPayloadAttributes { + let eth_attrs: EthPayloadAttributes = self.build(parent); + let timestamp = eth_attrs.timestamp; + // Holocene+ requires eip_1559_params to be Some. B64::ZERO (elasticity=0, denominator=0) + // tells the payload builder to use the chain spec's default base fee params. + let eip_1559_params = self + .chain_spec + .is_holocene_active_at_timestamp(timestamp) + .then_some(B64::ZERO); + // Jovian requires min_base_fee to be Some; use 1 wei as the floor for dev mode. + let min_base_fee = self + .chain_spec + .is_jovian_active_at_timestamp(timestamp) + .then_some(1u64); op_alloy_rpc_types_engine::OpPayloadAttributes { - payload_attributes: self.build(parent), + payload_attributes: eth_attrs, // Add dummy system transaction transactions: Some(vec![ reth_optimism_chainspec::constants::TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056 @@ -81,8 +94,8 @@ where ]), no_tx_pool: None, gas_limit: None, - eip_1559_params: None, - min_base_fee: None, + eip_1559_params, + min_base_fee, } } } diff --git a/crates/engine/tree/src/tree/payload_processor/prewarm.rs b/crates/engine/tree/src/tree/payload_processor/prewarm.rs index 494e2d0f261..0a142b1b7ec 100644 --- a/crates/engine/tree/src/tree/payload_processor/prewarm.rs +++ b/crates/engine/tree/src/tree/payload_processor/prewarm.rs @@ -328,12 +328,14 @@ where } let total_slots = total_slots(&bal); + let total_entries = bal.len(); - trace!( + debug!( target: "engine::tree::payload_processor::prewarm", total_slots, + total_entries, max_concurrency = self.max_concurrency, - "Starting BAL prewarm" + "Starting BAL prewarm — prefetching EIP-2930 access list slots" ); if total_slots == 0 { @@ -376,12 +378,15 @@ where completed_workers += 1; } - trace!( + debug!( target: "engine::tree::payload_processor::prewarm", completed_workers, + total_slots, "All BAL prewarm workers completed" ); + self.ctx.metrics.bal_slots_prefetched.increment(total_slots as u64); + // Signal that execution has finished let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 }); } @@ -863,4 +868,6 @@ pub(crate) struct PrewarmMetrics { pub(crate) transaction_errors: Counter, /// A histogram of BAL slot iteration duration during prefetching pub(crate) bal_slot_iteration_duration: Histogram, + /// Total storage slots prefetched via BAL across all workers + pub(crate) bal_slots_prefetched: Counter, } diff --git a/crates/optimism/payload/Cargo.toml b/crates/optimism/payload/Cargo.toml index 0b7e3e44d63..5b738635806 100644 --- a/crates/optimism/payload/Cargo.toml +++ b/crates/optimism/payload/Cargo.toml @@ -48,6 +48,7 @@ alloy-evm.workspace = true # misc derive_more.workspace = true +metrics.workspace = true tracing.workspace = true thiserror.workspace = true sha2.workspace = true diff --git a/crates/optimism/payload/src/al_prefetch.rs b/crates/optimism/payload/src/al_prefetch.rs new file mode 100644 index 00000000000..08364abd2da --- /dev/null +++ b/crates/optimism/payload/src/al_prefetch.rs @@ -0,0 +1,84 @@ +use alloy_consensus::Transaction as _; +use alloy_primitives::{map::HashSet, Address, U256}; +use reth_payload_util::PayloadTransactions; +use std::sync::OnceLock; +use tracing::{debug, info}; + +static ENABLED: OnceLock = OnceLock::new(); + +pub fn is_enabled() -> bool { + *ENABLED.get_or_init(|| { + let raw = std::env::var("TXPOOL_AL_PREFETCH_ONLY").unwrap_or_default(); + let enabled = matches!(raw.as_str(), "1" | "true" | "True" | "TRUE"); + // Fires exactly once at first call — visible in node logs regardless of log level. + tracing::warn!( + target: "payload_builder::al_prefetch", + enabled, + raw_value = %raw, + "AL prefetch init (TXPOOL_AL_PREFETCH_ONLY)" + ); + enabled + }) +} + +/// Pre-loads EIP-2930 access list keys from MDBX into the EVM's cached DB +/// for the already-selected best transactions, before execution begins. +/// Reads go through `builder.evm_mut().db_mut()` so State/CachedReads is +/// populated automatically — the EVM then hits cache instead of MDBX. +pub fn prefetch_from_best_txs(mut txs: Txs, db: &mut DB) +where + Txs: PayloadTransactions, + Txs::Transaction: alloy_consensus::Transaction, + DB: revm::Database, +{ + let start = std::time::Instant::now(); + let mut tx_count = 0usize; + + let mut accounts: HashSet
= HashSet::default(); + let mut slots: HashSet<(Address, U256)> = HashSet::default(); + + while let Some(tx) = txs.next(()) { + let Some(al) = tx.access_list() else { + continue + }; + if al.0.is_empty() { + continue; + } + + tx_count += 1; + for item in &al.0 { + accounts.insert(item.address); + for key in &item.storage_keys { + slots.insert((item.address, U256::from_be_bytes(key.0))); + } + } + } + + // Pass 2: one MDBX read per unique key — populates State/CachedReads. + for &addr in &accounts { + let _ = db.basic(addr); + } + for &(addr, slot) in &slots { + let _ = db.storage(addr, slot); + } + + let key_count = accounts.len() + slots.len(); + + let elapsed_us = start.elapsed().as_micros() as u64; + + // Always increments — non-zero confirms is_enabled() fired and the code + // path is executing, even if no incoming txns carry access lists. + metrics::counter!("reth_al_prefetch_calls_totcal").increment(1); + metrics::counter!("reth_al_prefetch_tx_with_access_list_total").increment(tx_count as u64); + metrics::counter!("reth_al_prefetch_keys_extracted_total").increment(key_count as u64); + metrics::histogram!("reth_al_prefetch_duration_seconds") + .record(elapsed_us as f64 / 1_000_000.0); + + debug!( + target: "payload_builder::al_prefetch", + tx_count, + key_count, + elapsed_us, + "AL prefetch completed" + ); +} diff --git a/crates/optimism/payload/src/builder.rs b/crates/optimism/payload/src/builder.rs index b21178bde97..3e546c028e2 100644 --- a/crates/optimism/payload/src/builder.rs +++ b/crates/optimism/payload/src/builder.rs @@ -202,7 +202,7 @@ where fn build_payload<'a, Txs>( &self, args: BuildArguments>, - best: impl FnOnce(BestTransactionsAttributes) -> Txs + Send + Sync + 'a, + best: impl Fn(BestTransactionsAttributes) -> Txs + Send + Sync + 'a, ) -> Result>, PayloadBuilderError> where Txs: @@ -223,6 +223,7 @@ where let builder = OpBuilder::new(best); let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?; + let state = StateProviderDatabase::new(&state_provider); if ctx.attributes().no_tx_pool() { @@ -286,7 +287,7 @@ where args: BuildArguments, ) -> Result, PayloadBuilderError> { let pool = self.pool.clone(); - self.build_payload(args, |attrs| self.best_transactions.best_transactions(pool, attrs)) + self.build_payload(args, |attrs| self.best_transactions.best_transactions(pool.clone(), attrs)) } fn on_missing_payload( @@ -335,12 +336,12 @@ where pub struct OpBuilder<'a, Txs> { /// Yields the best transaction to include if transactions from the mempool are allowed. #[debug(skip)] - best: Box Txs + 'a>, + best: Box Txs + 'a>, } impl<'a, Txs> OpBuilder<'a, Txs> { /// Creates a new [`OpBuilder`]. - pub fn new(best: impl FnOnce(BestTransactionsAttributes) -> Txs + Send + Sync + 'a) -> Self { + pub fn new(best: impl Fn(BestTransactionsAttributes) -> Txs + Send + Sync + 'a) -> Self { Self { best: Box::new(best) } } } @@ -426,11 +427,25 @@ impl OpBuilder<'_, Txs> { // 3. if mem pool transactions are requested we execute them if !ctx.attributes().no_tx_pool() { // 3.1. select/pack mempool transactions + let tx_attrs = ctx.best_transaction_attributes(builder.evm_mut().block()); let best_txs = { let _guard = timing_ctx.time_select_mempool_transactions(); - best(ctx.best_transaction_attributes(builder.evm_mut().block())) + best(tx_attrs) }; + // 3.1.5. AL prefetch: extract EIP-2930 access-list keys from the + // already-selected best transactions and pre-load them from MDBX + // into CachedReads before the EVM starts executing. + // A second iterator over the same selection is created — `best` is + // now `Fn` (not `FnOnce`) so this is safe and cheap. + // if crate::al_prefetch::is_enabled() { + let prefetch_txs = best(tx_attrs); + crate::al_prefetch::prefetch_from_best_txs( + prefetch_txs, + builder.evm_mut().db_mut(), + ); + //} + // 3.2. execute mempool transactions { let _guard = timing_ctx.time_exec_mempool_transactions(); diff --git a/crates/optimism/payload/src/lib.rs b/crates/optimism/payload/src/lib.rs index cbfc4536e41..57013bb1be0 100644 --- a/crates/optimism/payload/src/lib.rs +++ b/crates/optimism/payload/src/lib.rs @@ -11,6 +11,7 @@ extern crate alloc; +pub mod al_prefetch; pub mod builder; pub use builder::OpPayloadBuilder; pub mod error; diff --git a/crates/payload/primitives/src/payload.rs b/crates/payload/primitives/src/payload.rs index d50f6ffd052..923f6c0e42e 100644 --- a/crates/payload/primitives/src/payload.rs +++ b/crates/payload/primitives/src/payload.rs @@ -58,6 +58,14 @@ pub trait ExecutionPayload: /// Returns the number of transactions in the payload. fn transaction_count(&self) -> usize; + + /// Returns the raw RLP-encoded transactions in the payload. + /// + /// Used to extract EIP-2930 access lists for pre-warming state before block execution. + /// Returns an empty slice for payload types that don't expose raw transaction bytes. + fn encoded_transactions(&self) -> &[Bytes] { + &[] + } } impl ExecutionPayload for ExecutionData { @@ -207,6 +215,10 @@ impl ExecutionPayload for op_alloy_rpc_types_engine::OpExecutionData { fn transaction_count(&self) -> usize { self.payload.as_v1().transactions.len() } + + fn encoded_transactions(&self) -> &[Bytes] { + &self.payload.as_v1().transactions + } } /// Extended functionality for Ethereum execution payloads