Skip to content
This repository was archived by the owner on Jan 16, 2026. It is now read-only.
Merged
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
296 changes: 170 additions & 126 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion crates/derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ anyhow.workspace = true
tracing.workspace = true

# External
op-alloy-consensus = { git = "https://github.com/clabby/op-alloy", branch = "refcell/consensus-port", default-features = false }
alloy-primitives = { version = "0.7.0", default-features = false, features = ["rlp"] }
alloy-rlp = { version = "0.3.4", default-features = false, features = ["derive"] }
alloy-sol-types = { version = "0.7.0", default-features = false }
Expand All @@ -24,6 +25,7 @@ hashbrown = "0.14.3"
unsigned-varint = "0.8.0"
miniz_oxide = { version = "0.7.2" }
lru = "0.12.3"
spin = { version = "0.9.8", features = ["mutex"] }

# `serde` feature dependencies
serde = { version = "1.0.197", default-features = false, features = ["derive"], optional = true }
Expand All @@ -36,7 +38,6 @@ reqwest = { version = "0.12", default-features = false, optional = true }
[dev-dependencies]
tokio = { version = "1.36", features = ["full"] }
proptest = "1.4.0"
spin = { version = "0.9.8", features = ["mutex"] } # Spin is used for testing synchronization primitives
tracing-subscriber = "0.3.18"

