diff --git a/crates/optimism/src/handler_register.rs b/crates/optimism/src/handler_register.rs index 89e81b12f0..635ccfa524 100644 --- a/crates/optimism/src/handler_register.rs +++ b/crates/optimism/src/handler_register.rs @@ -4,7 +4,7 @@ use super::{ optimism_spec_to_generic, OptimismContext, OptimismHaltReason, OptimismInvalidTransaction, OptimismSpec, OptimismSpecId, OptimismTransaction, OptimismWiring, }; -use crate::{BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT}; +use crate::{l1block::OPERATOR_FEE_RECIPIENT, BASE_FEE_RECIPIENT, L1_FEE_RECIPIENT}; use core::ops::Mul; use revm::{ database_interface::Database, @@ -47,6 +47,7 @@ where // Refund is calculated differently then mainnet. handler.execution.last_frame_return = Arc::new(last_frame_return::); handler.post_execution.refund = Arc::new(refund::); + handler.post_execution.reimburse_caller = Arc::new(reimburse_caller::); handler.post_execution.reward_beneficiary = Arc::new(reward_beneficiary::); // In case of halt of deposit transaction return Error. @@ -177,6 +178,39 @@ pub fn refund( } } +/// Reimburse the transaction caller. +#[inline] +pub fn reimburse_caller( + context: &mut Context, + gas: &Gas, +) -> EVMResultGeneric<(), EvmWiringT> { + mainnet::reimburse_caller::(context, gas)?; + let caller = *context.evm.env.tx.caller(); + let caller_account = context + .evm + .inner + .journaled_state + .load_account(caller, &mut context.evm.inner.db) + .map_err(EVMError::Database)?; + // In additional to the normal transaction fee, additionally refund the caller + // for the operator fee. + let operator_fee_refund = context + .evm + .inner + .chain + .l1_block_info() + .expect("L1BlockInfo should be loaded") + .operator_fee_refund(gas, SPEC::OPTIMISM_SPEC_ID); + + caller_account.data.info.balance = caller_account + .data + .info + .balance + .saturating_add(operator_fee_refund); + + Ok(()) +} + /// Load precompiles for Optimism chain. #[inline] pub fn load_precompiles( @@ -248,6 +282,7 @@ pub fn deduct_caller( // 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. + // Additionally deduct the operator fee from the caller's account. if context.evm.inner.env.tx.source_hash().is_none() { // get envelope let Some(enveloped_tx) = &context.evm.inner.env.tx.enveloped_tx() else { @@ -273,6 +308,22 @@ pub fn deduct_caller( )); } caller_account.info.balance = caller_account.info.balance.saturating_sub(tx_l1_cost); + + // Deduct the operator fee from the caller's account. + let gas_limit = U256::from(context.evm.inner.env.tx.gas_limit()); + + let operator_fee_charge = context + .evm + .inner + .chain + .l1_block_info() + .expect("L1BlockInfo should be loaded") + .operator_fee_charge(gas_limit, SPEC::OPTIMISM_SPEC_ID); + + caller_account.info.balance = caller_account + .info + .balance + .saturating_sub(operator_fee_charge); } Ok(()) } @@ -306,6 +357,10 @@ pub fn reward_beneficiary( }; let l1_cost = l1_block_info.calculate_tx_l1_cost(enveloped_tx, SPEC::OPTIMISM_SPEC_ID); + let operator_fee_cost = l1_block_info.operator_fee_charge( + U256::from(gas.spent() - gas.refunded() as u64), + SPEC::OPTIMISM_SPEC_ID, + ); // Send the L1 cost of the transaction to the L1 Fee Vault. let mut l1_fee_vault_account = context @@ -332,6 +387,20 @@ pub fn reward_beneficiary( .block .basefee() .mul(U256::from(gas.spent() - gas.refunded() as u64)); + + // Send the operator fee of the transaction to the coinbase. + let operator_fee_vault_account = context + .evm + .inner + .journaled_state + .load_account(OPERATOR_FEE_RECIPIENT, &mut context.evm.inner.db) + .map_err(EVMError::Database)?; + + operator_fee_vault_account.data.info.balance = operator_fee_vault_account + .data + .info + .balance + .saturating_add(operator_fee_cost); } Ok(()) } @@ -428,7 +497,9 @@ pub fn end( #[cfg(test)] mod tests { use super::*; - use crate::{BedrockSpec, L1BlockInfo, LatestSpec, OptimismEvmWiring, RegolithSpec}; + use crate::{ + BedrockSpec, HoloceneSpec, L1BlockInfo, LatestSpec, OptimismEvmWiring, RegolithSpec, + }; use database::InMemoryDB; use revm::{ database_interface::EmptyDB, @@ -630,6 +701,40 @@ mod tests { assert_eq!(account.info.balance, U256::from(1)); } + #[test] + fn test_remove_operator_cost() { + let caller = Address::ZERO; + let mut db = InMemoryDB::default(); + db.insert_account_info( + caller, + AccountInfo { + balance: U256::from(151), + ..Default::default() + }, + ); + let mut context = Context::::new_with_db(db); + *context.evm.chain.l1_block_info_mut() = Some(L1BlockInfo { + operator_fee_scalar: Some(U256::from(10_000_000)), + operator_fee_constant: Some(U256::from(50)), + ..Default::default() + }); + context.evm.inner.env.tx.base.gas_limit = 10; + + // operator fee cost is operator_fee_scalar * gas_limit / 1e6 + operator_fee_constant + // 10_000_000 * 10 / 1_000_000 + 50 = 150 + context.evm.inner.env.tx.enveloped_tx = Some(bytes!("FACADE")); + deduct_caller::(&mut context).unwrap(); + + // Check the account balance is updated. + let account = context + .evm + .inner + .journaled_state + .load_account(caller, &mut context.evm.inner.db) + .unwrap(); + assert_eq!(account.info.balance, U256::from(1)); + } + #[test] fn test_remove_l1_cost_lack_of_funds() { let caller = Address::ZERO; diff --git a/crates/optimism/src/l1block.rs b/crates/optimism/src/l1block.rs index 5fdc9c794e..3f5f00b3a7 100644 --- a/crates/optimism/src/l1block.rs +++ b/crates/optimism/src/l1block.rs @@ -2,6 +2,7 @@ use crate::fast_lz::flz_compress_len; use core::ops::Mul; use revm::{ database_interface::Database, + interpreter::Gas, primitives::{address, Address, U256}, }; @@ -16,6 +17,17 @@ const BASE_FEE_SCALAR_OFFSET: usize = 16; /// The two 4-byte Ecotone fee scalar values are packed into the same storage slot as the 8-byte sequence number. /// Byte offset within the storage slot of the 4-byte blobBaseFeeScalar attribute. const BLOB_BASE_FEE_SCALAR_OFFSET: usize = 20; +/// The two 8-byte Isthmus operator fee scalar values are similarly packed. Byte offset within +/// the storage slot of the 8-byte operatorFeeScalar attribute. +const OPERATOR_FEE_SCALAR_OFFSET: usize = 4; +/// The two 8-byte Isthmus operator fee scalar values are similarly packed. Byte offset within +/// the storage slot of the 8-byte operatorFeeConstant attribute. +const OPERATOR_FEE_CONSTANT_OFFSET: usize = 8; + +/// The fixed point decimal scaling factor associated with the operator fee scalar. +/// +/// Allows users to use 6 decimal points of precision when specifying the operator_fee_scalar. +const OPERATOR_FEE_SCALAR_DECIMAL: u64 = 1_000_000; 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]); @@ -28,6 +40,10 @@ const ECOTONE_L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([7u64, 0, 0, 0]); /// offsets [BASE_FEE_SCALAR_OFFSET] and [BLOB_BASE_FEE_SCALAR_OFFSET] respectively. const ECOTONE_L1_FEE_SCALARS_SLOT: U256 = U256::from_limbs([3u64, 0, 0, 0]); +/// This storage slot stores the 32-bit operatorFeeScalar and operatorFeeConstant attributes at +/// offsets [OPERATOR_FEE_SCALAR_OFFSET] and [OPERATOR_FEE_CONSTANT_OFFSET] respectively. +const OPERATOR_FEE_SCALARS_SLOT: U256 = U256::from_limbs([8u64, 0, 0, 0]); + /// An empty 64-bit set of scalar values. const EMPTY_SCALARS: [u8; 8] = [0u8; 8]; @@ -37,6 +53,9 @@ pub const L1_FEE_RECIPIENT: Address = address!("42000000000000000000000000000000 /// The address of the base fee recipient. pub const BASE_FEE_RECIPIENT: Address = address!("4200000000000000000000000000000000000019"); +/// The address of the operator fee recipient. +pub const OPERATOR_FEE_RECIPIENT: Address = address!("420000000000000000000000000000000000001B"); + /// The address of the L1Block contract. pub const L1_BLOCK_CONTRACT: Address = address!("4200000000000000000000000000000000000015"); @@ -63,8 +82,12 @@ pub struct L1BlockInfo { pub l1_blob_base_fee: Option, /// The current L1 blob base fee scalar. None if Ecotone is not activated. pub l1_blob_base_fee_scalar: Option, + /// The current L1 blob base fee. None if Isthmus is not activated, except if `empty_scalars` is `true`. + pub operator_fee_scalar: Option, + /// The current L1 blob base fee scalar. None if Isthmus is not activated. + pub operator_fee_constant: Option, /// True if Ecotone is activated, but the L1 fee scalars have not yet been set. - pub(crate) empty_scalars: bool, + pub(crate) empty_ecotone_scalars: bool, } impl L1BlockInfo { @@ -107,21 +130,56 @@ impl L1BlockInfo { // Check if the L1 fee scalars are empty. If so, we use the Bedrock cost function. The L1 fee overhead is // only necessary if `empty_scalars` is true, as it was deprecated in Ecotone. - let empty_scalars = l1_blob_base_fee.is_zero() + let empty_ecotone_scalars = l1_blob_base_fee.is_zero() && l1_fee_scalars[BASE_FEE_SCALAR_OFFSET..BLOB_BASE_FEE_SCALAR_OFFSET + 4] == EMPTY_SCALARS; - let l1_fee_overhead = empty_scalars + + let l1_fee_overhead = empty_ecotone_scalars .then(|| db.storage(L1_BLOCK_CONTRACT, L1_OVERHEAD_SLOT)) .transpose()?; - Ok(L1BlockInfo { - l1_base_fee, - l1_base_fee_scalar, - l1_blob_base_fee: Some(l1_blob_base_fee), - l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar), - empty_scalars, - l1_fee_overhead, - }) + // Pre-isthmus L1 block info + if !spec_id.is_enabled_in(OptimismSpecId::ISTHMUS) { + Ok(L1BlockInfo { + l1_base_fee, + l1_base_fee_scalar, + l1_blob_base_fee: Some(l1_blob_base_fee), + l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar), + empty_ecotone_scalars, + l1_fee_overhead, + ..Default::default() + }) + } else { + let operator_fee_scalars = db + .storage(L1_BLOCK_CONTRACT, OPERATOR_FEE_SCALARS_SLOT)? + .to_be_bytes::<32>(); + + // Post-isthmus L1 block info + // The `operator_fee_scalar` is stored as a big endian u32 at + // OPERATOR_FEE_SCALAR_OFFSET. + let operator_fee_scalar = U256::from_be_slice( + operator_fee_scalars + [OPERATOR_FEE_SCALAR_OFFSET..OPERATOR_FEE_SCALAR_OFFSET + 4] + .as_ref(), + ); + // The `operator_fee_constant` is stored as a big endian u64 at + // OPERATOR_FEE_CONSTANT_OFFSET. + let operator_fee_constant = U256::from_be_slice( + operator_fee_scalars + [OPERATOR_FEE_CONSTANT_OFFSET..OPERATOR_FEE_CONSTANT_OFFSET + 8] + .as_ref(), + ); + Ok(L1BlockInfo { + l1_base_fee, + l1_base_fee_scalar, + l1_blob_base_fee: Some(l1_blob_base_fee), + l1_blob_base_fee_scalar: Some(l1_blob_base_fee_scalar), + empty_ecotone_scalars, + l1_fee_overhead, + operator_fee_scalar: Some(operator_fee_scalar), + operator_fee_constant: Some(operator_fee_constant), + }) + } } } @@ -157,6 +215,44 @@ impl L1BlockInfo { rollup_data_gas_cost } + /// Calculate the operator fee for executing this transaction. + /// + /// Introduced in isthmus. Prior to isthmus, the operator fee is always zero. + pub fn operator_fee_charge(&self, gas_limit: U256, spec_id: OptimismSpecId) -> U256 { + if !spec_id.is_enabled_in(OptimismSpecId::ISTHMUS) { + return U256::ZERO; + } + let operator_fee_scalar = self + .operator_fee_scalar + .expect("Missing operator fee scalar for isthmus L1 Block"); + let operator_fee_constant = self + .operator_fee_constant + .expect("Missing operator fee constant for isthmus L1 Block"); + + let product = gas_limit.saturating_mul(operator_fee_scalar) + / (U256::from(OPERATOR_FEE_SCALAR_DECIMAL)); + + product.saturating_add(operator_fee_constant) + } + + /// Calculate the operator fee for executing this transaction. + /// + /// Introduced in isthmus. Prior to isthmus, the operator fee is always zero. + pub fn operator_fee_refund(&self, gas: &Gas, spec_id: OptimismSpecId) -> U256 { + if !spec_id.is_enabled_in(OptimismSpecId::ISTHMUS) { + return U256::ZERO; + } + + let operator_fee_scalar = self + .operator_fee_scalar + .expect("Missing operator fee scalar for isthmus L1 Block"); + + // We're computing the difference between two operator fees, so no need to include the + // constant. + + operator_fee_scalar.saturating_mul(U256::from(gas.remaining() + gas.refunded() as u64)) + } + // Calculate the estimated compressed transaction size in bytes, scaled by 1e6. // This value is computed based on the following formula: // max(minTransactionSize, intercept + fastlzCoef*fastlzSize) @@ -209,7 +305,7 @@ impl L1BlockInfo { // There is an edgecase where, for the very first Ecotone block (unless it is activated at Genesis), we must // use the Bedrock cost function. To determine if this is the case, we can check if the Ecotone parameters are // unset. - if self.empty_scalars { + if self.empty_ecotone_scalars { return self.calculate_tx_l1_cost_bedrock(input, spec_id); } @@ -367,7 +463,7 @@ mod tests { assert_eq!(gas_cost, U256::ZERO); // If the scalars are empty, the bedrock cost function should be used. - l1_block_info.empty_scalars = true; + l1_block_info.empty_ecotone_scalars = true; let input = bytes!("FACADE"); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, OptimismSpecId::ECOTONE); assert_eq!(gas_cost, U256::from(1048)); diff --git a/crates/optimism/src/lib.rs b/crates/optimism/src/lib.rs index 3b602b6297..f1915d2bda 100644 --- a/crates/optimism/src/lib.rs +++ b/crates/optimism/src/lib.rs @@ -15,10 +15,12 @@ mod spec; pub use handler_register::{ deduct_caller, end, last_frame_return, load_accounts, load_precompiles, - optimism_handle_register, output, refund, reward_beneficiary, validate_env, + optimism_handle_register, output, refund, reimburse_caller, reward_beneficiary, validate_env, validate_tx_against_state, }; -pub use l1block::{L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT}; +pub use l1block::{ + L1BlockInfo, BASE_FEE_RECIPIENT, L1_BLOCK_CONTRACT, L1_FEE_RECIPIENT, OPERATOR_FEE_RECIPIENT, +}; pub use result::{OptimismHaltReason, OptimismInvalidTransaction}; use revm::{ primitives::{Bytes, B256}, diff --git a/crates/optimism/src/spec.rs b/crates/optimism/src/spec.rs index a8be1a6690..76f474ea9c 100644 --- a/crates/optimism/src/spec.rs +++ b/crates/optimism/src/spec.rs @@ -88,6 +88,8 @@ pub enum OptimismSpecId { GRANITE = 23, PRAGUE = 24, PRAGUE_EOF = 25, + HOLOCENE = 26, + ISTHMUS = 27, #[default] LATEST = u8::MAX, } @@ -136,7 +138,9 @@ impl OptimismSpecId { OptimismSpecId::CANCUN | OptimismSpecId::ECOTONE | OptimismSpecId::FJORD - | OptimismSpecId::GRANITE => SpecId::CANCUN, + | OptimismSpecId::GRANITE + | OptimismSpecId::HOLOCENE + | OptimismSpecId::ISTHMUS => SpecId::CANCUN, OptimismSpecId::PRAGUE => SpecId::PRAGUE, OptimismSpecId::PRAGUE_EOF => SpecId::PRAGUE_EOF, OptimismSpecId::LATEST => SpecId::LATEST, @@ -195,6 +199,8 @@ pub mod id { pub const ECOTONE: &str = "Ecotone"; pub const FJORD: &str = "Fjord"; pub const GRANITE: &str = "Granite"; + pub const HOLOCENE: &str = "Holocene"; + pub const ISTHMUS: &str = "Isthmus"; } impl From<&str> for OptimismSpecId { @@ -260,6 +266,8 @@ impl From for &'static str { OptimismSpecId::ECOTONE => id::ECOTONE, OptimismSpecId::FJORD => id::FJORD, OptimismSpecId::GRANITE => id::GRANITE, + OptimismSpecId::HOLOCENE => id::HOLOCENE, + OptimismSpecId::ISTHMUS => id::ISTHMUS, OptimismSpecId::LATEST => id::LATEST, } } @@ -321,6 +329,8 @@ spec!(CANYON, CanyonSpec); spec!(ECOTONE, EcotoneSpec); spec!(FJORD, FjordSpec); spec!(GRANITE, GraniteSpec); +spec!(HOLOCENE, HoloceneSpec); +spec!(ISTHMUS, IsthmusSpec); #[macro_export] macro_rules! optimism_spec_to_generic { @@ -413,6 +423,14 @@ macro_rules! optimism_spec_to_generic { use $crate::FjordSpec as SPEC; $e } + $crate::OptimismSpecId::HOLOCENE => { + use $crate::HoloceneSpec as SPEC; + $e + } + $crate::OptimismSpecId::ISTHMUS => { + use $crate::IsthmusSpec as SPEC; + $e + } } }}; } @@ -627,6 +645,14 @@ mod tests { OptimismSpecId::PRAGUE_EOF, assert_eq!(SPEC::OPTIMISM_SPEC_ID, OptimismSpecId::PRAGUE_EOF) ); + optimism_spec_to_generic!( + OptimismSpecId::HOLOCENE, + assert_eq!(SPEC::OPTIMISM_SPEC_ID, OptimismSpecId::HOLOCENE) + ); + optimism_spec_to_generic!( + OptimismSpecId::ISTHMUS, + assert_eq!(SPEC::OPTIMISM_SPEC_ID, OptimismSpecId::ISTHMUS) + ); optimism_spec_to_generic!( OptimismSpecId::LATEST, assert_eq!(SPEC::OPTIMISM_SPEC_ID, OptimismSpecId::LATEST)