From 17af43b1a158266fa8357c0a7cf11a2fb50a6c82 Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Wed, 1 Jul 2026 21:03:36 -0700 Subject: [PATCH 1/4] Optimize L2 block info proof reads --- .../proof/proof/src/l2/chain_provider.rs | 194 +++++++++++++++++- 1 file changed, 185 insertions(+), 9 deletions(-) diff --git a/rust/kona/crates/proof/proof/src/l2/chain_provider.rs b/rust/kona/crates/proof/proof/src/l2/chain_provider.rs index a6d4264c339..8ecb4a9f9e6 100644 --- a/rust/kona/crates/proof/proof/src/l2/chain_provider.rs +++ b/rust/kona/crates/proof/proof/src/l2/chain_provider.rs @@ -2,18 +2,21 @@ use crate::{HintType, eip2935::eip_2935_history_lookup, errors::OracleProviderError}; use alloc::{boxed::Box, sync::Arc, vec::Vec}; -use alloy_consensus::{BlockBody, Header}; +use alloy_consensus::{BlockBody, Header, Transaction, Typed2718}; use alloy_eips::eip2718::Decodable2718; use alloy_primitives::{Address, B256, Bytes}; -use alloy_rlp::Decodable; +use alloy_rlp::{Decodable, Encodable}; use async_trait::async_trait; use kona_derive::L2ChainProvider; use kona_driver::PipelineCursor; use kona_executor::TrieDBProvider; use kona_genesis::{RollupConfig, SystemConfig}; -use kona_mpt::{OrderedListWalker, TrieHinter, TrieNode, TrieProvider}; +use kona_mpt::{Nibbles, OrderedListWalker, TrieHinter, TrieNode, TrieProvider}; use kona_preimage::{CommsClient, PreimageKey, PreimageKeyType}; -use kona_protocol::{BatchValidationProvider, L2BlockInfo, to_system_config}; +use kona_protocol::{ + BatchValidationProvider, BlockInfo, FromBlockError, L1BlockInfoTx, L2BlockInfo, + to_system_config, +}; use op_alloy_consensus::{OpBlock, OpTxEnvelope}; use spin::RwLock; @@ -58,6 +61,38 @@ impl OracleL2ChainProvider { } impl OracleL2ChainProvider { + fn block_info_from_header(header: &Header) -> BlockInfo { + BlockInfo { + hash: header.hash_slow(), + number: header.number, + parent_hash: header.parent_hash, + timestamp: header.timestamp, + } + } + + async fn first_transaction_rlp( + &self, + transactions_root: B256, + header_hash: B256, + ) -> Result, OracleProviderError> { + HintType::L2Transactions + .with_data(&[header_hash.as_ref()]) + .with_data(self.chain_id.map_or_else(Vec::new, |id| id.to_be_bytes().to_vec())) + .send(self.oracle.as_ref()) + .await?; + + let mut trie = TrieNode::new_blinded(transactions_root); + // The ordered trie stores logical transaction 0 under RLP(0). The full-list walker + // compensates by moving this final leaf to the front after hydrating all leaves. + let mut encoded_index = Vec::new(); + 0usize.encode(&mut encoded_index); + if let Some(tx) = trie.open(&Nibbles::unpack(&encoded_index), self)? { + return Ok(Some(tx.clone())); + } + + Ok(None) + } + /// Returns a [Header] corresponding to the given L2 block number, by walking back from the /// L2 safe head. async fn header_by_number(&self, block_number: u64) -> Result { @@ -107,12 +142,37 @@ impl BatchValidationProvider for OracleL2ChainProv type Error = OracleProviderError; async fn l2_block_info_by_number(&mut self, number: u64) -> Result { - // Get the block at the given number. - let block = self.block_by_number(number).await?; + let header @ Header { transactions_root, .. } = self.header_by_number(number).await?; + let block_info = Self::block_info_from_header(&header); - // Construct the system config from the payload. - L2BlockInfo::from_block_and_genesis(&block, &self.rollup_config.genesis) - .map_err(OracleProviderError::BlockInfo) + if block_info.number == self.rollup_config.genesis.l2.number { + if block_info.hash != self.rollup_config.genesis.l2.hash { + return Err(OracleProviderError::BlockInfo(FromBlockError::InvalidGenesisHash)); + } + return Ok(L2BlockInfo::new(block_info, self.rollup_config.genesis.l1, 0)); + } + + let first_tx_rlp = self + .first_transaction_rlp(transactions_root, block_info.hash) + .await? + .ok_or_else(|| { + OracleProviderError::BlockInfo(FromBlockError::MissingL1InfoDeposit( + block_info.hash, + )) + })?; + let first_tx = OpTxEnvelope::decode_2718(&mut first_tx_rlp.as_ref()) + .map_err(FromBlockError::from) + .map_err(OracleProviderError::BlockInfo)?; + let Some(deposit) = first_tx.as_deposit() else { + return Err(OracleProviderError::BlockInfo(FromBlockError::FirstTxNonDeposit( + first_tx.ty(), + ))); + }; + + let l1_info = L1BlockInfoTx::decode_calldata(deposit.input().as_ref()) + .map_err(FromBlockError::BlockInfoDecodeError) + .map_err(OracleProviderError::BlockInfo)?; + Ok(L2BlockInfo::new(block_info, l1_info.id(), l1_info.sequence_number())) } async fn block_by_number(&mut self, number: u64) -> Result { @@ -284,3 +344,119 @@ impl TrieHinter for OracleL2ChainProvider { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::{collections::BTreeMap, vec, vec::Vec}; + use alloy_eips::eip2718::Encodable2718; + use alloy_primitives::{Address, Sealed, U256, keccak256}; + use core::sync::atomic::{AtomicUsize, Ordering}; + use kona_genesis::ChainGenesis; + use kona_mpt::ordered_trie_with_encoder; + use kona_preimage::{HintWriterClient, PreimageOracleClient, errors::PreimageOracleResult}; + use kona_protocol::{L1BlockInfoBedrock, L1BlockInfoTx}; + use op_alloy_consensus::TxDeposit; + + #[derive(Clone, Default)] + struct MockOracle { + preimages: Arc>>, + get_calls: Arc, + } + + #[async_trait] + impl PreimageOracleClient for MockOracle { + async fn get(&self, key: PreimageKey) -> PreimageOracleResult> { + self.get_calls.fetch_add(1, Ordering::SeqCst); + Ok(self.preimages.get(&key).expect("missing preimage in mock").clone()) + } + + async fn get_exact(&self, key: PreimageKey, buf: &mut [u8]) -> PreimageOracleResult<()> { + let value = self.get(key).await?; + buf.copy_from_slice(&value); + Ok(()) + } + } + + #[async_trait] + impl HintWriterClient for MockOracle { + async fn write(&self, _hint: &str) -> PreimageOracleResult<()> { + Ok(()) + } + } + + fn deposit_tx(l1_number: u64, seq_num: u64) -> OpTxEnvelope { + let l1_info = L1BlockInfoTx::Bedrock(L1BlockInfoBedrock::new( + l1_number, + 1_000 + l1_number, + 1, + B256::from([l1_number as u8; 32]), + seq_num, + Address::ZERO, + U256::ZERO, + U256::ZERO, + )); + OpTxEnvelope::Deposit(Sealed::new(TxDeposit { + input: l1_info.encode_calldata(), + ..Default::default() + })) + } + + fn provider_with_block( + txs: &[OpTxEnvelope], + ) -> (OracleL2ChainProvider, Arc) { + let mut tx_trie = ordered_trie_with_encoder(txs, |tx, buf| tx.encode_2718(buf)); + let transactions_root = tx_trie.root(); + let header = Header { number: 42, transactions_root, ..Default::default() }; + + let mut preimages = tx_trie.take_proof_nodes().into_inner().into_iter().fold( + BTreeMap::new(), + |mut acc, (_, value)| { + acc.insert( + PreimageKey::new(*keccak256(value.as_ref()), PreimageKeyType::Keccak256), + value.to_vec(), + ); + acc + }, + ); + + let mut header_rlp = Vec::new(); + header.encode(&mut header_rlp); + let l2_head = header.hash_slow(); + preimages.insert(PreimageKey::new(*l2_head, PreimageKeyType::Keccak256), header_rlp); + + let oracle = + MockOracle { preimages: Arc::new(preimages), get_calls: Arc::new(AtomicUsize::new(0)) }; + let calls = Arc::clone(&oracle.get_calls); + let rollup_config = Arc::new(RollupConfig { + genesis: ChainGenesis { + l2: alloy_eips::BlockNumHash { number: 0, ..Default::default() }, + ..Default::default() + }, + ..Default::default() + }); + + (OracleL2ChainProvider::new(l2_head, rollup_config, Arc::new(oracle)), calls) + } + + #[tokio::test] + async fn l2_block_info_by_number_reads_only_first_transaction_path() { + let txs = vec![deposit_tx(11, 7), deposit_tx(12, 8), deposit_tx(13, 9), deposit_tx(14, 10)]; + + let (mut fast_provider, fast_calls) = provider_with_block(&txs); + let info = fast_provider.l2_block_info_by_number(42).await.unwrap(); + let fast_gets = fast_calls.load(Ordering::SeqCst); + + let (mut full_provider, full_calls) = provider_with_block(&txs); + full_provider.block_by_number(42).await.unwrap(); + let full_gets = full_calls.load(Ordering::SeqCst); + + assert_eq!(info.block_info.number, 42); + assert_eq!(info.l1_origin.number, 11); + assert_eq!(info.seq_num, 7); + assert!( + fast_gets < full_gets, + "l2_block_info_by_number should not hydrate the full transaction trie" + ); + } +} From 1c76f7928ccf3270d0d5b4da2f84ac7a00ae94a4 Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Thu, 2 Jul 2026 05:18:40 -0700 Subject: [PATCH 2/4] perf(kona): avoid full block reads for reset system config --- .../proof/proof/src/l2/chain_provider.rs | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/rust/kona/crates/proof/proof/src/l2/chain_provider.rs b/rust/kona/crates/proof/proof/src/l2/chain_provider.rs index 8ecb4a9f9e6..15bbe3358cc 100644 --- a/rust/kona/crates/proof/proof/src/l2/chain_provider.rs +++ b/rust/kona/crates/proof/proof/src/l2/chain_provider.rs @@ -224,10 +224,38 @@ impl L2ChainProvider for OracleL2ChainProvider number: u64, rollup_config: Arc, ) -> Result::Error> { - // Get the block at the given number. - let block = self.block_by_number(number).await?; + let header @ Header { transactions_root, timestamp, .. } = + self.header_by_number(number).await?; + let header_hash = header.hash_slow(); + + let transactions = if header.number == rollup_config.genesis.l2.number { + Vec::new() + } else { + match self.first_transaction_rlp(transactions_root, header_hash).await? { + Some(rlp) => { + let mut transactions = Vec::with_capacity(1); + transactions.push( + OpTxEnvelope::decode_2718(&mut rlp.as_ref()) + .map_err(FromBlockError::from) + .map_err(OracleProviderError::BlockInfo)?, + ); + transactions + } + None => Vec::new(), + } + }; + + let block = OpBlock { + header, + body: BlockBody { + transactions, + ommers: Vec::new(), + withdrawals: rollup_config + .is_canyon_active(timestamp) + .then(|| alloy_eips::eip4895::Withdrawals::new(Vec::new())), + }, + }; - // Construct the system config from the payload. to_system_config(&block, rollup_config.as_ref()) .map_err(OracleProviderError::OpBlockConversion) } @@ -407,7 +435,8 @@ mod tests { ) -> (OracleL2ChainProvider, Arc) { let mut tx_trie = ordered_trie_with_encoder(txs, |tx, buf| tx.encode_2718(buf)); let transactions_root = tx_trie.root(); - let header = Header { number: 42, transactions_root, ..Default::default() }; + let header = + Header { number: 42, gas_limit: 30_000_000, transactions_root, ..Default::default() }; let mut preimages = tx_trie.take_proof_nodes().into_inner().into_iter().fold( BTreeMap::new(), @@ -459,4 +488,25 @@ mod tests { "l2_block_info_by_number should not hydrate the full transaction trie" ); } + + #[tokio::test] + async fn system_config_by_number_reads_only_first_transaction_path() { + let txs = vec![deposit_tx(11, 7), deposit_tx(12, 8), deposit_tx(13, 9), deposit_tx(14, 10)]; + + let (mut fast_provider, fast_calls) = provider_with_block(&txs); + let rollup_config = Arc::clone(&fast_provider.rollup_config); + let config = fast_provider.system_config_by_number(42, rollup_config).await.unwrap(); + let fast_gets = fast_calls.load(Ordering::SeqCst); + + let (mut full_provider, full_calls) = provider_with_block(&txs); + full_provider.block_by_number(42).await.unwrap(); + let full_gets = full_calls.load(Ordering::SeqCst); + + assert_eq!(config.batcher_address, Address::ZERO); + assert_eq!(config.gas_limit, 30_000_000); + assert!( + fast_gets < full_gets, + "system_config_by_number should not hydrate the full transaction trie" + ); + } } From 972c485e063cfaa452c5da3f87c36ce86f42efb4 Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Thu, 2 Jul 2026 05:45:17 -0700 Subject: [PATCH 3/4] perf(kona): binary search reset walkback --- .../protocol/derive/src/pipeline/core.rs | 81 ++++++++++++++----- 1 file changed, 62 insertions(+), 19 deletions(-) diff --git a/rust/kona/crates/protocol/derive/src/pipeline/core.rs b/rust/kona/crates/protocol/derive/src/pipeline/core.rs index 5abb534174f..3880571811d 100644 --- a/rust/kona/crates/protocol/derive/src/pipeline/core.rs +++ b/rust/kona/crates/protocol/derive/src/pipeline/core.rs @@ -55,24 +55,55 @@ where let channel_timeout = self.rollup_config.channel_timeout(l2_safe_head.block_info.timestamp); let mut current = l2_safe_head; - loop { - let before_l2_genesis = - current.block_info.number <= self.rollup_config.genesis.l2.number; - let before_l1_genesis = - current.l1_origin.number <= self.rollup_config.genesis.l1.number; - let before_channel_timeout = - current.l1_origin.number + channel_timeout <= l1_origin_number; - if before_l2_genesis || before_l1_genesis || before_channel_timeout { - break; + if !self.initial_reset_walk_complete(¤t, l1_origin_number, channel_timeout) { + let mut low = self.rollup_config.genesis.l2.number; + let mut high = current.block_info.number - 1; + let mut found = None; + + // L2 block L1 origins are monotonically non-decreasing on a valid OP chain, so the + // walkback stop predicate is true for an early prefix and false near the safe head. + // Find the highest block where the linear walk would stop. + while low <= high { + let mid = low + (high - low) / 2; + let candidate = + self.l2_chain_provider.l2_block_info_by_number(mid).await.map_err(|e| { + PipelineError::Provider(alloc::string::ToString::to_string(&e)).temp() + })?; + + if self.initial_reset_walk_complete(&candidate, l1_origin_number, channel_timeout) { + found = Some(candidate); + low = mid + 1; + } else if mid == 0 { + break; + } else { + high = mid - 1; + } } - current = self - .l2_chain_provider - .l2_block_info_by_number(current.block_info.number - 1) - .await - .map_err(|e| { - PipelineError::Provider(alloc::string::ToString::to_string(&e)).temp() - })?; + current = found.ok_or_else(|| { + PipelineError::Provider(alloc::string::String::from( + "Failed to find pipeline reset walkback boundary", + )) + .temp() + })?; + + if current.block_info.number < l2_safe_head.block_info.number { + let next_number = current.block_info.number + 1; + let next = if next_number == l2_safe_head.block_info.number { + l2_safe_head + } else { + self.l2_chain_provider.l2_block_info_by_number(next_number).await.map_err( + |e| PipelineError::Provider(alloc::string::ToString::to_string(&e)).temp(), + )? + }; + + if self.initial_reset_walk_complete(&next, l1_origin_number, channel_timeout) { + return Err(PipelineError::Provider(alloc::string::String::from( + "Invalid pipeline reset walkback boundary", + )) + .temp()); + } + } } let system_config = self @@ -83,6 +114,18 @@ where Ok((current.l1_origin, system_config)) } + + fn initial_reset_walk_complete( + &self, + current: &L2BlockInfo, + l1_origin_number: u64, + channel_timeout: u64, + ) -> bool { + let before_l2_genesis = current.block_info.number <= self.rollup_config.genesis.l2.number; + let before_l1_genesis = current.l1_origin.number <= self.rollup_config.genesis.l1.number; + let before_channel_timeout = current.l1_origin.number + channel_timeout <= l1_origin_number; + before_l2_genesis || before_l1_genesis || before_channel_timeout + } } impl OriginProvider for DerivationPipeline @@ -403,13 +446,13 @@ mod tests { let rollup_config = RollupConfig { channel_timeout: 10, ..Default::default() }; let mut l2_chain_provider = TestL2ChainProvider::default(); - // L2 blocks 89..=100: block N has L1 origin (N - 50). + // L2 blocks 0..=100: block N has L1 origin saturating at (N - 50). // Safe head at block 100, L1 origin 50. // Walkback: block 90 has L1 origin 40. 40 + 10 = 50, NOT > 50, so walkback stops. - for n in 89u64..=100 { + for n in 0u64..=100 { l2_chain_provider.blocks.push(L2BlockInfo { block_info: BlockInfo { number: n, ..Default::default() }, - l1_origin: BlockNumHash { number: n - 50, ..Default::default() }, + l1_origin: BlockNumHash { number: n.saturating_sub(50), ..Default::default() }, seq_num: 0, }); } From 654eb009c7ff6874fe92192e4771a72f9d508273 Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Thu, 2 Jul 2026 05:49:58 -0700 Subject: [PATCH 4/4] perf(kona): skip same-origin reset blocks --- .../protocol/derive/src/pipeline/core.rs | 97 +++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/rust/kona/crates/protocol/derive/src/pipeline/core.rs b/rust/kona/crates/protocol/derive/src/pipeline/core.rs index 3880571811d..3f807b83277 100644 --- a/rust/kona/crates/protocol/derive/src/pipeline/core.rs +++ b/rust/kona/crates/protocol/derive/src/pipeline/core.rs @@ -55,55 +55,20 @@ where let channel_timeout = self.rollup_config.channel_timeout(l2_safe_head.block_info.timestamp); let mut current = l2_safe_head; - if !self.initial_reset_walk_complete(¤t, l1_origin_number, channel_timeout) { - let mut low = self.rollup_config.genesis.l2.number; - let mut high = current.block_info.number - 1; - let mut found = None; - - // L2 block L1 origins are monotonically non-decreasing on a valid OP chain, so the - // walkback stop predicate is true for an early prefix and false near the safe head. - // Find the highest block where the linear walk would stop. - while low <= high { - let mid = low + (high - low) / 2; - let candidate = - self.l2_chain_provider.l2_block_info_by_number(mid).await.map_err(|e| { - PipelineError::Provider(alloc::string::ToString::to_string(&e)).temp() - })?; - - if self.initial_reset_walk_complete(&candidate, l1_origin_number, channel_timeout) { - found = Some(candidate); - low = mid + 1; - } else if mid == 0 { - break; - } else { - high = mid - 1; - } - } - - current = found.ok_or_else(|| { - PipelineError::Provider(alloc::string::String::from( - "Failed to find pipeline reset walkback boundary", - )) - .temp() - })?; - - if current.block_info.number < l2_safe_head.block_info.number { - let next_number = current.block_info.number + 1; - let next = if next_number == l2_safe_head.block_info.number { - l2_safe_head - } else { - self.l2_chain_provider.l2_block_info_by_number(next_number).await.map_err( - |e| PipelineError::Provider(alloc::string::ToString::to_string(&e)).temp(), - )? - }; - - if self.initial_reset_walk_complete(&next, l1_origin_number, channel_timeout) { - return Err(PipelineError::Provider(alloc::string::String::from( - "Invalid pipeline reset walkback boundary", - )) - .temp()); - } - } + while !self.initial_reset_walk_complete(¤t, l1_origin_number, channel_timeout) { + // All blocks with the same L1 origin have the same stop predicate. `seq_num` is the + // zero-based L2 block index within the current L1 origin, so jump directly to the last + // block of the previous L1 origin instead of checking every same-origin L2 block. + let previous_number = current + .block_info + .number + .saturating_sub(current.seq_num.saturating_add(1)) + .max(self.rollup_config.genesis.l2.number); + + current = + self.l2_chain_provider.l2_block_info_by_number(previous_number).await.map_err( + |e| PipelineError::Provider(alloc::string::ToString::to_string(&e)).temp(), + )?; } let system_config = self @@ -545,4 +510,38 @@ mod tests { assert_eq!(l1_origin.number, 0); assert_eq!(sys_cfg, SystemConfig::default()); } + + #[tokio::test] + async fn test_initial_reset_uses_sequence_number_to_skip_same_origin() { + use alloy_primitives::address; + + let rollup_config = RollupConfig { channel_timeout: 1, ..Default::default() }; + let mut l2_chain_provider = TestL2ChainProvider::default(); + l2_chain_provider.blocks.push(L2BlockInfo { + block_info: BlockInfo { number: 90, ..Default::default() }, + l1_origin: BlockNumHash { number: 49, ..Default::default() }, + seq_num: 4, + }); + l2_chain_provider.system_configs.insert( + 90, + SystemConfig { + batcher_address: address!("000000000000000000000000000000000000aaaa"), + ..Default::default() + }, + ); + + let rollup_config = Arc::new(rollup_config); + let attributes = TestNextAttributes::default(); + let mut pipeline = DerivationPipeline::new(attributes, rollup_config, l2_chain_provider); + + let l2_safe_head = L2BlockInfo { + block_info: BlockInfo { number: 100, ..Default::default() }, + l1_origin: BlockNumHash { number: 50, ..Default::default() }, + seq_num: 9, + }; + + let (l1_origin, sys_cfg) = pipeline.initial_reset(l2_safe_head).await.unwrap(); + assert_eq!(l1_origin.number, 49); + assert_eq!(sys_cfg.batcher_address, address!("000000000000000000000000000000000000aaaa")); + } }