[features]
Expand Down
17 changes: 17 additions & 0 deletions crates/derive/src/alloy_providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const CACHE_SIZE: usize = 16;
pub struct AlloyChainProvider<T: Provider<Http<reqwest::Client>>> {
/// The inner Ethereum JSON-RPC provider.
inner: T,
/// `header_by_hash` LRU cache.
header_by_hash_cache: LruCache<B256, Header>,
/// `block_info_by_number` LRU cache.
block_info_by_number_cache: LruCache<u64, BlockInfo>,
/// `block_info_by_number` LRU cache.
Expand All @@ -41,6 +43,7 @@ impl<T: Provider<Http<reqwest::Client>>> AlloyChainProvider<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
header_by_hash_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()),
block_info_by_number_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()),
receipts_by_hash_cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()),
block_info_and_transactions_by_hash_cache: LruCache::new(
Expand All @@ -52,6 +55,20 @@ impl<T: Provider<Http<reqwest::Client>>> AlloyChainProvider<T> {

#[async_trait]
impl<T: Provider<Http<reqwest::Client>>> ChainProvider for AlloyChainProvider<T> {
async fn header_by_hash(&mut self, hash: B256) -> Result<Header> {
if let Some(header) = self.header_by_hash_cache.get(&hash) {
return Ok(header.clone());
}

let raw_header: Bytes = self
.inner
.client()
.request("debug_getRawHeader", [hash])
.await
.map_err(|e| anyhow!(e))?;
Header::decode(&mut raw_header.as_ref()).map_err(|e| anyhow!(e))
}

async fn block_info_by_number(&mut self, number: u64) -> Result<BlockInfo> {
if let Some(block_info) = self.block_info_by_number_cache.get(&number) {
return Ok(*block_info);
Expand Down
5 changes: 3 additions & 2 deletions crates/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ use types::RollupConfig;
mod params;
pub use params::{
ChannelID, CHANNEL_ID_LENGTH, CONFIG_UPDATE_EVENT_VERSION_0, CONFIG_UPDATE_TOPIC,
DERIVATION_VERSION_0, FRAME_OVERHEAD, MAX_CHANNEL_BANK_SIZE, MAX_FRAME_LEN,
MAX_RLP_BYTES_PER_CHANNEL, MAX_SPAN_BATCH_BYTES,
DEPOSIT_EVENT_ABI, DEPOSIT_EVENT_ABI_HASH, DEPOSIT_EVENT_VERSION_0, DERIVATION_VERSION_0,
FRAME_OVERHEAD, MAX_CHANNEL_BANK_SIZE, MAX_FRAME_LEN, MAX_RLP_BYTES_PER_CHANNEL,
MAX_SPAN_BATCH_BYTES, SEQUENCER_FEE_VAULT_ADDRESS,
};

pub mod sources;
Expand Down
19 changes: 18 additions & 1 deletion crates/derive/src/params.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
//! This module contains the parameters and identifying types for the derivation pipeline.

use alloy_primitives::{b256, B256};
use alloy_primitives::{address, b256, Address, B256};

/// The sequencer fee vault address.
pub const SEQUENCER_FEE_VAULT_ADDRESS: Address =
address!("4200000000000000000000000000000000000011");

/// Count the tagging info as 200 in terms of buffer size.
pub const FRAME_OVERHEAD: usize = 200;
Expand Down Expand Up @@ -37,3 +41,16 @@ pub const CONFIG_UPDATE_EVENT_VERSION_0: B256 = B256::ZERO;
/// Data transactions that carry frames are generally not larger than 128 KB due to L1 network
/// conditions, but we leave space to grow larger anyway (gas limit allows for more data).
pub const MAX_FRAME_LEN: usize = 1000;

/// Deposit log event abi signature.
pub const DEPOSIT_EVENT_ABI: &str = "TransactionDeposited(address,address,uint256,bytes)";

/// Deposit event abi hash.
///
/// This is the keccak256 hash of the deposit event ABI signature.
/// `keccak256("TransactionDeposited(address,address,uint256,bytes)")`
pub const DEPOSIT_EVENT_ABI_HASH: B256 =
b256!("b3813568d9991fc951961fcb4c784893574240a28925604d09fc577c55bb7c32");

/// The initial version of the deposit event log.
pub const DEPOSIT_EVENT_VERSION_0: B256 = B256::ZERO;
24 changes: 12 additions & 12 deletions crates/derive/src/stages/attributes_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@
use crate::{
traits::{OriginProvider, ResettableStage},
types::{
AttributesWithParent, BlockID, BlockInfo, L2BlockInfo, PayloadAttributes, ResetError,
RollupConfig, SingleBatch, StageError, StageResult, SystemConfig,
AttributesWithParent, BlockInfo, L2BlockInfo, PayloadAttributes, ResetError, RollupConfig,
SingleBatch, StageError, StageResult, SystemConfig,
},
};
use alloc::boxed::Box;
use async_trait::async_trait;
use core::fmt::Debug;
use tracing::info;

pub trait AttributesBuilder {
/// Prepare the payload attributes.
fn prepare_payload_attributes(
&mut self,
l2_parent: L2BlockInfo,
epoch: BlockID,
) -> anyhow::Result<PayloadAttributes>;
}
mod deposits;
pub(crate) use deposits::derive_deposits;

mod builder;
pub use builder::{AttributesBuilder, StatefulAttributesBuilder};

/// [AttributesProvider] is a trait abstraction that generalizes the [BatchQueue] stage.
#[async_trait]
Expand Down Expand Up @@ -124,6 +121,7 @@ where
let mut attributes = self
.builder
.prepare_payload_attributes(parent, batch.epoch())
.await
.map_err(StageError::AttributesBuild)?;
attributes.no_tx_pool = true;
attributes.transactions.extend(batch.transactions);
Expand Down Expand Up @@ -173,7 +171,7 @@ mod tests {
stages::test_utils::{
new_attributes_provider, MockAttributesBuilder, MockAttributesProvider,
},
types::RawTransaction,
types::{BuilderError, RawTransaction},
};
use alloc::{vec, vec::Vec};
use alloy_primitives::b256;
Expand Down Expand Up @@ -264,7 +262,9 @@ mod tests {
let result = attributes_queue.create_next_attributes(batch, parent).await.unwrap_err();
assert_eq!(
result,
StageError::AttributesBuild(anyhow::anyhow!("missing payload attribute"))
StageError::AttributesBuild(BuilderError::Custom(anyhow::anyhow!(
"missing payload attribute"
)))
);
}

Expand Down
167 changes: 167 additions & 0 deletions crates/derive/src/stages/attributes_queue/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! The [`AttributesBuilder`] and it's default implementation.

use super::derive_deposits;
use crate::{
params::SEQUENCER_FEE_VAULT_ADDRESS,
traits::ChainProvider,
types::{
BlockID, BuilderError, EcotoneTransactionBuilder, L2BlockInfo, PayloadAttributes,
RawTransaction, RollupConfig, SystemConfig,
},
};
use alloc::{boxed::Box, fmt::Debug, sync::Arc, vec, vec::Vec};
use alloy_primitives::B256;
use async_trait::async_trait;

/// The [AttributesBuilder] is responsible for preparing [PayloadAttributes]
/// that can be used to construct an L2 Block containing only deposits.
#[async_trait]
pub trait AttributesBuilder {
/// Prepares a template [PayloadAttributes] that is ready to be used to build an L2 block.
/// The block will contain deposits only, on top of the given L2 parent, with the L1 origin
/// set to the given epoch.
/// By default, the [PayloadAttributes] template will have `no_tx_pool` set to true,
/// and no sequencer transactions. The caller has to modify the template to add transactions.
/// This can be done by either setting the `no_tx_pool` to false as sequencer, or by appending
/// batch transactions as the verifier.
async fn prepare_payload_attributes(
&mut self,
l2_parent: L2BlockInfo,
epoch: BlockID,
) -> Result<PayloadAttributes, BuilderError>;
}

/// The [SystemConfigL2Fetcher] fetches the system config by L2 hash.
pub trait SystemConfigL2Fetcher {
/// Fetch the system config by L2 hash.
fn system_config_by_l2_hash(&self, hash: B256) -> anyhow::Result<SystemConfig>;
}

/// A stateful implementation of the [AttributesBuilder].
#[derive(Debug, Default)]
pub struct StatefulAttributesBuilder<S, R>
where
S: SystemConfigL2Fetcher + Debug,
R: ChainProvider + Debug,
{
/// The rollup config.
rollup_cfg: Arc<RollupConfig>,
/// The system config fetcher.
config_fetcher: S,
/// The L1 receipts fetcher.
receipts_fetcher: R,
}

impl<S, R> StatefulAttributesBuilder<S, R>
where
S: SystemConfigL2Fetcher + Debug,
R: ChainProvider + Debug,
{
/// Create a new [StatefulAttributesBuilder] with the given epoch.
pub fn new(rcfg: Arc<RollupConfig>, cfg: S, receipts: R) -> Self {
Self { rollup_cfg: rcfg, config_fetcher: cfg, receipts_fetcher: receipts }
}
}

#[async_trait]
impl<S, R> AttributesBuilder for StatefulAttributesBuilder<S, R>
where
S: SystemConfigL2Fetcher + Send + Debug,
R: ChainProvider + Send + Debug,
{
async fn prepare_payload_attributes(
&mut self,
l2_parent: L2BlockInfo,
epoch: BlockID,
) -> Result<PayloadAttributes, BuilderError> {
let l1_header;
let deposit_transactions: Vec<RawTransaction>;
// let mut sequence_number = 0u64;
let mut sys_config =
self.config_fetcher.system_config_by_l2_hash(l2_parent.block_info.hash)?;

// If the L1 origin changed in this block, then we are in the first block of the epoch.
// In this case we need to fetch all transaction receipts from the L1 origin block so
// we can scan for user deposits.
if l2_parent.l1_origin.number != epoch.number {
let header = self.receipts_fetcher.header_by_hash(epoch.hash).await?;
if l2_parent.l1_origin.hash != header.parent_hash {
return Err(BuilderError::BlockMismatchEpochReset(
epoch,
l2_parent.l1_origin,
header.parent_hash,
));
}
let receipts = self.receipts_fetcher.receipts_by_hash(epoch.hash).await?;
sys_config.update_with_receipts(&receipts, &self.rollup_cfg, header.timestamp)?;
let deposits =
derive_deposits(epoch.hash, receipts, self.rollup_cfg.deposit_contract_address)
.await?;
l1_header = header;
deposit_transactions = deposits;
// sequence_number = 0;
} else {
#[allow(clippy::collapsible_else_if)]
if l2_parent.l1_origin.hash != epoch.hash {
return Err(BuilderError::BlockMismatch(epoch, l2_parent.l1_origin));
}

let header = self.receipts_fetcher.header_by_hash(epoch.hash).await?;
l1_header = header;
deposit_transactions = vec![];
// sequence_number = l2_parent.seq_num + 1;
}

// Sanity check the L1 origin was correctly selected to maintain the time invariant
// between L1 and L2.
let next_l2_time = l2_parent.block_info.timestamp + self.rollup_cfg.block_time;
if next_l2_time < l1_header.timestamp {
return Err(BuilderError::BrokenTimeInvariant(
l2_parent.l1_origin,
next_l2_time,
BlockID { hash: l1_header.hash_slow(), number: l1_header.number },
l1_header.timestamp,
));
}

let mut upgrade_transactions: Vec<RawTransaction> = vec![];
if self.rollup_cfg.is_ecotone_active(next_l2_time) {
upgrade_transactions =
EcotoneTransactionBuilder::build_txs().map_err(BuilderError::Custom)?;
}

// TODO(clabby): `L1BlockInfo` parsing from calldata.
// let l1_info_tx = l1_info_deposit_bytes(self.rollup_cfg, sys_config, sequence_number,
// l1_info, next_l2_time)?;

let mut txs =
Vec::with_capacity(1 + deposit_transactions.len() + upgrade_transactions.len());
// txs.push(l1_info_tx);
txs.extend(deposit_transactions);
txs.extend(upgrade_transactions);

let mut withdrawals = None;
if self.rollup_cfg.is_canyon_active(next_l2_time) {
withdrawals = Some(Vec::default());
}

let mut parent_beacon_root = None;
if self.rollup_cfg.is_ecotone_active(next_l2_time) {
// if the parent beacon root is not available, default to zero hash
parent_beacon_root = Some(l1_header.parent_beacon_block_root.unwrap_or_default());
}

Ok(PayloadAttributes {
timestamp: next_l2_time,
prev_randao: l1_header.mix_hash,
fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS,
transactions: txs,
no_tx_pool: true,
gas_limit: Some(u64::from_be_bytes(
alloy_primitives::U64::from(sys_config.gas_limit).to_be_bytes(),
)),
withdrawals,
parent_beacon_block_root: parent_beacon_root,
})
}
}
35 changes: 35 additions & 0 deletions crates/derive/src/stages/attributes_queue/deposits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Contains a helper method to derive deposit transactions from L1 Receipts.

use crate::{
params::DEPOSIT_EVENT_ABI_HASH,
types::{decode_deposit, DepositError, RawTransaction},
};
use alloc::vec::Vec;
use alloy_consensus::Receipt;
use alloy_primitives::{Address, Log, B256};

/// Derive deposits for transaction receipts.
///
/// Successful deposits must be emitted by the deposit contract and have the correct event
/// signature. So the receipt address must equal the specified deposit contract and the first topic
/// must be the [DEPOSIT_EVENT_ABI_HASH].
pub(crate) async fn derive_deposits(
block_hash: B256,
receipts: Vec<Receipt>,
deposit_contract: Address,
) -> anyhow::Result<Vec<RawTransaction>> {
let receipts = receipts.into_iter().filter(|r| r.status).collect::<Vec<_>>();
// Flatten the list of receipts into a list of logs.
let addr = |l: &Log| l.address == deposit_contract;
let topics = |l: &Log| l.data.topics().first().map_or(false, |i| *i == DEPOSIT_EVENT_ABI_HASH);
let filter_logs =
|r: Receipt| r.logs.into_iter().filter(|l| addr(l) && topics(l)).collect::<Vec<Log>>();
let logs = receipts.into_iter().flat_map(filter_logs).collect::<Vec<Log>>();
// TODO(refcell): are logs **and** receipts guaranteed to be _in order_?
// If not, we need to somehow get the index of each log in the block.
logs.iter()
.enumerate()
.map(|(i, l)| decode_deposit(block_hash, i, l))
.collect::<Result<Vec<_>, DepositError>>()
.map_err(|e| anyhow::anyhow!(e))
}
4 changes: 3 additions & 1 deletion crates/derive/src/stages/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ mod batch_queue;
pub use batch_queue::{BatchQueue, BatchQueueProvider};

mod attributes_queue;
pub use attributes_queue::{AttributesProvider, AttributesQueue};
pub use attributes_queue::{
AttributesBuilder, AttributesProvider, AttributesQueue, StatefulAttributesBuilder,
};

#[cfg(test)]
pub mod test_utils;
Loading