diff --git a/guests/op-derive/src/main.rs b/guests/op-derive/src/main.rs index c87edeae..55ec8374 100644 --- a/guests/op-derive/src/main.rs +++ b/guests/op-derive/src/main.rs @@ -15,15 +15,13 @@ #![no_main] use risc0_zkvm::guest::env; -use zeth_lib::optimism::{ - batcher_db::MemDb, config::OPTIMISM_CHAIN_SPEC, DeriveInput, DeriveMachine, -}; +use zeth_lib::optimism::{batcher_db::MemDb, config::ChainConfig, DeriveInput, DeriveMachine}; risc0_zkvm::guest::entry!(main); pub fn main() { let derive_input: DeriveInput = env::read(); - let mut derive_machine = DeriveMachine::new(&OPTIMISM_CHAIN_SPEC, derive_input, None) + let mut derive_machine = DeriveMachine::new(ChainConfig::optimism(), derive_input, None) .expect("Could not create derive machine"); let output = derive_machine .derive(None) diff --git a/host/src/operations/rollups.rs b/host/src/operations/rollups.rs index 60ac4e74..4c9348a5 100644 --- a/host/src/operations/rollups.rs +++ b/host/src/operations/rollups.rs @@ -26,7 +26,7 @@ use zeth_lib::{ optimism::{ batcher_db::BatcherDb, composition::{ComposeInput, ComposeInputOperation, ComposeOutputOperation}, - config::OPTIMISM_CHAIN_SPEC, + config::ChainConfig, DeriveInput, DeriveMachine, }, output::BlockBuildOutput, @@ -52,8 +52,10 @@ pub async fn derive_rollup_blocks(cli: &Cli) -> anyhow::Result anyhow::Result anyhow::Result = Vec::new(); for op_block_index in (0..build_args.block_count).step_by(composition_size as usize) { + let config = ChainConfig::optimism(); let db = RpcDb::new( + &config, build_args.eth_rpc_url.clone(), build_args.op_rpc_url.clone(), build_args.cache.clone(), @@ -173,7 +176,7 @@ pub async fn compose_derived_rollup_blocks( }; let factory_clone = op_builder_provider_factory.clone(); let mut derive_machine = tokio::task::spawn_blocking(move || { - DeriveMachine::new(&OPTIMISM_CHAIN_SPEC, derive_input, Some(factory_clone)) + DeriveMachine::new(config, derive_input, Some(factory_clone)) .expect("Could not create derive machine") }) .await?; @@ -232,7 +235,7 @@ pub async fn compose_derived_rollup_blocks( let input_clone = derive_input_mem.clone(); let output_mem = tokio::task::spawn_blocking(move || { DeriveMachine::new( - &OPTIMISM_CHAIN_SPEC, + ChainConfig::optimism(), input_clone, Some(op_builder_provider_factory), ) diff --git a/host/testdata/optimism/115892782.json.gz b/host/testdata/optimism/115892782.json.gz new file mode 100644 index 00000000..7701d202 Binary files /dev/null and b/host/testdata/optimism/115892782.json.gz differ diff --git a/lib/src/builder/execute/ethereum.rs b/lib/src/builder/execute/ethereum.rs index 872bdadb..ec67145a 100644 --- a/lib/src/builder/execute/ethereum.rs +++ b/lib/src/builder/execute/ethereum.rs @@ -37,9 +37,6 @@ use zeth_primitives::{ use super::TxExecStrategy; use crate::{builder::BlockBuilder, consts, guest_mem_forget}; -/// Minimum supported protocol version: Paris (Block no. 15537394). -const MIN_SPEC_ID: SpecId = SpecId::MERGE; - pub struct EthTxExecStrategy {} impl TxExecStrategy for EthTxExecStrategy { @@ -50,18 +47,11 @@ impl TxExecStrategy for EthTxExecStrategy { D: Database + DatabaseCommit, ::Error: Debug, { + let spec_id = block_builder.spec_id.expect("Spec ID is not initialized"); let header = block_builder .header .as_mut() .expect("Header is not initialized"); - // Compute the spec id - let spec_id = block_builder.chain_spec.spec_id(header.number); - if !SpecId::enabled(spec_id, MIN_SPEC_ID) { - panic!( - "Invalid protocol version: expected >= {:?}, got {:?}", - MIN_SPEC_ID, spec_id, - ) - } #[cfg(not(target_os = "zkvm"))] { diff --git a/lib/src/builder/execute/optimism.rs b/lib/src/builder/execute/optimism.rs index 8518f486..cd9fab00 100644 --- a/lib/src/builder/execute/optimism.rs +++ b/lib/src/builder/execute/optimism.rs @@ -16,7 +16,7 @@ use core::{fmt::Debug, mem::take}; use anyhow::{anyhow, bail, Context, Result}; #[cfg(not(target_os = "zkvm"))] -use log::{debug, trace}; +use log::trace; use revm::{ interpreter::Host, optimism, @@ -32,16 +32,13 @@ use zeth_primitives::{ optimism::{OptimismTxEssence, TxEssenceOptimismDeposited}, TxEssence, }, - trie::MptNode, + trie::{MptNode, EMPTY_ROOT}, Bloom, Bytes, }; use super::{ethereum, TxExecStrategy}; use crate::{builder::BlockBuilder, consts, guest_mem_forget}; -/// Minimum supported protocol version: Bedrock (Block no. 105235063). -const MIN_SPEC_ID: SpecId = SpecId::BEDROCK; - pub struct OpTxExecStrategy {} impl TxExecStrategy for OpTxExecStrategy { @@ -52,19 +49,11 @@ impl TxExecStrategy for OpTxExecStrategy { D: Database + DatabaseCommit, ::Error: Debug, { + let spec_id = block_builder.spec_id.expect("Spec ID is not initialized"); let header = block_builder .header .as_mut() .expect("Header is not initialized"); - // Compute the spec id - let spec_id = block_builder.chain_spec.spec_id(header.number); - if !SpecId::enabled(spec_id, MIN_SPEC_ID) { - panic!( - "Invalid protocol version: expected >= {:?}, got {:?}", - MIN_SPEC_ID, spec_id, - ) - } - let chain_id = block_builder.chain_spec.chain_id(); #[cfg(not(target_os = "zkvm"))] { @@ -81,9 +70,9 @@ impl TxExecStrategy for OpTxExecStrategy { ) .unwrap(); - debug!("Block no. {}", header.number); - debug!(" EVM spec ID: {:?}", spec_id); - debug!(" Timestamp: {}", dt); + trace!("Block no. {}", header.number); + trace!(" EVM spec ID: {:?}", spec_id); + trace!(" Timestamp: {}", dt); trace!( " Transactions: {}", block_builder.input.state_input.transactions.len() @@ -100,6 +89,7 @@ impl TxExecStrategy for OpTxExecStrategy { ); } + let chain_id = block_builder.chain_spec.chain_id(); let mut evm = Evm::builder() .spec_id(spec_id) .modify_cfg_env(|cfg_env| { @@ -154,6 +144,15 @@ impl TxExecStrategy for OpTxExecStrategy { bail!("Error at transaction {}: gas exceeds block limit", tx_no); } + // cache account nonce if the transaction is a deposit, starting with Canyon + let deposit_nonce = (spec_id >= SpecId::CANYON + && matches!(tx.essence, OptimismTxEssence::OptimismDeposited(_))) + .then(|| { + let db = &mut evm.context.evm.db; + let account = db.basic(tx_from).expect("Depositor account not found"); + account.unwrap_or_default().nonce + }); + match &tx.essence { OptimismTxEssence::OptimismDeposited(deposit) => { #[cfg(not(target_os = "zkvm"))] @@ -185,12 +184,15 @@ impl TxExecStrategy for OpTxExecStrategy { trace!(" Ok: {:?}", result); // create the receipt from the EVM result - let receipt = Receipt::new( + let mut receipt = Receipt::new( tx.essence.tx_type(), result.is_success(), cumulative_gas_used, result.logs().into_iter().map(|log| log.into()).collect(), ); + if let Some(nonce) = deposit_nonce { + receipt = receipt.with_deposit_nonce(nonce); + } // update account states #[cfg(not(target_os = "zkvm"))] @@ -244,7 +246,11 @@ impl TxExecStrategy for OpTxExecStrategy { header.receipts_root = receipt_trie.hash(); header.logs_bloom = logs_bloom; header.gas_used = cumulative_gas_used; - header.withdrawals_root = None; + header.withdrawals_root = if spec_id < SpecId::CANYON { + None + } else { + Some(EMPTY_ROOT) + }; // Leak memory, save cycles guest_mem_forget([tx_trie, receipt_trie]); diff --git a/lib/src/builder/mod.rs b/lib/src/builder/mod.rs index b7647223..ff64d866 100644 --- a/lib/src/builder/mod.rs +++ b/lib/src/builder/mod.rs @@ -16,7 +16,7 @@ use std::sync::{Arc, Mutex}; use anyhow::Result; -use revm::{Database, DatabaseCommit}; +use revm::{primitives::SpecId, Database, DatabaseCommit}; use serde::Serialize; use zeth_primitives::{ block::Header, @@ -53,6 +53,7 @@ pub struct BlockBuilder<'a, D, E: TxEssence> { pub(crate) chain_spec: &'a ChainSpec, pub(crate) input: BlockBuildInput, pub(crate) db: Option, + pub(crate) spec_id: Option, pub(crate) header: Option
, pub db_drop_destination: Option>, } @@ -86,6 +87,7 @@ where BlockBuilder { chain_spec, db: None, + spec_id: None, header: None, input, db_drop_destination: db_backup, diff --git a/lib/src/builder/prepare.rs b/lib/src/builder/prepare.rs index e38a1f7d..e8a4450d 100644 --- a/lib/src/builder/prepare.rs +++ b/lib/src/builder/prepare.rs @@ -65,9 +65,8 @@ impl HeaderPrepStrategy for EthHeaderPrepStrategy { ); } // Validate timestamp - if block_builder.input.state_input.timestamp - <= block_builder.input.state_input.parent_header.timestamp - { + let timestamp = block_builder.input.state_input.timestamp; + if timestamp <= block_builder.input.state_input.parent_header.timestamp { bail!( "Invalid timestamp: expected > {}, got {}", block_builder.input.state_input.parent_header.timestamp, @@ -83,6 +82,18 @@ impl HeaderPrepStrategy for EthHeaderPrepStrategy { extra_data_bytes, ) } + // Validate number + let parent_number = block_builder.input.state_input.parent_header.number; + let number = parent_number + .checked_add(1) + .context("Invalid number: too large")?; + + // Derive fork version + let spec_id = block_builder + .chain_spec + .active_fork(number, ×tamp) + .unwrap_or_else(|err| panic!("Invalid version: {:#}", err)); + block_builder.spec_id = Some(spec_id); // Derive header block_builder.header = Some(Header { // Initialize fields that we can compute from the parent @@ -96,12 +107,12 @@ impl HeaderPrepStrategy for EthHeaderPrepStrategy { .context("Invalid block number: too large")?, base_fee_per_gas: derive_base_fee( &block_builder.input.state_input.parent_header, - block_builder.chain_spec.gas_constants(), + block_builder.chain_spec.gas_constants(spec_id).unwrap(), ), // Initialize metadata from input beneficiary: block_builder.input.state_input.beneficiary, gas_limit: block_builder.input.state_input.gas_limit, - timestamp: block_builder.input.state_input.timestamp, + timestamp, mix_hash: block_builder.input.state_input.mix_hash, extra_data: block_builder.input.state_input.extra_data.clone(), // do not fill the remaining fields diff --git a/lib/src/consts.rs b/lib/src/consts.rs index 59555405..947275d9 100644 --- a/lib/src/consts.rs +++ b/lib/src/consts.rs @@ -17,6 +17,7 @@ extern crate alloc; use std::collections::BTreeMap; +use anyhow::{bail, Result}; use once_cell::sync::Lazy; use revm::primitives::SpecId; use serde::{Deserialize, Serialize}; @@ -42,57 +43,78 @@ pub const MAX_BLOCK_HASH_AGE: u64 = 256; pub const GWEI_TO_WEI: U256 = uint!(1_000_000_000_U256); /// The Ethereum mainnet specification. -pub static ETH_MAINNET_CHAIN_SPEC: Lazy = Lazy::new(|| { - ChainSpec { - chain_id: 1, - hard_forks: BTreeMap::from([ - (SpecId::FRONTIER, ForkCondition::Block(0)), - // previous versions not supported - (SpecId::MERGE, ForkCondition::Block(15537394)), - (SpecId::SHANGHAI, ForkCondition::Block(17034870)), - (SpecId::CANCUN, ForkCondition::TBD), - ]), - eip_1559_constants: Eip1559Constants { - base_fee_change_denominator: uint!(8_U256), - base_fee_max_increase_denominator: uint!(8_U256), - base_fee_max_decrease_denominator: uint!(8_U256), - elasticity_multiplier: uint!(2_U256), - }, - } +pub static ETH_MAINNET_CHAIN_SPEC: Lazy = Lazy::new(|| ChainSpec { + chain_id: 1, + max_spec_id: SpecId::SHANGHAI, + hard_forks: BTreeMap::from([ + (SpecId::MERGE, ForkCondition::Block(15537394)), + (SpecId::SHANGHAI, ForkCondition::Timestamp(1681338455)), + (SpecId::CANCUN, ForkCondition::Timestamp(1710338135)), + ]), + gas_constants: BTreeMap::from([(SpecId::LONDON, ETH_MAINNET_EIP1559_CONSTANTS)]), }); +/// The Ethereum mainnet EIP-1559 gas constants. +pub const ETH_MAINNET_EIP1559_CONSTANTS: Eip1559Constants = Eip1559Constants { + base_fee_change_denominator: uint!(8_U256), + base_fee_max_increase_denominator: uint!(8_U256), + base_fee_max_decrease_denominator: uint!(8_U256), + elasticity_multiplier: uint!(2_U256), +}; + /// The Optimism mainnet specification. pub static OP_MAINNET_CHAIN_SPEC: Lazy = Lazy::new(|| ChainSpec { chain_id: 10, + max_spec_id: SpecId::CANYON, hard_forks: BTreeMap::from([ - (SpecId::FRONTIER, ForkCondition::Block(0)), - // previous versions not supported - (SpecId::BEDROCK, ForkCondition::Block(105235063)), + (SpecId::BEDROCK, ForkCondition::Timestamp(1679079600)), // Regolith is activated from day 1 of Bedrock on mainnet - (SpecId::REGOLITH, ForkCondition::Block(105235063)), + (SpecId::REGOLITH, ForkCondition::Timestamp(1679079600)), + // Canyon is activated 2024-01-11 at 17:00:01 UTC + (SpecId::CANYON, ForkCondition::Timestamp(1704992401)), + // Delta is activated 2024-02-22 at 17:00:01 UTC + (SpecId::LATEST, ForkCondition::Timestamp(1708560000)), + ]), + gas_constants: BTreeMap::from([ + ( + SpecId::BEDROCK, + Eip1559Constants { + base_fee_change_denominator: uint!(50_U256), + base_fee_max_increase_denominator: uint!(10_U256), + base_fee_max_decrease_denominator: uint!(50_U256), + elasticity_multiplier: uint!(6_U256), + }, + ), + ( + SpecId::CANYON, + Eip1559Constants { + base_fee_change_denominator: uint!(250_U256), + base_fee_max_increase_denominator: uint!(10_U256), + base_fee_max_decrease_denominator: uint!(50_U256), + elasticity_multiplier: uint!(6_U256), + }, + ), ]), - eip_1559_constants: Eip1559Constants { - base_fee_change_denominator: uint!(50_U256), - base_fee_max_increase_denominator: uint!(10_U256), - base_fee_max_decrease_denominator: uint!(50_U256), - elasticity_multiplier: uint!(6_U256), - }, }); /// The condition at which a fork is activated. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub enum ForkCondition { /// The fork is activated with a certain block. Block(BlockNumber), - /// The fork is not yet active. + /// The fork is activated with a specific timestamp. + Timestamp(u64), + /// The fork is never activated + #[default] TBD, } impl ForkCondition { /// Returns whether the condition has been met. - pub fn active(&self, block_number: BlockNumber) -> bool { + pub fn active(&self, block_number: BlockNumber, timestamp: u64) -> bool { match self { ForkCondition::Block(block) => *block <= block_number, + ForkCondition::Timestamp(ts) => *ts <= timestamp, ForkCondition::TBD => false, } } @@ -107,24 +129,13 @@ pub struct Eip1559Constants { pub elasticity_multiplier: U256, } -impl Default for Eip1559Constants { - /// Defaults to Ethereum network values - fn default() -> Self { - Self { - base_fee_change_denominator: uint!(8_U256), - base_fee_max_increase_denominator: uint!(8_U256), - base_fee_max_decrease_denominator: uint!(8_U256), - elasticity_multiplier: uint!(2_U256), - } - } -} - /// Specification of a specific chain. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChainSpec { chain_id: ChainId, + max_spec_id: SpecId, hard_forks: BTreeMap, - eip_1559_constants: Eip1559Constants, + gas_constants: BTreeMap, } impl ChainSpec { @@ -136,26 +147,44 @@ impl ChainSpec { ) -> Self { ChainSpec { chain_id, + max_spec_id: spec_id, hard_forks: BTreeMap::from([(spec_id, ForkCondition::Block(0))]), - eip_1559_constants, + gas_constants: BTreeMap::from([(spec_id, eip_1559_constants)]), } } /// Returns the network chain ID. pub fn chain_id(&self) -> ChainId { self.chain_id } - /// Returns the revm specification ID for `block_number`. - pub fn spec_id(&self, block_number: BlockNumber) -> SpecId { - for (spec_id, fork) in self.hard_forks.iter().rev() { - if fork.active(block_number) { - return *spec_id; + /// Returns the [SpecId] for a given block number and timestamp or an error if not + /// supported. + pub fn active_fork(&self, block_number: BlockNumber, timestamp: &U256) -> Result { + match self.spec_id(block_number, timestamp.saturating_to()) { + Some(spec_id) => { + if spec_id > self.max_spec_id { + bail!("expected <= {:?}, got {:?}", self.max_spec_id, spec_id); + } else { + Ok(spec_id) + } } + None => bail!("no supported fork for block {}", block_number), } - unreachable!() } - /// Returns the Eip1559 constants - pub fn gas_constants(&self) -> &Eip1559Constants { - &self.eip_1559_constants + /// Returns the Eip1559 constants for a given [SpecId]. + pub fn gas_constants(&self, spec_id: SpecId) -> Option<&Eip1559Constants> { + self.gas_constants + .range(..=spec_id) + .next_back() + .map(|(_, v)| v) + } + + fn spec_id(&self, block_number: BlockNumber, timestamp: u64) -> Option { + for (spec_id, fork) in self.hard_forks.iter().rev() { + if fork.active(block_number, timestamp) { + return Some(*spec_id); + } + } + None } } @@ -164,10 +193,32 @@ mod tests { use super::*; #[test] - fn revm_spec_id() { - assert!(ETH_MAINNET_CHAIN_SPEC.spec_id(15537393) < SpecId::MERGE); - assert_eq!(ETH_MAINNET_CHAIN_SPEC.spec_id(15537394), SpecId::MERGE); - assert_eq!(ETH_MAINNET_CHAIN_SPEC.spec_id(17034869), SpecId::MERGE); - assert_eq!(ETH_MAINNET_CHAIN_SPEC.spec_id(17034870), SpecId::SHANGHAI); + fn spec_id() { + assert_eq!(ETH_MAINNET_CHAIN_SPEC.spec_id(15537393, 0), None); + assert_eq!( + ETH_MAINNET_CHAIN_SPEC.spec_id(15537394, 0), + Some(SpecId::MERGE) + ); + assert_eq!( + ETH_MAINNET_CHAIN_SPEC.spec_id(17034869, 0), + Some(SpecId::MERGE) + ); + assert_eq!( + ETH_MAINNET_CHAIN_SPEC.spec_id(0, 1681338455), + Some(SpecId::SHANGHAI) + ); + } + + #[test] + fn gas_constants() { + assert_eq!(ETH_MAINNET_CHAIN_SPEC.gas_constants(SpecId::BERLIN), None); + assert_eq!( + ETH_MAINNET_CHAIN_SPEC.gas_constants(SpecId::MERGE), + Some(Ð_MAINNET_EIP1559_CONSTANTS) + ); + assert_eq!( + ETH_MAINNET_CHAIN_SPEC.gas_constants(SpecId::SHANGHAI), + Some(Ð_MAINNET_EIP1559_CONSTANTS) + ); } } diff --git a/lib/src/host/rpc_db.rs b/lib/src/host/rpc_db.rs index a9e67997..2a4cba0b 100644 --- a/lib/src/host/rpc_db.rs +++ b/lib/src/host/rpc_db.rs @@ -18,13 +18,15 @@ use anyhow::Context; use zeth_primitives::{ block::Header, transactions::{ethereum::EthereumTxEssence, optimism::OptimismTxEssence}, + Address, }; use crate::{ host::provider::{new_provider, BlockQuery}, optimism::{ batcher_db::{BatcherDb, BlockInput, MemDb}, - config::OPTIMISM_CHAIN_SPEC, + config::ChainConfig, + deposits, system_config, }, }; @@ -48,6 +50,8 @@ fn op_cache_path(cache: &Option, block_no: u64) -> Option { } pub struct RpcDb { + deposit_contract: Address, + system_config_contract: Address, eth_rpc_url: Option, op_rpc_url: Option, cache: Option, @@ -56,11 +60,14 @@ pub struct RpcDb { impl RpcDb { pub fn new( + config: &ChainConfig, eth_rpc_url: Option, op_rpc_url: Option, cache: Option, ) -> Self { RpcDb { + deposit_contract: config.deposit_contract, + system_config_contract: config.system_config_contract, eth_rpc_url, op_rpc_url, cache, @@ -74,7 +81,7 @@ impl RpcDb { } impl BatcherDb for RpcDb { - fn validate(&self) -> anyhow::Result<()> { + fn validate(&self, _: &ChainConfig) -> anyhow::Result<()> { Ok(()) } @@ -130,14 +137,10 @@ impl BatcherDb for RpcDb { let ethers_block = provider.get_full_block(&query)?; let block_header: Header = ethers_block.clone().try_into().unwrap(); // include receipts when needed - let can_contain_deposits = crate::optimism::deposits::can_contain( - &OPTIMISM_CHAIN_SPEC.deposit_contract, - &block_header.logs_bloom, - ); - let can_contain_config = crate::optimism::system_config::can_contain( - &OPTIMISM_CHAIN_SPEC.system_config_contract, - &block_header.logs_bloom, - ); + let can_contain_deposits = + deposits::can_contain(&self.deposit_contract, &block_header.logs_bloom); + let can_contain_config = + system_config::can_contain(&self.system_config_contract, &block_header.logs_bloom); let receipts = if can_contain_config || can_contain_deposits { let receipts = provider.get_block_receipts(&query)?; Some( diff --git a/lib/src/optimism/batcher.rs b/lib/src/optimism/batcher.rs index 96da4fa6..0166f58e 100644 --- a/lib/src/optimism/batcher.rs +++ b/lib/src/optimism/batcher.rs @@ -16,6 +16,7 @@ use core::cmp::Ordering; use std::collections::{BTreeMap, VecDeque}; use anyhow::{bail, ensure, Context, Result}; +use revm::primitives::SpecId; use serde::{Deserialize, Serialize}; use zeth_primitives::{ batch::{Batch, BatchEssence}, @@ -124,11 +125,15 @@ pub struct BatchWithInclusion { } pub struct Batcher { + config: ChainConfig, + spec_id: SpecId, + + /// The current state of the batch derivation. + pub state: State, + /// Multimap of batches, keyed by timestamp batches: BTreeMap>, batcher_channel: BatcherChannels, - pub state: State, - pub config: ChainConfig, } impl Batcher { @@ -136,11 +141,13 @@ impl Batcher { config: ChainConfig, op_head: L2BlockInfo, eth_block: &BlockInput, - ) -> Result { - let eth_block_hash = eth_block.block_header.hash(); + ) -> Result { + let timestamp = eth_block.block_header.timestamp; + let spec_id = config.chain_spec.active_fork(0, ×tamp)?; - let batcher_channel = BatcherChannels::new(&config); + let batcher_channel = BatcherChannels::new(&config, spec_id); + let eth_block_hash = eth_block.block_header.hash(); let state = State::new( eth_block.block_header.number, eth_block_hash, @@ -148,20 +155,26 @@ impl Batcher { Epoch { number: eth_block.block_header.number, hash: eth_block_hash, - timestamp: eth_block.block_header.timestamp.try_into().unwrap(), + timestamp: timestamp.try_into().unwrap(), base_fee_per_gas: eth_block.block_header.base_fee_per_gas, deposits: deposits::extract_transactions(&config, eth_block)?, }, ); Ok(Batcher { + config, + spec_id, + state, batches: BTreeMap::new(), batcher_channel, - state, - config, }) } + /// Returns a reference to the chain configuration. + pub fn config(&self) -> &ChainConfig { + &self.config + } + pub fn process_l1_block(&mut self, eth_block: &BlockInput) -> Result<()> { let eth_block_hash = eth_block.block_header.hash(); @@ -171,11 +184,17 @@ impl Batcher { "Eth block has invalid parent hash" ); - // Update the system config. From the spec: - // "Upon traversal of the L1 block, the system configuration copy used by the L1 retrieval - // stage is updated, such that the batch-sender authentication is always accurate to the - // exact L1 block that is read by the stage" + // Set the spec_id according to the L1 timestamp + self.spec_id = self + .config + .chain_spec + .active_fork(0, ð_block.block_header.timestamp)?; + if eth_block.receipts.is_some() { + // Update the system config. From the spec: + // "Upon traversal of the L1 block, the system configuration copy used by the L1 + // retrieval stage is updated, such that the batch-sender authentication is always + // accurate to the exact L1 block that is read by the stage" self.config .system_config .update(&self.config.system_config_contract, eth_block) diff --git a/lib/src/optimism/batcher_channel.rs b/lib/src/optimism/batcher_channel.rs index 3054ffdf..cf8c14df 100644 --- a/lib/src/optimism/batcher_channel.rs +++ b/lib/src/optimism/batcher_channel.rs @@ -20,6 +20,7 @@ use std::{ use anyhow::{bail, ensure, Context, Result}; use bytes::Buf; use libflate::zlib::Decoder; +use revm::primitives::SpecId; use zeth_primitives::{ alloy_rlp::Decodable, batch::Batch, @@ -33,6 +34,7 @@ use crate::utils::MultiReader; pub const MAX_RLP_BYTES_PER_CHANNEL: u64 = 10_000_000; pub struct BatcherChannels { + spec_id: SpecId, batch_inbox: Address, max_channel_bank_size: u64, channel_timeout: u64, @@ -41,8 +43,9 @@ pub struct BatcherChannels { } impl BatcherChannels { - pub fn new(config: &ChainConfig) -> Self { + pub fn new(config: &ChainConfig, spec_id: SpecId) -> Self { Self { + spec_id, batch_inbox: config.batch_inbox, max_channel_bank_size: config.max_channel_bank_size, channel_timeout: config.channel_timeout, @@ -113,13 +116,31 @@ impl BatcherChannels { log::debug!("timed-out channel: {}", _channel.id); } - // read all ready channels from the front of the queue - while matches!(self.channels.front(), Some(channel) if channel.is_ready()) { - let channel = self.channels.pop_front().unwrap(); - #[cfg(not(target_os = "zkvm"))] - log::trace!("received channel: {}", channel.id); + if self.spec_id >= SpecId::CANYON { + // From the spec: + // "After the Canyon network upgrade, the entire channel bank is scanned in FIFO + // order and the first ready (i.e. not timed-out) channel will be returned." + self.channels.retain(|channel| { + if channel.is_ready() { + #[cfg(not(target_os = "zkvm"))] + log::trace!("channel is ready: {}", channel.id); + self.batches.push_back(channel.read_batches(block_number)); + false + } else { + true + } + }); + } else { + // From the spec: + // "Prior to the Canyon network upgrade, once the first opened channel, if any, is + // not timed-out and is ready, then it is read and removed from the channel-bank." + while matches!(self.channels.front(), Some(channel) if channel.is_ready()) { + let channel = self.channels.pop_front().unwrap(); + #[cfg(not(target_os = "zkvm"))] + log::trace!("received channel: {}", channel.id); - self.batches.push_back(channel.read_batches(block_number)); + self.batches.push_back(channel.read_batches(block_number)); + } } } diff --git a/lib/src/optimism/batcher_db.rs b/lib/src/optimism/batcher_db.rs index 84985422..284927c1 100644 --- a/lib/src/optimism/batcher_db.rs +++ b/lib/src/optimism/batcher_db.rs @@ -26,7 +26,7 @@ use zeth_primitives::{ trie::MptNode, }; -use crate::optimism::{config::OPTIMISM_CHAIN_SPEC, deposits, system_config}; +use super::{config::ChainConfig, deposits, system_config}; /// Input for extracting deposits. #[derive(Debug, Clone, Deserialize, Serialize)] @@ -40,7 +40,7 @@ pub struct BlockInput { } pub trait BatcherDb { - fn validate(&self) -> Result<()>; + fn validate(&self, config: &ChainConfig) -> Result<()>; fn get_full_op_block(&mut self, block_no: u64) -> Result>; fn get_op_block_header(&mut self, block_no: u64) -> Result
; fn get_full_eth_block(&mut self, block_no: u64) -> Result<&BlockInput>; @@ -72,12 +72,10 @@ impl Default for MemDb { } impl BatcherDb for MemDb { - fn validate(&self) -> Result<()> { + fn validate(&self, config: &ChainConfig) -> Result<()> { for (block_no, op_block) in &self.full_op_block { - ensure!( - *block_no == op_block.block_header.number, - "Block number mismatch" - ); + let header = &op_block.block_header; + ensure!(*block_no == header.number, "Block number mismatch"); // Validate tx list { @@ -86,7 +84,7 @@ impl BatcherDb for MemDb { tx_trie.insert_rlp(&alloy_rlp::encode(tx_no), tx)?; } ensure!( - tx_trie.hash() == op_block.block_header.transactions_root, + tx_trie.hash() == header.transactions_root, "Invalid op block transaction data!" ); } @@ -103,10 +101,8 @@ impl BatcherDb for MemDb { } for (block_no, eth_block) in &self.full_eth_block { - ensure!( - *block_no == eth_block.block_header.number, - "Block number mismatch" - ); + let header = ð_block.block_header; + ensure!(*block_no == header.number, "Block number mismatch"); // Validate tx list { @@ -115,7 +111,7 @@ impl BatcherDb for MemDb { tx_trie.insert_rlp(&alloy_rlp::encode(tx_no), tx)?; } ensure!( - tx_trie.hash() == eth_block.block_header.transactions_root, + tx_trie.hash() == header.transactions_root, "Invalid eth block transaction data!" ); } @@ -127,18 +123,14 @@ impl BatcherDb for MemDb { receipt_trie.insert_rlp(&alloy_rlp::encode(tx_no), receipt)?; } ensure!( - receipt_trie.hash() == eth_block.block_header.receipts_root, + receipt_trie.hash() == header.receipts_root, "Invalid eth block receipt data!" ); } else { - let can_contain_deposits = deposits::can_contain( - &OPTIMISM_CHAIN_SPEC.deposit_contract, - ð_block.block_header.logs_bloom, - ); - let can_contain_config = system_config::can_contain( - &OPTIMISM_CHAIN_SPEC.system_config_contract, - ð_block.block_header.logs_bloom, - ); + let can_contain_deposits = + deposits::can_contain(&config.deposit_contract, &header.logs_bloom); + let can_contain_config = + system_config::can_contain(&config.system_config_contract, &header.logs_bloom); ensure!( !can_contain_deposits, "Eth block has no receipts, but bloom filter indicates it has deposits" diff --git a/lib/src/optimism/config.rs b/lib/src/optimism/config.rs index e1f8f57b..b442a4f7 100644 --- a/lib/src/optimism/config.rs +++ b/lib/src/optimism/config.rs @@ -1,4 +1,4 @@ -// Copyright 2023 RISC Zero, Inc. +// Copyright 2024 RISC Zero, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,16 +13,18 @@ // limitations under the License. use ruint::uint; -use serde::{Deserialize, Serialize}; use zeth_primitives::{address, Address}; use super::system_config::SystemConfig; +use crate::consts::{ChainSpec, OP_MAINNET_CHAIN_SPEC}; -/// A Chain Configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +/// A Chain derivation configuration +#[derive(Debug)] pub struct ChainConfig { /// The initial system config value pub system_config: SystemConfig, + // The chain specification + pub chain_spec: &'static ChainSpec, /// The L1 attributes depositor address pub l1_attributes_depositor: Address, /// The L1 attributes contract @@ -48,7 +50,8 @@ pub struct ChainConfig { } impl ChainConfig { - pub const fn optimism() -> Self { + /// Creates the OP mainnet chain configuration. + pub fn optimism() -> Self { Self { system_config: SystemConfig { batch_sender: address!("6887246668a3b87f54deb3b94ba47a6f63f32985"), @@ -57,6 +60,7 @@ impl ChainConfig { l1_fee_scalar: uint!(684000_U256), unsafe_block_signer: address!("AAAA45d9549EDA09E70937013520214382Ffc4A2"), }, + chain_spec: &OP_MAINNET_CHAIN_SPEC, l1_attributes_depositor: address!("deaddeaddeaddeaddeaddeaddeaddeaddead0001"), l1_attributes_contract: address!("4200000000000000000000000000000000000015"), sequencer_fee_vault: address!("4200000000000000000000000000000000000011"), @@ -71,5 +75,3 @@ impl ChainConfig { } } } - -pub const OPTIMISM_CHAIN_SPEC: ChainConfig = ChainConfig::optimism(); diff --git a/lib/src/optimism/mod.rs b/lib/src/optimism/mod.rs index db24ac53..9ea5f86a 100644 --- a/lib/src/optimism/mod.rs +++ b/lib/src/optimism/mod.rs @@ -125,11 +125,12 @@ pub struct DeriveMachine { impl DeriveMachine { /// Creates a new instance of DeriveMachine. pub fn new( - chain_config: &ChainConfig, + mut chain_config: ChainConfig, mut derive_input: DeriveInput, provider_factory: Option, ) -> Result { - derive_input.db.validate()?; + derive_input.db.validate(&chain_config)?; + #[cfg(not(target_os = "zkvm"))] ensure!(provider_factory.is_some(), "Missing provider factory!"); @@ -152,7 +153,7 @@ impl DeriveMachine { .first() .context("block is empty")? .essence; - if let Err(err) = validate_l1_attributes_deposited_tx(chain_config, l1_attributes_tx) { + if let Err(err) = validate_l1_attributes_deposited_tx(&chain_config, l1_attributes_tx) { bail!( "First transaction in block is not a valid L1 attributes deposited transaction: {}", err @@ -184,15 +185,13 @@ impl DeriveMachine { ); let op_batcher = { - // copy the chain config and update the system config - let mut op_chain_config = chain_config.clone(); - op_chain_config.system_config.batch_sender = + chain_config.system_config.batch_sender = Address::from_slice(&set_l1_block_values.batcher_hash.as_slice()[12..]); - op_chain_config.system_config.l1_fee_overhead = set_l1_block_values.l1_fee_overhead; - op_chain_config.system_config.l1_fee_scalar = set_l1_block_values.l1_fee_scalar; + chain_config.system_config.l1_fee_overhead = set_l1_block_values.l1_fee_overhead; + chain_config.system_config.l1_fee_scalar = set_l1_block_values.l1_fee_scalar; Batcher::new( - op_chain_config, + chain_config, L2BlockInfo { hash: op_head_block_hash, timestamp: op_head.block_header.timestamp.try_into().unwrap(), @@ -340,8 +339,8 @@ impl DeriveMachine { let new_op_head_input = BlockBuildInput { state_input: StateInput { parent_header: self.op_head_block_header.clone(), - beneficiary: self.op_batcher.config.sequencer_fee_vault, - gas_limit: self.op_batcher.config.system_config.gas_limit, + beneficiary: self.op_batcher.config().sequencer_fee_vault, + gas_limit: self.op_batcher.config().system_config.gas_limit, timestamp: U256::from(op_batch.0.timestamp), extra_data: Default::default(), mix_hash: l1_epoch_header_mix_hash, @@ -475,7 +474,7 @@ impl DeriveMachine { ) -> Transaction { let batcher_hash = { let all_zero: FixedBytes<12> = FixedBytes::ZERO; - all_zero.concat_const::<20, 32>(self.op_batcher.config.system_config.batch_sender.0) + all_zero.concat_const::<20, 32>(self.op_batcher.config().system_config.batch_sender.0) }; let set_l1_block_values = @@ -486,8 +485,8 @@ impl DeriveMachine { hash: self.op_batcher.state.epoch.hash, sequence_number: self.op_block_seq_no, batcher_hash, - l1_fee_overhead: self.op_batcher.config.system_config.l1_fee_overhead, - l1_fee_scalar: self.op_batcher.config.system_config.l1_fee_scalar, + l1_fee_overhead: self.op_batcher.config().system_config.l1_fee_overhead, + l1_fee_scalar: self.op_batcher.config().system_config.l1_fee_scalar, }); let source_hash: B256 = { @@ -496,8 +495,8 @@ impl DeriveMachine { let source_hash_sequencing = keccak([l1_block_hash, seq_number].concat()); keccak([ONE.to_be_bytes::<32>(), source_hash_sequencing].concat()).into() }; - let config = &self.op_batcher.config; + let config = self.op_batcher.config(); Transaction { essence: OptimismTxEssence::OptimismDeposited(TxEssenceOptimismDeposited { source_hash, diff --git a/primitives/src/ethers.rs b/primitives/src/ethers.rs index 40f01472..8b44cf9f 100644 --- a/primitives/src/ethers.rs +++ b/primitives/src/ethers.rs @@ -1,4 +1,4 @@ -// Copyright 2023 RISC Zero, Inc. +// Copyright 2024 RISC Zero, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ use ethers_core::types::{ use crate::{ access_list::{AccessList, AccessListItem}, block::Header, - receipt::{Log, Receipt, ReceiptPayload}, + receipt::{Log, Receipt, ReceiptPayload, OPTIMISM_DEPOSIT_NONCE_VERSION}, transactions::{ ethereum::{ EthereumTxEssence, TransactionKind, TxEssenceEip1559, TxEssenceEip2930, TxEssenceLegacy, @@ -294,6 +294,10 @@ impl TryFrom for Receipt { } }) .collect(), + deposit_nonce: receipt.deposit_nonce, + deposit_nonce_version: receipt + .deposit_nonce + .map(|_| OPTIMISM_DEPOSIT_NONCE_VERSION), }, }) } diff --git a/primitives/src/receipt.rs b/primitives/src/receipt.rs index 5b641d25..290dcb99 100644 --- a/primitives/src/receipt.rs +++ b/primitives/src/receipt.rs @@ -1,4 +1,4 @@ -// Copyright 2023 RISC Zero, Inc. +// Copyright 2024 RISC Zero, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloy_primitives::{Address, Bloom, BloomInput, Bytes, B256, U256}; +use alloy_primitives::{Address, Bloom, BloomInput, Bytes, TxNumber, B256, U256}; use alloy_rlp::Encodable; use alloy_rlp_derive::RlpEncodable; use serde::{Deserialize, Serialize}; +/// Version of the deposit nonce field in the receipt. +pub const OPTIMISM_DEPOSIT_NONCE_VERSION: u32 = 1; + /// Represents an Ethereum log entry. #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, RlpEncodable)] pub struct Log { @@ -30,6 +33,7 @@ pub struct Log { /// Payload of a [Receipt]. #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, RlpEncodable)] +#[rlp(trailing)] pub struct ReceiptPayload { /// Indicates whether the transaction was executed successfully. pub success: bool, @@ -39,6 +43,12 @@ pub struct ReceiptPayload { pub logs_bloom: Bloom, /// Logs generated during the execution of the transaction. pub logs: Vec, + /// Nonce of the Optimism deposit transaction persisted during execution. + #[serde(default)] + pub deposit_nonce: Option, + /// Version of the deposit nonce field in the receipt. + #[serde(default)] + pub deposit_nonce_version: Option, } /// Receipt containing result of transaction execution. @@ -80,8 +90,6 @@ impl Encodable for Receipt { impl Receipt { /// Constructs a new [Receipt]. - /// - /// This function also computes the `logs_bloom` based on the provided logs. pub fn new(tx_type: u8, success: bool, cumulative_gas_used: U256, logs: Vec) -> Receipt { let mut logs_bloom = Bloom::default(); for log in &logs { @@ -98,9 +106,17 @@ impl Receipt { cumulative_gas_used, logs_bloom, logs, + deposit_nonce: None, + deposit_nonce_version: None, }, } } + /// Adds a deposit nonce to the receipt. + pub fn with_deposit_nonce(mut self, deposit_nonce: TxNumber) -> Self { + self.payload.deposit_nonce = Some(deposit_nonce); + self.payload.deposit_nonce_version = Some(OPTIMISM_DEPOSIT_NONCE_VERSION); + self + } } // test vectors from https://github.com/ethereum/go-ethereum/blob/c40ab6af72ce282020d03c33e8273ea9b03d58f6/core/types/receipt_test.go diff --git a/testing/ef-tests/src/ethtests.rs b/testing/ef-tests/src/ethtests.rs index 59a87e25..bcb84a3b 100644 --- a/testing/ef-tests/src/ethtests.rs +++ b/testing/ef-tests/src/ethtests.rs @@ -16,7 +16,7 @@ use std::{fs::File, io::BufReader, path::PathBuf}; use revm::primitives::SpecId; use serde_json::Value; -use zeth_lib::consts::ChainSpec; +use zeth_lib::consts::{ChainSpec, ETH_MAINNET_EIP1559_CONSTANTS}; use zeth_primitives::block::Header; use crate::TestJson; @@ -45,7 +45,7 @@ pub fn read_eth_test(path: PathBuf) -> Vec { println!("skipping ({})", json.network); return None; } - let chain_spec = ChainSpec::new_single(1, spec, Default::default()); + let chain_spec = ChainSpec::new_single(1, spec, ETH_MAINNET_EIP1559_CONSTANTS); let genesis: Header = json.genesis.clone().into(); assert_eq!(genesis.hash(), json.genesis.hash);