use crate::{
errors::{BuilderError, PipelineEncodingError, PipelineError, PipelineErrorKind},
traits::{AttributesBuilder, ChainProvider, L2ChainProvider},
types::PipelineResult,
};
use alloc::{boxed::Box, fmt::Debug, string::ToString, sync::Arc, vec, vec::Vec};
use alloy_consensus::{Eip658Value, Receipt};
use alloy_eips::{eip2718::Encodable2718, BlockNumHash};
use alloy_primitives::{address, Address, Bytes, B256};
use alloy_rlp::Encodable;
use alloy_rpc_types_engine::PayloadAttributes;
use async_trait::async_trait;
use op_alloy_consensus::Hardforks;
use op_alloy_genesis::RollupConfig;
use op_alloy_protocol::{decode_deposit, L1BlockInfoTx, L2BlockInfo, DEPOSIT_EVENT_ABI_HASH};
use op_alloy_rpc_types_engine::OpPayloadAttributes;
const SEQUENCER_FEE_VAULT_ADDRESS: Address = address!("4200000000000000000000000000000000000011");
#[derive(Debug, Default)]
pub struct StatefulAttributesBuilder<L1P, L2P>
where
L1P: ChainProvider + Debug,
L2P: L2ChainProvider + Debug,
{
rollup_cfg: Arc<RollupConfig>,
config_fetcher: L2P,
receipts_fetcher: L1P,
}
impl<L1P, L2P> StatefulAttributesBuilder<L1P, L2P>
where
L1P: ChainProvider + Debug,
L2P: L2ChainProvider + Debug,
{
pub const fn new(rcfg: Arc<RollupConfig>, sys_cfg_fetcher: L2P, receipts: L1P) -> Self {
Self { rollup_cfg: rcfg, config_fetcher: sys_cfg_fetcher, receipts_fetcher: receipts }
}
}
#[async_trait]
impl<L1P, L2P> AttributesBuilder for StatefulAttributesBuilder<L1P, L2P>
where
L1P: ChainProvider + Debug + Send,
L2P: L2ChainProvider + Debug + Send,
{
async fn prepare_payload_attributes(
&mut self,
l2_parent: L2BlockInfo,
epoch: BlockNumHash,
) -> PipelineResult<OpPayloadAttributes> {
let l1_header;
let deposit_transactions: Vec<Bytes>;
let mut sys_config = self
.config_fetcher
.system_config_by_number(l2_parent.block_info.number, self.rollup_cfg.clone())
.await
.map_err(Into::into)?;
let sequence_number = if l2_parent.l1_origin.number != epoch.number {
let header =
self.receipts_fetcher.header_by_hash(epoch.hash).await.map_err(Into::into)?;
if l2_parent.l1_origin.hash != header.parent_hash {
return Err(PipelineErrorKind::Reset(
BuilderError::BlockMismatchEpochReset(
epoch,
l2_parent.l1_origin,
header.parent_hash,
)
.into(),
));
}
let receipts =
self.receipts_fetcher.receipts_by_hash(epoch.hash).await.map_err(Into::into)?;
let deposits =
derive_deposits(epoch.hash, &receipts, self.rollup_cfg.deposit_contract_address)
.await
.map_err(|e| PipelineError::BadEncoding(e).crit())?;
sys_config
.update_with_receipts(
&receipts,
self.rollup_cfg.l1_system_config_address,
self.rollup_cfg.is_ecotone_active(header.timestamp),
)
.map_err(|e| PipelineError::SystemConfigUpdate(e).crit())?;
l1_header = header;
deposit_transactions = deposits;
0
} else {
#[allow(clippy::collapsible_else_if)]
if l2_parent.l1_origin.hash != epoch.hash {
return Err(PipelineErrorKind::Reset(
BuilderError::BlockMismatch(epoch, l2_parent.l1_origin).into(),
));
}
let header =
self.receipts_fetcher.header_by_hash(epoch.hash).await.map_err(Into::into)?;
l1_header = header;
deposit_transactions = vec![];
l2_parent.seq_num + 1
};
let next_l2_time = l2_parent.block_info.timestamp + self.rollup_cfg.block_time;
if next_l2_time < l1_header.timestamp {
return Err(PipelineErrorKind::Reset(
BuilderError::BrokenTimeInvariant(
l2_parent.l1_origin,
next_l2_time,
BlockNumHash { hash: l1_header.hash_slow(), number: l1_header.number },
l1_header.timestamp,
)
.into(),
));
}
let mut upgrade_transactions: Vec<Bytes> = vec![];
if self.rollup_cfg.is_ecotone_active(next_l2_time) &&
!self.rollup_cfg.is_ecotone_active(l2_parent.block_info.timestamp)
{
upgrade_transactions = Hardforks::ecotone_txs();
}
if self.rollup_cfg.is_fjord_active(next_l2_time) &&
!self.rollup_cfg.is_fjord_active(l2_parent.block_info.timestamp)
{
upgrade_transactions.append(&mut Hardforks::fjord_txs());
}
let (_, l1_info_tx_envelope) = L1BlockInfoTx::try_new_with_deposit_tx(
&self.rollup_cfg,
&sys_config,
sequence_number,
&l1_header,
next_l2_time,
)
.map_err(|e| {
PipelineError::AttributesBuilder(BuilderError::Custom(e.to_string())).crit()
})?;
let mut encoded_l1_info_tx = Vec::with_capacity(l1_info_tx_envelope.length());
l1_info_tx_envelope.encode_2718(&mut encoded_l1_info_tx);
let mut txs =
Vec::with_capacity(1 + deposit_transactions.len() + upgrade_transactions.len());
txs.push(encoded_l1_info_tx.into());
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) {
parent_beacon_root = Some(l1_header.parent_beacon_block_root.unwrap_or_default());
}
Ok(OpPayloadAttributes {
payload_attributes: PayloadAttributes {
timestamp: next_l2_time,
prev_randao: l1_header.mix_hash,
suggested_fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS,
parent_beacon_block_root: parent_beacon_root,
withdrawals,
},
transactions: Some(txs),
no_tx_pool: Some(true),
gas_limit: Some(u64::from_be_bytes(
alloy_primitives::U64::from(sys_config.gas_limit).to_be_bytes(),
)),
eip_1559_params: sys_config.eip_1559_params(
&self.rollup_cfg,
l2_parent.block_info.timestamp,
next_l2_time,
),
})
}
}
async fn derive_deposits(
block_hash: B256,
receipts: &[Receipt],
deposit_contract: Address,
) -> Result<Vec<Bytes>, PipelineEncodingError> {
let mut global_index = 0;
let mut res = Vec::new();
for r in receipts.iter() {
if Eip658Value::Eip658(false) == r.status {
continue;
}
for l in r.logs.iter() {
let curr_index = global_index;
global_index += 1;
if !l.data.topics().first().map_or(false, |i| *i == DEPOSIT_EVENT_ABI_HASH) {
continue;
}
if l.address != deposit_contract {
continue;
}
let decoded = decode_deposit(block_hash, curr_index, l)?;
res.push(decoded);
}
}
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
errors::ResetError,
test_utils::{TestChainProvider, TestSystemConfigL2Fetcher},
};
use alloc::vec;
use alloy_consensus::Header;
use alloy_primitives::{Log, LogData, B256, U256, U64};
use op_alloy_genesis::SystemConfig;
use op_alloy_protocol::{BlockInfo, DepositError};
fn generate_valid_log() -> Log {
let deposit_contract = address!("1111111111111111111111111111111111111111");
let mut data = vec![0u8; 192];
let offset: [u8; 8] = U64::from(32).to_be_bytes();
data[24..32].copy_from_slice(&offset);
let len: [u8; 8] = U64::from(128).to_be_bytes();
data[56..64].copy_from_slice(&len);
let mint: [u8; 16] = 10_u128.to_be_bytes();
data[80..96].copy_from_slice(&mint);
let value: [u8; 32] = U256::from(100).to_be_bytes();
data[96..128].copy_from_slice(&value);
let gas: [u8; 8] = 1000_u64.to_be_bytes();
data[128..136].copy_from_slice(&gas);
data[136] = 1;
let from = address!("2222222222222222222222222222222222222222");
let mut from_bytes = vec![0u8; 32];
from_bytes[12..32].copy_from_slice(from.as_slice());
let to = address!("3333333333333333333333333333333333333333");
let mut to_bytes = vec![0u8; 32];
to_bytes[12..32].copy_from_slice(to.as_slice());
Log {
address: deposit_contract,
data: LogData::new_unchecked(
vec![
DEPOSIT_EVENT_ABI_HASH,
B256::from_slice(&from_bytes),
B256::from_slice(&to_bytes),
B256::default(),
],
Bytes::from(data),
),
}
}
fn generate_valid_receipt() -> Receipt {
let mut bad_dest_log = generate_valid_log();
bad_dest_log.data.topics_mut()[1] = B256::default();
let mut invalid_topic_log = generate_valid_log();
invalid_topic_log.data.topics_mut()[0] = B256::default();
Receipt {
status: Eip658Value::Eip658(true),
logs: vec![generate_valid_log(), bad_dest_log, invalid_topic_log],
..Default::default()
}
}
#[tokio::test]
async fn test_derive_deposits_empty() {
let receipts = vec![];
let deposit_contract = Address::default();
let result = derive_deposits(B256::default(), &receipts, deposit_contract).await;
assert!(result.unwrap().is_empty());
}
#[tokio::test]
async fn test_derive_deposits_non_deposit_events_filtered_out() {
let deposit_contract = address!("1111111111111111111111111111111111111111");
let mut invalid = generate_valid_receipt();
invalid.logs[0].data = LogData::new_unchecked(vec![], Bytes::default());
let receipts = vec![generate_valid_receipt(), generate_valid_receipt(), invalid];
let result = derive_deposits(B256::default(), &receipts, deposit_contract).await;
assert_eq!(result.unwrap().len(), 5);
}
#[tokio::test]
async fn test_derive_deposits_non_deposit_contract_addr() {
let deposit_contract = address!("1111111111111111111111111111111111111111");
let mut invalid = generate_valid_receipt();
invalid.logs[0].address = Address::default();
let receipts = vec![generate_valid_receipt(), generate_valid_receipt(), invalid];
let result = derive_deposits(B256::default(), &receipts, deposit_contract).await;
assert_eq!(result.unwrap().len(), 5);
}
#[tokio::test]
async fn test_derive_deposits_decoding_errors() {
let deposit_contract = address!("1111111111111111111111111111111111111111");
let mut invalid = generate_valid_receipt();
invalid.logs[0].data =
LogData::new_unchecked(vec![DEPOSIT_EVENT_ABI_HASH], Bytes::default());
let receipts = vec![generate_valid_receipt(), generate_valid_receipt(), invalid];
let result = derive_deposits(B256::default(), &receipts, deposit_contract).await;
let downcasted = result.unwrap_err();
assert_eq!(downcasted, DepositError::UnexpectedTopicsLen(1).into());
}
#[tokio::test]
async fn test_derive_deposits_succeeds() {
let deposit_contract = address!("1111111111111111111111111111111111111111");
let receipts = vec![generate_valid_receipt(), generate_valid_receipt()];
let result = derive_deposits(B256::default(), &receipts, deposit_contract).await;
assert_eq!(result.unwrap().len(), 4);
}
#[tokio::test]
async fn test_prepare_payload_block_mismatch_epoch_reset() {
let cfg = Arc::new(RollupConfig::default());
let l2_number = 1;
let mut fetcher = TestSystemConfigL2Fetcher::default();
fetcher.insert(l2_number, SystemConfig::default());
let mut provider = TestChainProvider::default();
let header = Header::default();
let hash = header.hash_slow();
provider.insert_header(hash, header);
let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider);
let epoch = BlockNumHash { hash, number: l2_number };
let l2_parent = L2BlockInfo {
block_info: BlockInfo { hash: B256::ZERO, number: l2_number, ..Default::default() },
l1_origin: BlockNumHash { hash: B256::left_padding_from(&[0xFF]), number: 2 },
seq_num: 0,
};
let expected =
BuilderError::BlockMismatchEpochReset(epoch, l2_parent.l1_origin, B256::default());
let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err();
assert_eq!(err, PipelineErrorKind::Reset(expected.into()));
}
#[tokio::test]
async fn test_prepare_payload_block_mismatch() {
let cfg = Arc::new(RollupConfig::default());
let l2_number = 1;
let mut fetcher = TestSystemConfigL2Fetcher::default();
fetcher.insert(l2_number, SystemConfig::default());
let mut provider = TestChainProvider::default();
let header = Header::default();
let hash = header.hash_slow();
provider.insert_header(hash, header);
let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider);
let epoch = BlockNumHash { hash, number: l2_number };
let l2_parent = L2BlockInfo {
block_info: BlockInfo { hash: B256::ZERO, number: l2_number, ..Default::default() },
l1_origin: BlockNumHash { hash: B256::ZERO, number: l2_number },
seq_num: 0,
};
let expected = BuilderError::BlockMismatch(epoch, l2_parent.l1_origin);
let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err();
assert_eq!(err, PipelineErrorKind::Reset(ResetError::AttributesBuilder(expected)));
}
#[tokio::test]
async fn test_prepare_payload_broken_time_invariant() {
let block_time = 10;
let timestamp = 100;
let cfg = Arc::new(RollupConfig { block_time, ..Default::default() });
let l2_number = 1;
let mut fetcher = TestSystemConfigL2Fetcher::default();
fetcher.insert(l2_number, SystemConfig::default());
let mut provider = TestChainProvider::default();
let header = Header { timestamp, ..Default::default() };
let hash = header.hash_slow();
provider.insert_header(hash, header);
let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider);
let epoch = BlockNumHash { hash, number: l2_number };
let l2_parent = L2BlockInfo {
block_info: BlockInfo { hash: B256::ZERO, number: l2_number, ..Default::default() },
l1_origin: BlockNumHash { hash, number: l2_number },
seq_num: 0,
};
let next_l2_time = l2_parent.block_info.timestamp + block_time;
let block_id = BlockNumHash { hash, number: 0 };
let expected = BuilderError::BrokenTimeInvariant(
l2_parent.l1_origin,
next_l2_time,
block_id,
timestamp,
);
let err = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap_err();
assert_eq!(err, PipelineErrorKind::Reset(ResetError::AttributesBuilder(expected)));
}
#[tokio::test]
async fn test_prepare_payload_without_forks() {
let block_time = 10;
let timestamp = 100;
let cfg = Arc::new(RollupConfig { block_time, ..Default::default() });
let l2_number = 1;
let mut fetcher = TestSystemConfigL2Fetcher::default();
fetcher.insert(l2_number, SystemConfig::default());
let mut provider = TestChainProvider::default();
let header = Header { timestamp, ..Default::default() };
let prev_randao = header.mix_hash;
let hash = header.hash_slow();
provider.insert_header(hash, header);
let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider);
let epoch = BlockNumHash { hash, number: l2_number };
let l2_parent = L2BlockInfo {
block_info: BlockInfo {
hash: B256::ZERO,
number: l2_number,
timestamp,
parent_hash: hash,
},
l1_origin: BlockNumHash { hash, number: l2_number },
seq_num: 0,
};
let next_l2_time = l2_parent.block_info.timestamp + block_time;
let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap();
let expected = OpPayloadAttributes {
payload_attributes: PayloadAttributes {
timestamp: next_l2_time,
prev_randao,
suggested_fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS,
parent_beacon_block_root: None,
withdrawals: None,
},
transactions: payload.transactions.clone(),
no_tx_pool: Some(true),
gas_limit: Some(u64::from_be_bytes(
alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(),
)),
eip_1559_params: None,
};
assert_eq!(payload, expected);
assert_eq!(payload.transactions.unwrap().len(), 1);
}
#[tokio::test]
async fn test_prepare_payload_with_canyon() {
let block_time = 10;
let timestamp = 100;
let cfg = Arc::new(RollupConfig { block_time, canyon_time: Some(0), ..Default::default() });
let l2_number = 1;
let mut fetcher = TestSystemConfigL2Fetcher::default();
fetcher.insert(l2_number, SystemConfig::default());
let mut provider = TestChainProvider::default();
let header = Header { timestamp, ..Default::default() };
let prev_randao = header.mix_hash;
let hash = header.hash_slow();
provider.insert_header(hash, header);
let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider);
let epoch = BlockNumHash { hash, number: l2_number };
let l2_parent = L2BlockInfo {
block_info: BlockInfo {
hash: B256::ZERO,
number: l2_number,
timestamp,
parent_hash: hash,
},
l1_origin: BlockNumHash { hash, number: l2_number },
seq_num: 0,
};
let next_l2_time = l2_parent.block_info.timestamp + block_time;
let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap();
let expected = OpPayloadAttributes {
payload_attributes: PayloadAttributes {
timestamp: next_l2_time,
prev_randao,
suggested_fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS,
parent_beacon_block_root: None,
withdrawals: Some(Vec::default()),
},
transactions: payload.transactions.clone(),
no_tx_pool: Some(true),
gas_limit: Some(u64::from_be_bytes(
alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(),
)),
eip_1559_params: None,
};
assert_eq!(payload, expected);
assert_eq!(payload.transactions.unwrap().len(), 1);
}
#[tokio::test]
async fn test_prepare_payload_with_ecotone() {
let block_time = 2;
let timestamp = 100;
let cfg =
Arc::new(RollupConfig { block_time, ecotone_time: Some(102), ..Default::default() });
let l2_number = 1;
let mut fetcher = TestSystemConfigL2Fetcher::default();
fetcher.insert(l2_number, SystemConfig::default());
let mut provider = TestChainProvider::default();
let header = Header { timestamp, ..Default::default() };
let parent_beacon_block_root = Some(header.parent_beacon_block_root.unwrap_or_default());
let prev_randao = header.mix_hash;
let hash = header.hash_slow();
provider.insert_header(hash, header);
let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider);
let epoch = BlockNumHash { hash, number: l2_number };
let l2_parent = L2BlockInfo {
block_info: BlockInfo {
hash: B256::ZERO,
number: l2_number,
timestamp,
parent_hash: hash,
},
l1_origin: BlockNumHash { hash, number: l2_number },
seq_num: 0,
};
let next_l2_time = l2_parent.block_info.timestamp + block_time;
let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap();
let expected = OpPayloadAttributes {
payload_attributes: PayloadAttributes {
timestamp: next_l2_time,
prev_randao,
suggested_fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS,
parent_beacon_block_root,
withdrawals: Some(vec![]),
},
transactions: payload.transactions.clone(),
no_tx_pool: Some(true),
gas_limit: Some(u64::from_be_bytes(
alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(),
)),
eip_1559_params: None,
};
assert_eq!(payload, expected);
assert_eq!(payload.transactions.unwrap().len(), 7);
}
#[tokio::test]
async fn test_prepare_payload_with_fjord() {
let block_time = 2;
let timestamp = 100;
let cfg =
Arc::new(RollupConfig { block_time, fjord_time: Some(102), ..Default::default() });
let l2_number = 1;
let mut fetcher = TestSystemConfigL2Fetcher::default();
fetcher.insert(l2_number, SystemConfig::default());
let mut provider = TestChainProvider::default();
let header = Header { timestamp, ..Default::default() };
let prev_randao = header.mix_hash;
let hash = header.hash_slow();
provider.insert_header(hash, header);
let mut builder = StatefulAttributesBuilder::new(cfg, fetcher, provider);
let epoch = BlockNumHash { hash, number: l2_number };
let l2_parent = L2BlockInfo {
block_info: BlockInfo {
hash: B256::ZERO,
number: l2_number,
timestamp,
parent_hash: hash,
},
l1_origin: BlockNumHash { hash, number: l2_number },
seq_num: 0,
};
let next_l2_time = l2_parent.block_info.timestamp + block_time;
let payload = builder.prepare_payload_attributes(l2_parent, epoch).await.unwrap();
let expected = OpPayloadAttributes {
payload_attributes: PayloadAttributes {
timestamp: next_l2_time,
prev_randao,
suggested_fee_recipient: SEQUENCER_FEE_VAULT_ADDRESS,
parent_beacon_block_root: Some(B256::ZERO),
withdrawals: Some(vec![]),
},
transactions: payload.transactions.clone(),
no_tx_pool: Some(true),
gas_limit: Some(u64::from_be_bytes(
alloy_primitives::U64::from(SystemConfig::default().gas_limit).to_be_bytes(),
)),
eip_1559_params: None,
};
assert_eq!(payload.transactions.as_ref().unwrap().len(), 10);
assert_eq!(payload, expected);
}
}