Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ __pycache__/
# direnv
.envrc
.direnv/

# Local spec / design docs
*_SPEC.md
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions crates/engine/local/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
]
23 changes: 18 additions & 5 deletions crates/engine/local/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,23 +66,36 @@ impl<ChainSpec>
PayloadAttributesBuilder<op_alloy_rpc_types_engine::OpPayloadAttributes, ChainSpec::Header>
for LocalPayloadAttributesBuilder<ChainSpec>
where
ChainSpec: EthChainSpec + EthereumHardforks + 'static,
ChainSpec: EthChainSpec + EthereumHardforks + reth_optimism_forks::OpHardforks + 'static,
{
fn build(
&self,
parent: &SealedHeader<ChainSpec::Header>,
) -> 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
.into(),
]),
no_tx_pool: None,
gas_limit: None,
eip_1559_params: None,
min_base_fee: None,
eip_1559_params,
min_base_fee,
}
}
}
13 changes: 10 additions & 3 deletions crates/engine/tree/src/tree/payload_processor/prewarm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 });
}
Expand Down Expand Up @@ -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,
}
1 change: 1 addition & 0 deletions crates/optimism/payload/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 84 additions & 0 deletions crates/optimism/payload/src/al_prefetch.rs
Original file line number Diff line number Diff line change
@@ -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<bool> = 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<Txs, DB>(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<Address> = 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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prefetch reads the same MDBX data on the same thread, sequentially

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"
);
}
25 changes: 20 additions & 5 deletions crates/optimism/payload/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ where
fn build_payload<'a, Txs>(
&self,
args: BuildArguments<Attrs, OpBuiltPayload<N>>,
best: impl FnOnce(BestTransactionsAttributes) -> Txs + Send + Sync + 'a,
best: impl Fn(BestTransactionsAttributes) -> Txs + Send + Sync + 'a,
) -> Result<BuildOutcome<OpBuiltPayload<N>>, PayloadBuilderError>
where
Txs:
Expand All @@ -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() {
Expand Down Expand Up @@ -286,7 +287,7 @@ where
args: BuildArguments<Self::Attributes, Self::BuiltPayload>,
) -> Result<BuildOutcome<Self::BuiltPayload>, 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(
Expand Down Expand Up @@ -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<dyn FnOnce(BestTransactionsAttributes) -> Txs + 'a>,
best: Box<dyn Fn(BestTransactionsAttributes) -> 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) }
}
}
Expand Down Expand Up @@ -426,11 +427,25 @@ impl<Txs> 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();
Expand Down
1 change: 1 addition & 0 deletions crates/optimism/payload/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

extern crate alloc;

pub mod al_prefetch;
pub mod builder;
pub use builder::OpPayloadBuilder;
pub mod error;
Expand Down
12 changes: 12 additions & 0 deletions crates/payload/primitives/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down