diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c18393338..be5e3a5d98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,9 @@ jobs: cache-on-failure: true - name: cargo test + run: cargo test --workspace + + - name: cargo test all features run: cargo test --workspace --all-features - name: cargo check no_std diff --git a/crates/interpreter/Cargo.toml b/crates/interpreter/Cargo.toml index 7140f51b7b..8f35cb97fe 100644 --- a/crates/interpreter/Cargo.toml +++ b/crates/interpreter/Cargo.toml @@ -42,6 +42,8 @@ arbitrary = [ "revm-primitives/arbitrary", ] +optimism = ["revm-primitives/optimism"] + dev = [ "memory_limit", "optional_balance_check", diff --git a/crates/interpreter/src/instructions/opcode.rs b/crates/interpreter/src/instructions/opcode.rs index 3e98306127..61ef794b51 100644 --- a/crates/interpreter/src/instructions/opcode.rs +++ b/crates/interpreter/src/instructions/opcode.rs @@ -834,12 +834,24 @@ const fn make_gas_table(spec: SpecId) -> [OpInfo; 256] { pub const fn spec_opcode_gas(spec_id: SpecId) -> &'static [OpInfo; 256] { macro_rules! gas_maps { ($($id:ident),* $(,)?) => { - match spec_id {$( + match spec_id { + $( SpecId::$id => { const TABLE: &[OpInfo; 256] = &make_gas_table(SpecId::$id); TABLE } - )*} + )* + #[cfg(feature = "optimism")] + SpecId::BEDROCK => { + const TABLE: &[OpInfo;256] = &make_gas_table(SpecId::BEDROCK); + TABLE + } + #[cfg(feature = "optimism")] + SpecId::REGOLITH => { + const TABLE: &[OpInfo;256] = &make_gas_table(SpecId::REGOLITH); + TABLE + } + } }; } diff --git a/crates/precompile/Cargo.toml b/crates/precompile/Cargo.toml index 26a6e9d9f9..b5d2471b26 100644 --- a/crates/precompile/Cargo.toml +++ b/crates/precompile/Cargo.toml @@ -54,3 +54,4 @@ c-kzg = ["dep:c-kzg", "revm-primitives/c-kzg"] # The problem that `secp256k1` has is it fails to build for `wasm` target on Windows and Mac as it is c lib. # In Linux it passes. If you don't require to build wasm on win/mac, it is safe to use it and it is enabled by default. secp256k1 = ["dep:secp256k1"] +optimism = ["revm-primitives/optimism"] diff --git a/crates/precompile/src/lib.rs b/crates/precompile/src/lib.rs index c75f6bcc0a..feecb34bfa 100644 --- a/crates/precompile/src/lib.rs +++ b/crates/precompile/src/lib.rs @@ -110,6 +110,8 @@ impl SpecId { BERLIN | LONDON | ARROW_GLACIER | GRAY_GLACIER | MERGE | SHANGHAI => Self::BERLIN, CANCUN => Self::CANCUN, LATEST => Self::LATEST, + #[cfg(feature = "optimism")] + BEDROCK | REGOLITH => Self::BERLIN, } } diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 5aee6972f9..a4131ff4e2 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -83,6 +83,8 @@ arbitrary = [ "bitflags/arbitrary", ] +optimism = [] + dev = [ "memory_limit", "optional_balance_check", diff --git a/crates/primitives/src/env.rs b/crates/primitives/src/env.rs index 0f7bbe83db..8d883c7e54 100644 --- a/crates/primitives/src/env.rs +++ b/crates/primitives/src/env.rs @@ -77,6 +77,23 @@ impl BlobExcessGasAndPrice { } } +#[cfg(feature = "optimism")] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct OptimismFields { + pub source_hash: Option, + pub mint: Option, + pub is_system_transaction: Option, + /// An enveloped EIP-2718 typed transaction. This is used + /// to compute the L1 tx cost using the L1 block info, as + /// opposed to requiring downstream apps to compute the cost + /// externally. + /// This field is optional to allow the [TxEnv] to be constructed + /// for non-optimism chains when the `optimism` feature is enabled, + /// but the [CfgEnv] `optimism` field is set to false. + pub enveloped_tx: Option, +} + impl BlockEnv { /// Takes `blob_excess_gas` saves it inside env /// and calculates `blob_fee` with [`BlobGasAndFee`]. @@ -162,6 +179,10 @@ pub struct TxEnv { /// /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 pub max_fee_per_blob_gas: Option, + + #[cfg_attr(feature = "serde", serde(flatten))] + #[cfg(feature = "optimism")] + pub optimism: OptimismFields, } impl TxEnv { @@ -248,9 +269,6 @@ pub struct CfgEnv { /// If some it will effects EIP-170: Contract code size limit. Useful to increase this because of tests. /// By default it is 0x6000 (~25kb). pub limit_contract_code_size: Option, - /// Disables the coinbase tip during the finalization of the transaction. This is useful for - /// rollups that redirect the tip to the sequencer. - pub disable_coinbase_tip: bool, /// A hard memory limit in bytes beyond which [Memory] cannot be resized. /// /// In cases where the gas limit may be extraordinarily high, it is recommended to set this to @@ -280,6 +298,14 @@ pub struct CfgEnv { /// This is useful for testing method calls with zero gas price. #[cfg(feature = "optional_no_base_fee")] pub disable_base_fee: bool, + /// Enables Optimism's execution changes for deposit transactions and fee + /// collection. Hot toggling the optimism field gives applications built + /// on revm the ability to switch optimism execution on and off at runtime, + /// allowing for features like multichain fork testing. Setting this field + /// to false will disable all optimism execution changes regardless of + /// compilation with the optimism feature flag. + #[cfg(feature = "optimism")] + pub optimism: bool, } impl CfgEnv { @@ -332,6 +358,16 @@ impl CfgEnv { pub fn is_block_gas_limit_disabled(&self) -> bool { false } + + #[cfg(feature = "optimism")] + pub fn is_optimism(&self) -> bool { + self.optimism + } + + #[cfg(not(feature = "optimism"))] + pub fn is_optimism(&self) -> bool { + false + } } /// What bytecode analysis to perform. @@ -354,7 +390,6 @@ impl Default for CfgEnv { spec_id: SpecId::LATEST, perf_analyse_created_bytecodes: AnalysisKind::default(), limit_contract_code_size: None, - disable_coinbase_tip: false, #[cfg(feature = "c-kzg")] kzg_settings: crate::kzg::EnvKzgSettings::Default, #[cfg(feature = "memory_limit")] @@ -369,6 +404,8 @@ impl Default for CfgEnv { disable_gas_refund: false, #[cfg(feature = "optional_no_base_fee")] disable_base_fee: false, + #[cfg(feature = "optimism")] + optimism: false, } } } @@ -403,6 +440,8 @@ impl Default for TxEnv { access_list: Vec::new(), blob_hashes: Vec::new(), max_fee_per_blob_gas: None, + #[cfg(feature = "optimism")] + optimism: OptimismFields::default(), } } } @@ -449,6 +488,21 @@ impl Env { /// Return initial spend gas (Gas needed to execute transaction). #[inline] pub fn validate_tx(&self) -> Result<(), InvalidTransaction> { + #[cfg(feature = "optimism")] + if self.cfg.optimism { + // Do not allow for a system transaction to be processed if Regolith is enabled. + if self.tx.optimism.is_system_transaction.unwrap_or(false) + && SPEC::enabled(SpecId::REGOLITH) + { + return Err(InvalidTransaction::DepositSystemTxPostRegolith); + } + + // Do not perform any extra validation for deposit transactions, they are pre-verified on L1. + if self.tx.optimism.source_hash.is_some() { + return Ok(()); + } + } + let gas_limit = self.tx.gas_limit; let effective_gas_price = self.effective_gas_price(); let is_create = self.tx.transact_to.is_create(); @@ -561,6 +615,13 @@ impl Env { return Err(InvalidTransaction::RejectCallerWithCode); } + // On Optimism, deposit transactions do not have verification on the nonce + // nor the balance of the account. + #[cfg(feature = "optimism")] + if self.cfg.optimism && self.tx.optimism.source_hash.is_some() { + return Ok(()); + } + // Check that the transaction's nonce is correct if let Some(tx) = self.tx.nonce { let state = account.info.nonce; @@ -604,3 +665,70 @@ impl Env { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "optimism")] + #[test] + fn test_validate_sys_tx() { + // Set the optimism flag to true and mark + // the tx as a system transaction. + let mut env = Env::default(); + env.cfg.optimism = true; + env.tx.optimism.is_system_transaction = Some(true); + assert_eq!( + env.validate_tx::(), + Err(InvalidTransaction::DepositSystemTxPostRegolith) + ); + + // Pre-regolith system transactions should be allowed. + assert!(env.validate_tx::().is_ok()); + } + + #[cfg(feature = "optimism")] + #[test] + fn test_validate_deposit_tx() { + // Set the optimism flag and source hash. + let mut env = Env::default(); + env.cfg.optimism = true; + env.tx.optimism.source_hash = Some(B256::zero()); + assert!(env.validate_tx::().is_ok()); + } + + #[cfg(feature = "optimism")] + #[test] + fn test_validate_tx_against_state_deposit_tx() { + // Set the optimism flag and source hash. + let mut env = Env::default(); + env.cfg.optimism = true; + env.tx.optimism.source_hash = Some(B256::zero()); + + // Nonce and balance checks should be skipped for deposit transactions. + assert!(env + .validate_tx_against_state(&mut Account::default()) + .is_ok()); + } + + #[test] + fn test_validate_tx_chain_id() { + let mut env = Env::default(); + env.tx.chain_id = Some(1); + env.cfg.chain_id = 2; + assert_eq!( + env.validate_tx::(), + Err(InvalidTransaction::InvalidChainId) + ); + } + + #[test] + fn test_validate_tx_access_list() { + let mut env = Env::default(); + env.tx.access_list = vec![(B160::zero(), vec![])]; + assert_eq!( + env.validate_tx::(), + Err(InvalidTransaction::AccessListNotSupported) + ); + } +} diff --git a/crates/primitives/src/result.rs b/crates/primitives/src/result.rs index d7ed3f2133..934a66d78a 100644 --- a/crates/primitives/src/result.rs +++ b/crates/primitives/src/result.rs @@ -4,7 +4,11 @@ use bytes::Bytes; use core::fmt; use ruint::aliases::U256; -pub type EVMResult = core::result::Result>; +/// Result of EVM execution. +pub type EVMResult = EVMResultGeneric; + +/// Generic result of EVM execution. Used to represent error and generic output. +pub type EVMResultGeneric = core::result::Result>; #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -206,6 +210,10 @@ pub enum InvalidTransaction { TooManyBlobs, /// Blob transaction contains a versioned hash with an incorrect version BlobVersionNotSupported, + /// System transactions are not supported + /// post-regolith hardfork. + #[cfg(feature = "optimism")] + DepositSystemTxPostRegolith, } impl From for EVMError { diff --git a/crates/primitives/src/specification.rs b/crates/primitives/src/specification.rs index 60d753981d..741271535b 100644 --- a/crates/primitives/src/specification.rs +++ b/crates/primitives/src/specification.rs @@ -27,6 +27,10 @@ pub enum SpecId { MERGE = 15, // Paris/Merge 15537394 (TTD: 58750000000000000000000) SHANGHAI = 16, // Shanghai 17034870 (TS: 1681338455) CANCUN = 17, // Cancun TBD + #[cfg(feature = "optimism")] + BEDROCK = 128, + #[cfg(feature = "optimism")] + REGOLITH = 129, LATEST = u8::MAX, } @@ -38,6 +42,23 @@ impl SpecId { #[inline(always)] pub const fn enabled(our: SpecId, other: SpecId) -> bool { + #[cfg(feature = "optimism")] + { + let (our, other) = (our as u8, other as u8); + let (merge, bedrock, regolith) = + (Self::MERGE as u8, Self::BEDROCK as u8, Self::REGOLITH as u8); + // If the Spec is Bedrock or Regolith, and the input is not Bedrock or Regolith, + // then no hardforks should be enabled after the merge. This is because Optimism's + // Bedrock and Regolith hardforks implement changes on top of the Merge hardfork. + let is_self_optimism = our == bedrock || our == regolith; + let input_not_optimism = other != bedrock && other != regolith; + let after_merge = other > merge; + + if is_self_optimism && input_not_optimism && after_merge { + return false; + } + } + our as u8 >= other as u8 } } @@ -59,6 +80,10 @@ impl From<&str> for SpecId { "Merge" => Self::MERGE, "Shanghai" => Self::SHANGHAI, "Cancun" => Self::CANCUN, + #[cfg(feature = "optimism")] + "Bedrock" => SpecId::BEDROCK, + #[cfg(feature = "optimism")] + "Regolith" => SpecId::REGOLITH, _ => Self::LATEST, } } @@ -71,6 +96,21 @@ pub trait Spec: Sized { /// Returns `true` if the given specification ID is enabled in this spec. #[inline(always)] fn enabled(spec_id: SpecId) -> bool { + #[cfg(feature = "optimism")] + { + // If the Spec is Bedrock or Regolith, and the input is not Bedrock or Regolith, + // then no hardforks should be enabled after the merge. This is because Optimism's + // Bedrock and Regolith hardforks implement changes on top of the Merge hardfork. + let is_self_optimism = + Self::SPEC_ID == SpecId::BEDROCK || Self::SPEC_ID == SpecId::REGOLITH; + let input_not_optimism = spec_id != SpecId::BEDROCK && spec_id != SpecId::REGOLITH; + let after_merge = spec_id > SpecId::MERGE; + + if is_self_optimism && input_not_optimism && after_merge { + return false; + } + } + Self::SPEC_ID as u8 >= spec_id as u8 } } @@ -105,3 +145,55 @@ spec!(SHANGHAI, ShanghaiSpec); spec!(CANCUN, CancunSpec); spec!(LATEST, LatestSpec); + +// Optimism Hardforks +#[cfg(feature = "optimism")] +spec!(BEDROCK, BedrockSpec); +#[cfg(feature = "optimism")] +spec!(REGOLITH, RegolithSpec); + +#[cfg(feature = "optimism")] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bedrock_post_merge_hardforks() { + assert!(BedrockSpec::enabled(SpecId::MERGE)); + assert!(!BedrockSpec::enabled(SpecId::SHANGHAI)); + assert!(!BedrockSpec::enabled(SpecId::CANCUN)); + assert!(!BedrockSpec::enabled(SpecId::LATEST)); + assert!(BedrockSpec::enabled(SpecId::BEDROCK)); + assert!(!BedrockSpec::enabled(SpecId::REGOLITH)); + } + + #[test] + fn test_regolith_post_merge_hardforks() { + assert!(RegolithSpec::enabled(SpecId::MERGE)); + assert!(!RegolithSpec::enabled(SpecId::SHANGHAI)); + assert!(!RegolithSpec::enabled(SpecId::CANCUN)); + assert!(!RegolithSpec::enabled(SpecId::LATEST)); + assert!(RegolithSpec::enabled(SpecId::BEDROCK)); + assert!(RegolithSpec::enabled(SpecId::REGOLITH)); + } + + #[test] + fn test_bedrock_post_merge_hardforks_spec_id() { + assert!(SpecId::enabled(SpecId::BEDROCK, SpecId::MERGE)); + assert!(!SpecId::enabled(SpecId::BEDROCK, SpecId::SHANGHAI)); + assert!(!SpecId::enabled(SpecId::BEDROCK, SpecId::CANCUN)); + assert!(!SpecId::enabled(SpecId::BEDROCK, SpecId::LATEST)); + assert!(SpecId::enabled(SpecId::BEDROCK, SpecId::BEDROCK)); + assert!(!SpecId::enabled(SpecId::BEDROCK, SpecId::REGOLITH)); + } + + #[test] + fn test_regolith_post_merge_hardforks_spec_id() { + assert!(SpecId::enabled(SpecId::REGOLITH, SpecId::MERGE)); + assert!(!SpecId::enabled(SpecId::REGOLITH, SpecId::SHANGHAI)); + assert!(!SpecId::enabled(SpecId::REGOLITH, SpecId::CANCUN)); + assert!(!SpecId::enabled(SpecId::REGOLITH, SpecId::LATEST)); + assert!(SpecId::enabled(SpecId::REGOLITH, SpecId::BEDROCK)); + assert!(SpecId::enabled(SpecId::REGOLITH, SpecId::REGOLITH)); + } +} diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 96fbb56aee..61ffa0c501 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -68,6 +68,7 @@ c-kzg = ["revm-precompile/c-kzg"] # deprecated features web3db = [] with-serde = [] +optimism = ["revm-interpreter/optimism", "revm-precompile/optimism"] [[example]] name = "fork_ref_transact" diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index c047a8a342..5c98d142bd 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -231,5 +231,9 @@ pub fn evm_inner<'a, DB: Database, const INSPECT: bool>( SpecId::SHANGHAI => create_evm!(ShanghaiSpec), SpecId::CANCUN => create_evm!(CancunSpec), SpecId::LATEST => create_evm!(LatestSpec), + #[cfg(feature = "optimism")] + SpecId::BEDROCK => create_evm!(BedrockSpec), + #[cfg(feature = "optimism")] + SpecId::REGOLITH => create_evm!(RegolithSpec), } } diff --git a/crates/revm/src/evm_impl.rs b/crates/revm/src/evm_impl.rs index 86b5f88ddd..ba7291603b 100644 --- a/crates/revm/src/evm_impl.rs +++ b/crates/revm/src/evm_impl.rs @@ -1,33 +1,41 @@ +use crate::handler::Handler; use crate::interpreter::{ - analysis::to_analysed, gas, instruction_result::SuccessOrHalt, return_ok, return_revert, - CallContext, CallInputs, CallScheme, Contract, CreateInputs, CreateScheme, Gas, Host, - InstructionResult, Interpreter, SelfDestructResult, Transfer, CALL_STACK_LIMIT, + analysis::to_analysed, gas, instruction_result::SuccessOrHalt, return_ok, CallContext, + CallInputs, CallScheme, Contract, CreateInputs, CreateScheme, Gas, Host, InstructionResult, + Interpreter, SelfDestructResult, Transfer, CALL_STACK_LIMIT, }; use crate::journaled_state::{is_precompile, JournalCheckpoint}; use crate::primitives::{ - create2_address, create_address, keccak256, Account, AnalysisKind, Bytecode, Bytes, EVMError, - EVMResult, Env, ExecutionResult, HashMap, InvalidTransaction, Log, Output, ResultAndState, - Spec, SpecId::*, TransactTo, B160, B256, U256, + create2_address, create_address, keccak256, AnalysisKind, Bytecode, Bytes, EVMError, EVMResult, + Env, ExecutionResult, InvalidTransaction, Log, Output, ResultAndState, Spec, SpecId::*, + TransactTo, B160, B256, U256, }; use crate::{db::Database, journaled_state::JournaledState, precompile, Inspector}; use alloc::boxed::Box; use alloc::vec::Vec; -use core::{cmp::min, marker::PhantomData}; +use core::marker::PhantomData; use revm_interpreter::gas::initial_tx_gas; use revm_interpreter::MAX_CODE_SIZE; use revm_precompile::{Precompile, Precompiles}; +#[cfg(feature = "optimism")] +use crate::optimism; + pub struct EVMData<'a, DB: Database> { pub env: &'a mut Env, pub journaled_state: JournaledState, pub db: &'a mut DB, pub error: Option, pub precompiles: Precompiles, + /// Used as temporary value holder to store L1 block info. + #[cfg(feature = "optimism")] + pub l1_block_info: Option, } pub struct EVMImpl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> { data: EVMData<'a, DB>, inspector: &'a mut dyn Inspector, + handler: Handler, _phantomdata: PhantomData, } @@ -72,22 +80,78 @@ pub trait Transact { } } -impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, INSPECT> { +impl<'a, DB: Database> EVMData<'a, DB> { /// Load access list for berlin hardfork. /// /// Loading of accounts/storages is needed to make them warm. #[inline] fn load_access_list(&mut self) -> Result<(), EVMError> { - for (address, slots) in self.data.env.tx.access_list.iter() { - self.data - .journaled_state - .initial_account_load(*address, slots, self.data.db) + for (address, slots) in self.env.tx.access_list.iter() { + self.journaled_state + .initial_account_load(*address, slots, self.db) .map_err(EVMError::Database)?; } Ok(()) } } +#[cfg(feature = "optimism")] +impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, INSPECT> { + /// If the transaction is not a deposit transaction, subtract the L1 data fee from the + /// caller's balance directly after minting the requested amount of ETH. + fn remove_l1_cost( + is_deposit: bool, + tx_caller: B160, + l1_cost: U256, + db: &mut DB, + journal: &mut JournaledState, + ) -> Result<(), EVMError> { + if is_deposit { + return Ok(()); + } + let acc = journal + .load_account(tx_caller, db) + .map_err(EVMError::Database)? + .0; + if l1_cost.gt(&acc.info.balance) { + let u64_cost = if U256::from(u64::MAX).lt(&l1_cost) { + u64::MAX + } else { + l1_cost.as_limbs()[0] + }; + return Err(EVMError::Transaction( + InvalidTransaction::LackOfFundForMaxFee { + fee: u64_cost, + balance: acc.info.balance, + }, + )); + } + acc.info.balance = acc.info.balance.saturating_sub(l1_cost); + Ok(()) + } + + /// If the transaction is a deposit with a `mint` value, add the mint value + /// in wei to the caller's balance. This should be persisted to the database + /// prior to the rest of execution. + fn commit_mint_value( + tx_caller: B160, + tx_mint: Option, + db: &mut DB, + journal: &mut JournaledState, + ) -> Result<(), EVMError> { + if let Some(mint) = tx_mint { + journal + .load_account(tx_caller, db) + .map_err(EVMError::Database)? + .0 + .info + .balance += U256::from(mint); + journal.checkpoint(); + } + Ok(()) + } +} + impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> Transact for EVMImpl<'a, GSPEC, DB, INSPECT> { @@ -130,6 +194,36 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> Transact let tx_data = env.tx.data.clone(); let tx_gas_limit = env.tx.gas_limit; + #[cfg(feature = "optimism")] + let tx_l1_cost = { + let is_deposit = env.tx.optimism.source_hash.is_some(); + + let l1_block_info = + optimism::L1BlockInfo::try_fetch(self.data.db, self.data.env.cfg.optimism) + .map_err(EVMError::Database)?; + + // Perform this calculation optimistically to avoid cloning the enveloped tx. + let tx_l1_cost = l1_block_info.as_ref().map(|l1_block_info| { + env.tx + .optimism + .enveloped_tx + .as_ref() + .map(|enveloped_tx| { + l1_block_info.calculate_tx_l1_cost::(enveloped_tx, is_deposit) + }) + .unwrap_or(U256::ZERO) + }); + // storage l1 block info for later use. + self.data.l1_block_info = l1_block_info; + + // + let Some(tx_l1_cost) = tx_l1_cost else { + panic!("[OPTIMISM] L1 Block Info could not be loaded from the DB.") + }; + + tx_l1_cost + }; + let initial_gas_spend = initial_tx_gas::( &tx_data, env.tx.transact_to.is_create(), @@ -145,23 +239,42 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> Transact .map_err(EVMError::Database)?; } - self.load_access_list()?; - // Without this line, the borrow checker complains that `self` is borrowed mutable above. - let env = &self.data.env; + self.data.load_access_list()?; // load acc let journal = &mut self.data.journaled_state; + + #[cfg(feature = "optimism")] + if self.data.env.cfg.optimism { + EVMImpl::::commit_mint_value( + tx_caller, + self.data.env.tx.optimism.mint, + self.data.db, + journal, + )?; + + let is_deposit = self.data.env.tx.optimism.source_hash.is_some(); + EVMImpl::::remove_l1_cost( + is_deposit, + tx_caller, + tx_l1_cost, + self.data.db, + journal, + )?; + } + let (caller_account, _) = journal .load_account(tx_caller, self.data.db) .map_err(EVMError::Database)?; // Subtract gas costs from the caller's account. // We need to saturate the gas cost to prevent underflow in case that `disable_balance_check` is enabled. - let mut gas_cost = U256::from(tx_gas_limit).saturating_mul(env.effective_gas_price()); + let mut gas_cost = + U256::from(tx_gas_limit).saturating_mul(self.data.env.effective_gas_price()); // EIP-4844 if GSPEC::enabled(CANCUN) { - let data_fee = env.calc_data_fee().expect("already checked"); + let data_fee = self.data.env.calc_data_fee().expect("already checked"); gas_cost = gas_cost.saturating_add(U256::from(data_fee)); } @@ -173,7 +286,7 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> Transact let transact_gas_limit = tx_gas_limit - initial_gas_spend; // call inner handling of call/create - let (exit_reason, ret_gas, output) = match self.data.env.tx.transact_to { + let (call_result, ret_gas, output) = match self.data.env.tx.transact_to { TransactTo::Call(address) => { // Nonce is already checked caller_account.info.nonce = caller_account.info.nonce.saturating_add(1); @@ -210,47 +323,70 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> Transact } }; - // set gas with gas limit and spend it all. Gas is going to be reimbursed when - // transaction is returned successfully. - let mut gas = Gas::new(tx_gas_limit); - gas.record_cost(tx_gas_limit); + let handler = &self.handler; + let data = &mut self.data; - if crate::USE_GAS { - match exit_reason { - return_ok!() => { - gas.erase_cost(ret_gas.remaining()); - gas.record_refund(ret_gas.refunded()); - } - return_revert!() => { - gas.erase_cost(ret_gas.remaining()); - } - _ => {} - } - } + // handle output of call/create calls. + let gas = handler.call_return(data.env, call_result, ret_gas); + + let gas_refunded = handler.calculate_gas_refund(data.env, &gas); - let (state, logs, gas_used, gas_refunded) = self.finalize::(&gas); + // Reimburse the caller + handler.reimburse_caller(data, &gas, gas_refunded)?; - let result = match exit_reason.into() { + // Reward beneficiary + handler.reward_beneficiary(data, &gas, gas_refunded)?; + + // used gas with refund calculated. + let final_gas_used = gas.spend() - gas_refunded; + + // reset journal and return present state. + let (state, logs) = self.data.journaled_state.finalize(); + + let result = match call_result.into() { SuccessOrHalt::Success(reason) => ExecutionResult::Success { reason, - gas_used, + gas_used: final_gas_used, gas_refunded, logs, output, }, SuccessOrHalt::Revert => ExecutionResult::Revert { - gas_used, + gas_used: final_gas_used, output: match output { Output::Call(return_value) => return_value, Output::Create(return_value, _) => return_value, }, }, - SuccessOrHalt::Halt(reason) => ExecutionResult::Halt { reason, gas_used }, + SuccessOrHalt::Halt(reason) => { + // Post-regolith, if the transaction is a deposit transaction and the + // output is a contract creation, increment the account nonce even if + // the transaction halts. + #[cfg(feature = "optimism")] + { + let is_deposit = self.data.env.tx.optimism.source_hash.is_some(); + let is_creation = matches!(output, Output::Create(_, _)); + let regolith_enabled = GSPEC::enabled(REGOLITH); + let optimism_regolith = self.data.env.cfg.optimism && regolith_enabled; + if is_deposit && is_creation && optimism_regolith { + let (acc, _) = self + .data + .journaled_state + .load_account(tx_caller, self.data.db) + .map_err(EVMError::Database)?; + acc.info.nonce = acc.info.nonce.checked_add(1).unwrap_or(u64::MAX); + } + } + ExecutionResult::Halt { + reason, + gas_used: final_gas_used, + } + } SuccessOrHalt::FatalExternalError => { return Err(EVMError::Database(self.data.error.take().unwrap())); } SuccessOrHalt::InternalContinue => { - panic!("Internal return flags should remain internal {exit_reason:?}") + panic!("Internal return flags should remain internal {call_result:?}") } }; @@ -273,75 +409,15 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, db, error: None, precompiles, + #[cfg(feature = "optimism")] + l1_block_info: None, }, inspector, + handler: Handler::mainnet::(), _phantomdata: PhantomData {}, } } - fn finalize(&mut self, gas: &Gas) -> (HashMap, Vec, u64, u64) { - let caller = self.data.env.tx.caller; - let coinbase = self.data.env.block.coinbase; - let (gas_used, gas_refunded) = - if crate::USE_GAS { - let effective_gas_price = self.data.env.effective_gas_price(); - let basefee = self.data.env.block.basefee; - - let gas_refunded = if self.env().cfg.is_gas_refund_disabled() { - 0 - } else { - // EIP-3529: Reduction in refunds - let max_refund_quotient = if SPEC::enabled(LONDON) { 5 } else { 2 }; - min(gas.refunded() as u64, gas.spend() / max_refund_quotient) - }; - - // return balance of not spend gas. - let Ok((caller_account, _)) = - self.data.journaled_state.load_account(caller, self.data.db) - else { - panic!("caller account not found"); - }; - - caller_account.info.balance = caller_account.info.balance.saturating_add( - effective_gas_price * U256::from(gas.remaining() + gas_refunded), - ); - - // transfer fee to coinbase/beneficiary. - if !self.data.env.cfg.disable_coinbase_tip { - // EIP-1559 discard basefee for coinbase transfer. Basefee amount of gas is discarded. - let coinbase_gas_price = if SPEC::enabled(LONDON) { - effective_gas_price.saturating_sub(basefee) - } else { - effective_gas_price - }; - - let Ok((coinbase_account, _)) = self - .data - .journaled_state - .load_account(coinbase, self.data.db) - else { - panic!("coinbase account not found"); - }; - coinbase_account.mark_touch(); - coinbase_account.info.balance = coinbase_account.info.balance.saturating_add( - coinbase_gas_price * U256::from(gas.spend() - gas_refunded), - ); - } - - (gas.spend() - gas_refunded, gas_refunded) - } else { - // touch coinbase - let _ = self - .data - .journaled_state - .load_account(coinbase, self.data.db); - self.data.journaled_state.touch(&coinbase); - (0, 0) - }; - let (new_state, logs) = self.data.journaled_state.finalize(); - (new_state, logs, gas_used, gas_refunded) - } - #[inline(never)] fn prepare_create(&mut self, inputs: &CreateInputs) -> Result { let gas = Gas::new(inputs.gas_limit); @@ -920,3 +996,142 @@ impl<'a, GSPEC: Spec, DB: Database + 'a, const INSPECT: bool> Host } } } + +#[cfg(feature = "optimism")] +#[cfg(test)] +mod tests { + use super::*; + + use crate::db::InMemoryDB; + use crate::primitives::{specification::BedrockSpec, state::AccountInfo, SpecId}; + + #[test] + fn test_commit_mint_value() { + let caller = B160::zero(); + let mint_value = Some(1u128); + let mut db = InMemoryDB::default(); + db.insert_account_info( + caller, + AccountInfo { + nonce: 0, + balance: U256::from(100), + code_hash: B256::zero(), + code: None, + }, + ); + let mut journal = JournaledState::new(0, SpecId::BERLIN); + journal + .initial_account_load(caller, &[U256::from(100)], &mut db) + .unwrap(); + assert!( + EVMImpl::::commit_mint_value( + caller, + mint_value, + &mut db, + &mut journal + ) + .is_ok(), + ); + + // Check the account balance is updated. + let (account, _) = journal.load_account(caller, &mut db).unwrap(); + assert_eq!(account.info.balance, U256::from(101)); + + // No mint value should be a no-op. + assert!( + EVMImpl::::commit_mint_value( + caller, + None, + &mut db, + &mut journal + ) + .is_ok(), + ); + let (account, _) = journal.load_account(caller, &mut db).unwrap(); + assert_eq!(account.info.balance, U256::from(101)); + } + + #[test] + fn test_remove_l1_cost_non_deposit() { + let caller = B160::zero(); + let mut db = InMemoryDB::default(); + let mut journal = JournaledState::new(0, SpecId::BERLIN); + let slots = &[U256::from(100)]; + journal + .initial_account_load(caller, slots, &mut db) + .unwrap(); + assert!(EVMImpl::::remove_l1_cost( + true, + caller, + U256::ZERO, + &mut db, + &mut journal + ) + .is_ok(),); + } + + #[test] + fn test_remove_l1_cost() { + let caller = B160::zero(); + let mut db = InMemoryDB::default(); + db.insert_account_info( + caller, + AccountInfo { + nonce: 0, + balance: U256::from(100), + code_hash: B256::zero(), + code: None, + }, + ); + let mut journal = JournaledState::new(0, SpecId::BERLIN); + journal + .initial_account_load(caller, &[U256::from(100)], &mut db) + .unwrap(); + assert!(EVMImpl::::remove_l1_cost( + false, + caller, + U256::from(1), + &mut db, + &mut journal + ) + .is_ok(),); + + // Check the account balance is updated. + let (account, _) = journal.load_account(caller, &mut db).unwrap(); + assert_eq!(account.info.balance, U256::from(99)); + } + + #[test] + fn test_remove_l1_cost_lack_of_funds() { + let caller = B160::zero(); + let mut db = InMemoryDB::default(); + db.insert_account_info( + caller, + AccountInfo { + nonce: 0, + balance: U256::from(100), + code_hash: B256::zero(), + code: None, + }, + ); + let mut journal = JournaledState::new(0, SpecId::BERLIN); + journal + .initial_account_load(caller, &[U256::from(100)], &mut db) + .unwrap(); + assert_eq!( + EVMImpl::::remove_l1_cost( + false, + caller, + U256::from(101), + &mut db, + &mut journal + ), + Err(EVMError::Transaction( + InvalidTransaction::LackOfFundForMaxFee { + fee: 101u64, + balance: U256::from(100), + }, + )) + ); + } +} diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs new file mode 100644 index 0000000000..9a5e2f5b2f --- /dev/null +++ b/crates/revm/src/handler.rs @@ -0,0 +1,90 @@ +pub mod mainnet; +#[cfg(feature = "optimism")] +pub mod optimism; + +use revm_interpreter::primitives::db::Database; +use revm_interpreter::primitives::{EVMError, EVMResultGeneric}; + +use crate::interpreter::{Gas, InstructionResult}; +use crate::primitives::{Env, Spec}; +use crate::EVMData; + +/// Handle call return and return final gas value. +type CallReturnHandle = fn(&Env, InstructionResult, Gas) -> Gas; + +/// Reimburse the caller with ethereum it didn't spent. +type ReimburseCallerHandle = + fn(&mut EVMData<'_, DB>, &Gas, u64) -> EVMResultGeneric<(), ::Error>; + +/// Reward beneficiary with transaction rewards. +type RewardBeneficiaryHandle = ReimburseCallerHandle; + +/// Calculate gas refund for transaction. +type CalculateGasRefundHandle = fn(&Env, &Gas) -> u64; + +/// Handler acts as a proxy and allow to define different behavior for different +/// sections of the code. This allows nice integration of different chains or +/// to disable some mainnet behavior. +pub struct Handler { + // Uses env, call resul and returned gas from the call to determine the gas + // that is returned from transaction execution.. + pub call_return: CallReturnHandle, + pub reimburse_caller: ReimburseCallerHandle, + pub reward_beneficiary: RewardBeneficiaryHandle, + pub calculate_gas_refund: CalculateGasRefundHandle, +} + +impl Handler { + /// Handler for the mainnet + pub fn mainnet() -> Self { + Self { + call_return: mainnet::handle_call_return::, + calculate_gas_refund: mainnet::calculate_gas_refund::, + reimburse_caller: mainnet::handle_reimburse_caller::, + reward_beneficiary: mainnet::reward_beneficiary::, + } + } + + /// Handler for the optimism + #[cfg(feature = "optimism")] + pub fn optimism() -> Self { + Self { + call_return: optimism::handle_call_return::, + // we reinburse caller the same was as in mainnet. + // Refund is calculated differently then mainnet. + reimburse_caller: mainnet::handle_reimburse_caller::, + calculate_gas_refund: optimism::calculate_gas_refund::, + reward_beneficiary: optimism::reward_beneficiary::, + } + } + + /// Handle call return, depending on instruction result gas will be reimbursed or not. + pub fn call_return(&self, env: &Env, call_result: InstructionResult, returned_gas: Gas) -> Gas { + (self.call_return)(env, call_result, returned_gas) + } + + /// Reimburse the caller with gas that were not spend. + pub fn reimburse_caller( + &self, + data: &mut EVMData<'_, DB>, + gas: &Gas, + gas_refund: u64, + ) -> Result<(), EVMError> { + (self.reimburse_caller)(data, gas, gas_refund) + } + + /// Calculate gas refund for transaction. Some chains have it disabled. + pub fn calculate_gas_refund(&self, env: &Env, gas: &Gas) -> u64 { + (self.calculate_gas_refund)(env, gas) + } + + /// Reward beneficiary + pub fn reward_beneficiary( + &self, + data: &mut EVMData<'_, DB>, + gas: &Gas, + gas_refund: u64, + ) -> Result<(), EVMError> { + (self.reward_beneficiary)(data, gas, gas_refund) + } +} diff --git a/crates/revm/src/handler/mainnet.rs b/crates/revm/src/handler/mainnet.rs new file mode 100644 index 0000000000..81f12fdb6c --- /dev/null +++ b/crates/revm/src/handler/mainnet.rs @@ -0,0 +1,154 @@ +//! Mainnet related handlers. +use revm_interpreter::primitives::EVMError; + +use crate::{ + interpreter::{return_ok, return_revert, Gas, InstructionResult}, + primitives::{db::Database, Env, Spec, SpecId::LONDON, U256}, + EVMData, +}; + +/// Handle output of the transaction +pub fn handle_call_return( + env: &Env, + call_result: InstructionResult, + returned_gas: Gas, +) -> Gas { + let tx_gas_limit = env.tx.gas_limit; + // Spend the gas limit. Gas is reimbursed when the tx returns successfully. + let mut gas = Gas::new(tx_gas_limit); + gas.record_cost(tx_gas_limit); + + match call_result { + return_ok!() => { + gas.erase_cost(returned_gas.remaining()); + gas.record_refund(returned_gas.refunded()); + } + return_revert!() => { + gas.erase_cost(returned_gas.remaining()); + } + _ => {} + } + gas +} + +#[inline] +pub fn handle_reimburse_caller( + data: &mut EVMData<'_, DB>, + gas: &Gas, + gas_refund: u64, +) -> Result<(), EVMError> { + let _ = data; + let caller = data.env.tx.caller; + let effective_gas_price = data.env.effective_gas_price(); + + // return balance of not spend gas. + let (caller_account, _) = data + .journaled_state + .load_account(caller, data.db) + .map_err(EVMError::Database)?; + + caller_account.info.balance = caller_account + .info + .balance + .saturating_add(effective_gas_price * U256::from(gas.remaining() + gas_refund)); + + Ok(()) +} + +/// Reward beneficiary with gas fee. +#[inline] +pub fn reward_beneficiary( + data: &mut EVMData<'_, DB>, + gas: &Gas, + gas_refund: u64, +) -> Result<(), EVMError> { + let beneficiary = data.env.block.coinbase; + let effective_gas_price = data.env.effective_gas_price(); + + // transfer fee to coinbase/beneficiary. + // EIP-1559 discard basefee for coinbase transfer. Basefee amount of gas is discarded. + let coinbase_gas_price = if SPEC::enabled(LONDON) { + effective_gas_price.saturating_sub(data.env.block.basefee) + } else { + effective_gas_price + }; + + let (coinbase_account, _) = data + .journaled_state + .load_account(beneficiary, data.db) + .map_err(EVMError::Database)?; + + coinbase_account.mark_touch(); + coinbase_account.info.balance = coinbase_account + .info + .balance + .saturating_add(coinbase_gas_price * U256::from(gas.spend() - gas_refund)); + + Ok(()) +} + +/// Calculate gas refund for transaction. +/// +/// If config is set to disable gas refund, it will return 0. +/// +/// If spec is set to london, it will decrease the maximum refund amount to 5th part of +/// gas spend. (Before london it was 2th part of gas spend) +#[inline] +pub fn calculate_gas_refund(env: &Env, gas: &Gas) -> u64 { + if env.cfg.is_gas_refund_disabled() { + 0 + } else { + // EIP-3529: Reduction in refunds + let max_refund_quotient = if SPEC::enabled(LONDON) { 5 } else { 2 }; + (gas.refunded() as u64).min(gas.spend() / max_refund_quotient) + } +} + +#[cfg(test)] +mod tests { + use revm_interpreter::primitives::CancunSpec; + + use super::*; + + #[test] + fn test_consume_gas() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + + let gas = handle_call_return::(&env, InstructionResult::Stop, Gas::new(90)); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 0); + } + + #[test] + fn test_consume_gas_with_refund() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + + let mut return_gas = Gas::new(90); + return_gas.record_refund(30); + + let gas = + handle_call_return::(&env, InstructionResult::Stop, return_gas.clone()); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 30); + + let gas = handle_call_return::(&env, InstructionResult::Revert, return_gas); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 0); + } + + #[test] + fn test_revert_gas() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + + let gas = handle_call_return::(&env, InstructionResult::Revert, Gas::new(90)); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 0); + } +} diff --git a/crates/revm/src/handler/optimism.rs b/crates/revm/src/handler/optimism.rs new file mode 100644 index 0000000000..e71685f296 --- /dev/null +++ b/crates/revm/src/handler/optimism.rs @@ -0,0 +1,222 @@ +//! Handler related to Optimism chain + +use core::ops::Mul; + +use super::mainnet; +use crate::{ + interpreter::{return_ok, return_revert, Gas, InstructionResult}, + optimism, + primitives::{db::Database, EVMError, Env, Spec, SpecId::REGOLITH, U256}, + EVMData, +}; + +/// Handle output of the transaction +pub fn handle_call_return( + env: &Env, + call_result: InstructionResult, + returned_gas: Gas, +) -> Gas { + let is_deposit = env.tx.optimism.source_hash.is_some(); + let is_optimism = env.cfg.optimism; + let tx_system = env.tx.optimism.is_system_transaction; + let tx_gas_limit = env.tx.gas_limit; + let is_regolith = SPEC::enabled(REGOLITH); + // Spend the gas limit. Gas is reimbursed when the tx returns successfully. + let mut gas = Gas::new(tx_gas_limit); + gas.record_cost(tx_gas_limit); + + match call_result { + return_ok!() => { + // On Optimism, deposit transactions report gas usage uniquely to other + // transactions due to them being pre-paid on L1. + // + // Hardfork Behavior: + // - Bedrock (success path): + // - Deposit transactions (non-system) report their gas limit as the usage. + // No refunds. + // - Deposit transactions (system) report 0 gas used. No refunds. + // - Regular transactions report gas usage as normal. + // - Regolith (success path): + // - Deposit transactions (all) report their gas used as normal. Refunds + // enabled. + // - Regular transactions report their gas used as normal. + if is_optimism && (!is_deposit || is_regolith) { + // For regular transactions prior to Regolith and all transactions after + // Regolith, gas is reported as normal. + gas.erase_cost(returned_gas.remaining()); + gas.record_refund(returned_gas.refunded()); + } else if is_deposit && tx_system.unwrap_or(false) { + // System transactions were a special type of deposit transaction in + // the Bedrock hardfork that did not incur any gas costs. + gas.erase_cost(tx_gas_limit); + } + } + return_revert!() => { + // On Optimism, deposit transactions report gas usage uniquely to other + // transactions due to them being pre-paid on L1. + // + // Hardfork Behavior: + // - Bedrock (revert path): + // - Deposit transactions (all) report the gas limit as the amount of gas + // used on failure. No refunds. + // - Regular transactions receive a refund on remaining gas as normal. + // - Regolith (revert path): + // - Deposit transactions (all) report the actual gas used as the amount of + // gas used on failure. Refunds on remaining gas enabled. + // - Regular transactions receive a refund on remaining gas as normal. + if is_optimism && (!is_deposit || is_regolith) { + gas.erase_cost(returned_gas.remaining()); + } + } + _ => {} + } + gas +} + +#[inline] +pub fn calculate_gas_refund(env: &Env, gas: &Gas) -> u64 { + let is_deposit = env.cfg.optimism && env.tx.optimism.source_hash.is_some(); + + // Prior to Regolith, deposit transactions did not receive gas refunds. + let is_gas_refund_disabled = env.cfg.optimism && is_deposit && !SPEC::enabled(REGOLITH); + if is_gas_refund_disabled { + 0 + } else { + mainnet::calculate_gas_refund::(env, gas) + } +} + +/// Reward beneficiary with gas fee. +#[inline] +pub fn reward_beneficiary( + data: &mut EVMData<'_, DB>, + gas: &Gas, + gas_refund: u64, +) -> Result<(), EVMError> { + let is_deposit = data.env.cfg.optimism && data.env.tx.optimism.source_hash.is_some(); + let disable_coinbase_tip = data.env.cfg.optimism && is_deposit; + + // transfer fee to coinbase/beneficiary. + if !disable_coinbase_tip { + mainnet::reward_beneficiary::(data, gas, gas_refund)?; + } + + if data.env.cfg.optimism && !is_deposit { + // If the transaction is not a deposit transaction, fees are paid out + // to both the Base Fee Vault as well as the L1 Fee Vault. + let Some(l1_block_info) = data.l1_block_info.clone() else { + panic!("[OPTIMISM] Failed to load L1 block information."); + }; + + let Some(enveloped_tx) = &data.env.tx.optimism.enveloped_tx else { + panic!("[OPTIMISM] Failed to load enveloped transaction."); + }; + + let l1_cost = l1_block_info.calculate_tx_l1_cost::(enveloped_tx, is_deposit); + + // Send the L1 cost of the transaction to the L1 Fee Vault. + let Ok((l1_fee_vault_account, _)) = data + .journaled_state + .load_account(optimism::L1_FEE_RECIPIENT, data.db) + else { + panic!("[OPTIMISM] Failed to load L1 Fee Vault account"); + }; + l1_fee_vault_account.mark_touch(); + l1_fee_vault_account.info.balance += l1_cost; + + // Send the base fee of the transaction to the Base Fee Vault. + let Ok((base_fee_vault_account, _)) = data + .journaled_state + .load_account(optimism::BASE_FEE_RECIPIENT, data.db) + else { + panic!("[OPTIMISM] Failed to load Base Fee Vault account"); + }; + base_fee_vault_account.mark_touch(); + base_fee_vault_account.info.balance += + l1_block_info.l1_base_fee.mul(U256::from(gas.spend())); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::primitives::{BedrockSpec, RegolithSpec}; + + use super::*; + use crate::primitives::B256; + + #[test] + fn test_revert_gas() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + env.cfg.optimism = true; + env.tx.optimism.source_hash = None; + + let gas = handle_call_return::(&env, InstructionResult::Revert, Gas::new(90)); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 0); + } + + #[test] + fn test_revert_gas_non_optimism() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + env.cfg.optimism = false; + env.tx.optimism.source_hash = None; + + let gas = handle_call_return::(&env, InstructionResult::Revert, Gas::new(90)); + // else branch takes all gas. + assert_eq!(gas.remaining(), 0); + assert_eq!(gas.spend(), 100); + assert_eq!(gas.refunded(), 0); + } + + #[test] + fn test_consume_gas() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + env.cfg.optimism = true; + env.tx.optimism.source_hash = Some(B256::zero()); + + let gas = handle_call_return::(&env, InstructionResult::Stop, Gas::new(90)); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 0); + } + + #[test] + fn test_consume_gas_with_refund() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + env.cfg.optimism = true; + env.tx.optimism.source_hash = Some(B256::zero()); + + let mut ret_gas = Gas::new(90); + ret_gas.record_refund(20); + + let gas = + handle_call_return::(&env, InstructionResult::Stop, ret_gas.clone()); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 20); + + let gas = handle_call_return::(&env, InstructionResult::Revert, ret_gas); + assert_eq!(gas.remaining(), 90); + assert_eq!(gas.spend(), 10); + assert_eq!(gas.refunded(), 0); + } + + #[test] + fn test_consume_gas_sys_deposit_tx() { + let mut env = Env::default(); + env.tx.gas_limit = 100; + env.cfg.optimism = true; + env.tx.optimism.source_hash = Some(B256::zero()); + + let gas = handle_call_return::(&env, InstructionResult::Stop, Gas::new(90)); + assert_eq!(gas.remaining(), 0); + assert_eq!(gas.spend(), 100); + assert_eq!(gas.refunded(), 0); + } +} diff --git a/crates/revm/src/inspector/customprinter.rs b/crates/revm/src/inspector/customprinter.rs index 426caf6a21..44881be409 100644 --- a/crates/revm/src/inspector/customprinter.rs +++ b/crates/revm/src/inspector/customprinter.rs @@ -126,8 +126,9 @@ impl Inspector for CustomPrintTracer { #[cfg(test)] mod test { - #[cfg(not(feature = "no_gas_measuring"))] #[test] + #[cfg(not(feature = "no_gas_measuring"))] + #[cfg(not(feature = "optimism"))] fn gas_calculation_underflow() { use crate::primitives::hex_literal; // https://github.com/bluealloy/revm/issues/277 diff --git a/crates/revm/src/inspector/gas.rs b/crates/revm/src/inspector/gas.rs index 7886ed00be..fa19c7699d 100644 --- a/crates/revm/src/inspector/gas.rs +++ b/crates/revm/src/inspector/gas.rs @@ -93,13 +93,8 @@ impl Inspector for GasInspector { #[cfg(test)] mod tests { - use crate::db::BenchmarkDB; - use crate::interpreter::{ - opcode, CallInputs, CreateInputs, Gas, InstructionResult, Interpreter, OpCode, - }; - use crate::primitives::{ - hex_literal::hex, Bytecode, Bytes, ResultAndState, TransactTo, B160, B256, - }; + use crate::interpreter::{CallInputs, CreateInputs, Gas, InstructionResult, Interpreter}; + use crate::primitives::{Bytes, B160, B256}; use crate::{inspectors::GasInspector, Database, EVMData, Inspector}; #[derive(Default, Debug)] @@ -209,7 +204,14 @@ mod tests { } #[test] + #[cfg(not(feature = "optimism"))] fn test_gas_inspector() { + use crate::db::BenchmarkDB; + use crate::interpreter::{opcode, OpCode}; + use crate::primitives::{ + hex_literal::hex, Bytecode, Bytes, ResultAndState, TransactTo, B160, + }; + let contract_data: Bytes = Bytes::from(vec![ opcode::PUSH1, 0x1, diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index 6b93b8705b..0770d0325c 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -7,9 +7,13 @@ extern crate alloc; pub mod db; mod evm; mod evm_impl; +pub mod handler; mod inspector; mod journaled_state; +#[cfg(feature = "optimism")] +pub mod optimism; + #[cfg(all(feature = "with-serde", not(feature = "serde")))] compile_error!("`with-serde` feature has been renamed to `serde`."); @@ -41,3 +45,9 @@ pub use revm_interpreter::primitives; // reexport inspector implementations pub use inspector::inspectors; pub use inspector::Inspector; + +// export Optimism types, helpers, and constants +#[cfg(feature = "optimism")] +pub use optimism::{L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT}; + +pub use handler::Handler; diff --git a/crates/revm/src/optimism.rs b/crates/revm/src/optimism.rs new file mode 100644 index 0000000000..e33d1d8eff --- /dev/null +++ b/crates/revm/src/optimism.rs @@ -0,0 +1,178 @@ +//! Optimism-specific constants, types, and helpers. + +use core::ops::Mul; +use revm_interpreter::primitives::{ + db::Database, hex_literal::hex, Bytes, Spec, SpecId, B160, U256, +}; + +const ZERO_BYTE_COST: u64 = 4; +const NON_ZERO_BYTE_COST: u64 = 16; + +const L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1u64, 0, 0, 0]); +const L1_OVERHEAD_SLOT: U256 = U256::from_limbs([5u64, 0, 0, 0]); +const L1_SCALAR_SLOT: U256 = U256::from_limbs([6u64, 0, 0, 0]); + +/// The address of L1 fee recipient. +pub const L1_FEE_RECIPIENT: B160 = B160(hex!("420000000000000000000000000000000000001A")); + +/// The address of the base fee recipient. +pub const BASE_FEE_RECIPIENT: B160 = B160(hex!("4200000000000000000000000000000000000019")); + +/// The address of the L1Block contract. +pub const L1_BLOCK_CONTRACT: B160 = B160(hex!("4200000000000000000000000000000000000015")); + +/// L1 block info +/// +/// We can extract L1 epoch data from each L2 block, by looking at the `setL1BlockValues` +/// transaction data. This data is then used to calculate the L1 cost of a transaction. +/// +/// Here is the format of the `setL1BlockValues` transaction data: +/// +/// setL1BlockValues(uint64 _number, uint64 _timestamp, uint256 _basefee, bytes32 _hash, +/// uint64 _sequenceNumber, bytes32 _batcherHash, uint256 _l1FeeOverhead, uint256 _l1FeeScalar) +/// +/// For now, we only care about the fields necessary for L1 cost calculation. +#[derive(Clone, Debug)] +pub struct L1BlockInfo { + /// The base fee of the L1 origin block. + pub l1_base_fee: U256, + /// The current L1 fee overhead. + pub l1_fee_overhead: U256, + /// The current L1 fee scalar. + pub l1_fee_scalar: U256, +} + +impl L1BlockInfo { + pub fn try_fetch( + db: &mut DB, + is_optimism: bool, + ) -> Result, DB::Error> { + is_optimism + .then(|| { + let l1_base_fee = db.storage(L1_BLOCK_CONTRACT, L1_BASE_FEE_SLOT)?; + let l1_fee_overhead = db.storage(L1_BLOCK_CONTRACT, L1_OVERHEAD_SLOT)?; + let l1_fee_scalar = db.storage(L1_BLOCK_CONTRACT, L1_SCALAR_SLOT)?; + + Ok(L1BlockInfo { + l1_base_fee, + l1_fee_overhead, + l1_fee_scalar, + }) + }) + .map_or(Ok(None), |v| v.map(Some)) + } + + /// Calculate the data gas for posting the transaction on L1. Calldata costs 16 gas per non-zero + /// byte and 4 gas per zero byte. + /// + /// Prior to regolith, an extra 68 non-zero bytes were included in the rollup data costs to + /// account for the empty signature. + pub fn data_gas(&self, input: &Bytes) -> U256 { + let mut rollup_data_gas_cost = U256::from(input.iter().fold(0, |acc, byte| { + acc + if *byte == 0x00 { + ZERO_BYTE_COST + } else { + NON_ZERO_BYTE_COST + } + })); + + // Prior to regolith, an extra 68 non zero bytes were included in the rollup data costs. + if !SPEC::enabled(SpecId::REGOLITH) { + rollup_data_gas_cost += U256::from(NON_ZERO_BYTE_COST).mul(U256::from(68)); + } + + rollup_data_gas_cost + } + + /// Calculate the gas cost of a transaction based on L1 block data posted on L2 + pub fn calculate_tx_l1_cost(&self, input: &Bytes, is_deposit: bool) -> U256 { + let rollup_data_gas_cost = self.data_gas::(input); + + if is_deposit || rollup_data_gas_cost == U256::ZERO { + return U256::ZERO; + } + + rollup_data_gas_cost + .saturating_add(self.l1_fee_overhead) + .saturating_mul(self.l1_base_fee) + .saturating_mul(self.l1_fee_scalar) + .checked_div(U256::from(1_000_000)) + .unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::primitives::specification::*; + + #[test] + fn test_data_gas_non_zero_bytes() { + let l1_block_info = L1BlockInfo { + l1_base_fee: U256::from(1_000_000), + l1_fee_overhead: U256::from(1_000_000), + l1_fee_scalar: U256::from(1_000_000), + }; + + // 0xFACADE = 6 nibbles = 3 bytes + // 0xFACADE = 1111 1010 . 1100 1010 . 1101 1110 + + // Pre-regolith (ie bedrock) has an extra 68 non-zero bytes + // gas cost = 3 non-zero bytes * NON_ZERO_BYTE_COST + NON_ZERO_BYTE_COST * 68 + // gas cost = 3 * 16 + 68 * 16 = 1136 + let input = Bytes::from(hex!("FACADE").to_vec()); + let bedrock_data_gas = l1_block_info.data_gas::(&input); + assert_eq!(bedrock_data_gas, U256::from(1136)); + + // Regolith has no added 68 non zero bytes + // gas cost = 3 * 16 = 48 + let regolith_data_gas = l1_block_info.data_gas::(&input); + assert_eq!(regolith_data_gas, U256::from(48)); + } + + #[test] + fn test_data_gas_zero_bytes() { + let l1_block_info = L1BlockInfo { + l1_base_fee: U256::from(1_000_000), + l1_fee_overhead: U256::from(1_000_000), + l1_fee_scalar: U256::from(1_000_000), + }; + + // 0xFA00CA00DE = 10 nibbles = 5 bytes + // 0xFA00CA00DE = 1111 1010 . 0000 0000 . 1100 1010 . 0000 0000 . 1101 1110 + + // Pre-regolith (ie bedrock) has an extra 68 non-zero bytes + // gas cost = 3 non-zero * NON_ZERO_BYTE_COST + 2 * ZERO_BYTE_COST + NON_ZERO_BYTE_COST * 68 + // gas cost = 3 * 16 + 2 * 4 + 68 * 16 = 1144 + let input = Bytes::from(hex!("FA00CA00DE").to_vec()); + let bedrock_data_gas = l1_block_info.data_gas::(&input); + assert_eq!(bedrock_data_gas, U256::from(1144)); + + // Regolith has no added 68 non zero bytes + // gas cost = 3 * 16 + 2 * 4 = 56 + let regolith_data_gas = l1_block_info.data_gas::(&input); + assert_eq!(regolith_data_gas, U256::from(56)); + } + + #[test] + fn test_calculate_tx_l1_cost() { + let l1_block_info = L1BlockInfo { + l1_base_fee: U256::from(1_000), + l1_fee_overhead: U256::from(1_000), + l1_fee_scalar: U256::from(1_000), + }; + + // The gas cost here should be zero since the tx is a deposit + let input = Bytes::from(hex!("FACADE").to_vec()); + let gas_cost = l1_block_info.calculate_tx_l1_cost::(&input, true); + assert_eq!(gas_cost, U256::ZERO); + + let gas_cost = l1_block_info.calculate_tx_l1_cost::(&input, false); + assert_eq!(gas_cost, U256::from(1048)); + + // Zero rollup data gas cost should result in zero for non-deposits + let input = Bytes::from(hex!("").to_vec()); + let gas_cost = l1_block_info.calculate_tx_l1_cost::(&input, false); + assert_eq!(gas_cost, U256::ZERO); + } +}