diff --git a/crates/interpreter/src/instructions/opcode.rs b/crates/interpreter/src/instructions/opcode.rs index 116353803c..05865a6df0 100644 --- a/crates/interpreter/src/instructions/opcode.rs +++ b/crates/interpreter/src/instructions/opcode.rs @@ -872,6 +872,11 @@ pub const fn spec_opcode_gas(spec_id: SpecId) -> &'static [OpInfo; 256] { const TABLE: &[OpInfo;256] = &make_gas_table(SpecId::CANYON); TABLE } + #[cfg(feature = "optimism")] + SpecId::ECOTONE => { + const TABLE: &[OpInfo;256] = &make_gas_table(SpecId::ECOTONE); + TABLE + } } }; } diff --git a/crates/precompile/src/lib.rs b/crates/precompile/src/lib.rs index 0c590bcf80..e26a990169 100644 --- a/crates/precompile/src/lib.rs +++ b/crates/precompile/src/lib.rs @@ -258,6 +258,8 @@ impl PrecompileSpecId { LATEST => Self::LATEST, #[cfg(feature = "optimism")] BEDROCK | REGOLITH | CANYON => Self::BERLIN, + #[cfg(feature = "optimism")] + ECOTONE => Self::CANCUN, } } } diff --git a/crates/primitives/src/specification.rs b/crates/primitives/src/specification.rs index e7434be7d7..937acc141f 100644 --- a/crates/primitives/src/specification.rs +++ b/crates/primitives/src/specification.rs @@ -60,6 +60,7 @@ pub enum SpecId { SHANGHAI = 18, CANYON = 19, CANCUN = 20, + ECOTONE = 21, LATEST = u8::MAX, } @@ -102,6 +103,8 @@ impl From<&str> for SpecId { "Regolith" => SpecId::REGOLITH, #[cfg(feature = "optimism")] "Canyon" => SpecId::CANYON, + #[cfg(feature = "optimism")] + "Ecotone" => SpecId::ECOTONE, _ => Self::LATEST, } } @@ -157,6 +160,8 @@ spec!(BEDROCK, BedrockSpec); spec!(REGOLITH, RegolithSpec); #[cfg(feature = "optimism")] spec!(CANYON, CanyonSpec); +#[cfg(feature = "optimism")] +spec!(ECOTONE, EcotoneSpec); #[macro_export] macro_rules! spec_to_generic { @@ -232,6 +237,11 @@ macro_rules! spec_to_generic { use $crate::CanyonSpec as SPEC; $e } + #[cfg(feature = "optimism")] + $crate::SpecId::ECOTONE => { + use $crate::EcotoneSpec as SPEC; + $e + } } }}; } @@ -338,4 +348,28 @@ mod optimism_tests { assert!(SpecId::enabled(SpecId::CANYON, SpecId::REGOLITH)); assert!(SpecId::enabled(SpecId::CANYON, SpecId::CANYON)); } + + #[test] + fn test_ecotone_post_merge_hardforks() { + assert!(EcotoneSpec::enabled(SpecId::MERGE)); + assert!(EcotoneSpec::enabled(SpecId::SHANGHAI)); + assert!(EcotoneSpec::enabled(SpecId::CANCUN)); + assert!(!EcotoneSpec::enabled(SpecId::LATEST)); + assert!(EcotoneSpec::enabled(SpecId::BEDROCK)); + assert!(EcotoneSpec::enabled(SpecId::REGOLITH)); + assert!(EcotoneSpec::enabled(SpecId::CANYON)); + assert!(EcotoneSpec::enabled(SpecId::ECOTONE)); + } + + #[test] + fn test_ecotone_post_merge_hardforks_spec_id() { + assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::MERGE)); + assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::SHANGHAI)); + assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::CANCUN)); + assert!(!SpecId::enabled(SpecId::ECOTONE, SpecId::LATEST)); + assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::BEDROCK)); + assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::REGOLITH)); + assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::CANYON)); + assert!(SpecId::enabled(SpecId::ECOTONE, SpecId::ECOTONE)); + } } diff --git a/crates/revm/src/handler/mainnet/pre_execution.rs b/crates/revm/src/handler/mainnet/pre_execution.rs index 84bd2952f8..2c4199a055 100644 --- a/crates/revm/src/handler/mainnet/pre_execution.rs +++ b/crates/revm/src/handler/mainnet/pre_execution.rs @@ -30,8 +30,9 @@ pub fn load( // the L1-cost fee is only computed for Optimism non-deposit transactions. #[cfg(feature = "optimism")] if context.evm.env.cfg.optimism && context.evm.env.tx.optimism.source_hash.is_none() { - let l1_block_info = crate::optimism::L1BlockInfo::try_fetch(&mut context.evm.db) - .map_err(EVMError::Database)?; + let l1_block_info = + crate::optimism::L1BlockInfo::try_fetch(&mut context.evm.db, SPEC::SPEC_ID) + .map_err(EVMError::Database)?; // storage l1 block info for later use. context.evm.l1_block_info = Some(l1_block_info); diff --git a/crates/revm/src/optimism/handler_register.rs b/crates/revm/src/optimism/handler_register.rs index 9fc4cd3be0..1f08e2dcc0 100644 --- a/crates/revm/src/optimism/handler_register.rs +++ b/crates/revm/src/optimism/handler_register.rs @@ -438,8 +438,9 @@ mod tests { let mut context: Context<(), InMemoryDB> = Context::new_with_db(db); context.evm.l1_block_info = Some(L1BlockInfo { l1_base_fee: U256::from(1_000), - l1_fee_overhead: U256::from(1_000), - l1_fee_scalar: U256::from(1_000), + l1_fee_overhead: Some(U256::from(1_000)), + l1_base_fee_scalar: U256::from(1_000), + ..Default::default() }); // Enveloped needs to be some but it will deduce zero fee. context.evm.env.tx.optimism.enveloped_tx = Some(bytes!("")); @@ -471,8 +472,9 @@ mod tests { let mut context: Context<(), InMemoryDB> = Context::new_with_db(db); context.evm.l1_block_info = Some(L1BlockInfo { l1_base_fee: U256::from(1_000), - l1_fee_overhead: U256::from(1_000), - l1_fee_scalar: U256::from(1_000), + l1_fee_overhead: Some(U256::from(1_000)), + l1_base_fee_scalar: U256::from(1_000), + ..Default::default() }); // l1block cost is 1048 fee. context.evm.env.tx.optimism.enveloped_tx = Some(bytes!("FACADE")); @@ -507,8 +509,9 @@ mod tests { let mut context: Context<(), InMemoryDB> = Context::new_with_db(db); context.evm.l1_block_info = Some(L1BlockInfo { l1_base_fee: U256::from(1_000), - l1_fee_overhead: U256::from(1_000), - l1_fee_scalar: U256::from(1_000), + l1_fee_overhead: Some(U256::from(1_000)), + l1_base_fee_scalar: U256::from(1_000), + ..Default::default() }); // l1block cost is 1048 fee. context.evm.env.tx.optimism.enveloped_tx = Some(bytes!("FACADE")); @@ -537,8 +540,9 @@ mod tests { let mut context: Context<(), InMemoryDB> = Context::new_with_db(db); context.evm.l1_block_info = Some(L1BlockInfo { l1_base_fee: U256::from(1_000), - l1_fee_overhead: U256::from(1_000), - l1_fee_scalar: U256::from(1_000), + l1_fee_overhead: Some(U256::from(1_000)), + l1_base_fee_scalar: U256::from(1_000), + ..Default::default() }); // l1block cost is 1048 fee. context.evm.env.tx.optimism.enveloped_tx = Some(bytes!("FACADE")); diff --git a/crates/revm/src/optimism/l1block.rs b/crates/revm/src/optimism/l1block.rs index 85f8c7a620..2c7e795522 100644 --- a/crates/revm/src/optimism/l1block.rs +++ b/crates/revm/src/optimism/l1block.rs @@ -4,10 +4,27 @@ use core::ops::Mul; const ZERO_BYTE_COST: u64 = 4; const NON_ZERO_BYTE_COST: u64 = 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 baseFeeScalar attribute. +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; + 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]); +/// [ECOTONE_L1_BLOB_BASE_FEE_SLOT] was added in the Ecotone upgrade and stores the L1 blobBaseFee attribute. +const ECOTONE_L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([7u64, 0, 0, 0]); + +/// As of the ecotone upgrade, this storage slot stores the 32-bit basefeeScalar and blobBaseFeeScalar attributes at +/// 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]); + +/// An empty 64-bit set of scalar values. +const EMPTY_SCALARS: [u8; 8] = [0u8; 8]; + /// The address of L1 fee recipient. pub const L1_FEE_RECIPIENT: Address = address!("420000000000000000000000000000000000001A"); @@ -28,28 +45,69 @@ pub const L1_BLOCK_CONTRACT: Address = address!("4200000000000000000000000000000 /// uint64 _sequenceNumber, bytes32 _batcherHash, uint256 _l1FeeOverhead, uint256 _l1FeeScalar) /// /// For now, we only care about the fields necessary for L1 cost calculation. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] 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 overhead. None if Ecotone is activated. + pub l1_fee_overhead: Option, /// The current L1 fee scalar. - pub l1_fee_scalar: U256, + pub l1_base_fee_scalar: U256, + /// The current L1 blob base fee. None if Ecotone is not activated, except if `empty_scalars` is `true`. + 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, + /// True if Ecotone is activated, but the L1 fee scalars have not yet been set. + pub(crate) empty_scalars: bool, } impl L1BlockInfo { /// Try to fetch the L1 block info from the database. - pub fn try_fetch(db: &mut DB) -> Result { + pub fn try_fetch(db: &mut DB, spec_id: SpecId) -> Result { 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, - }) + + if !spec_id.is_enabled_in(SpecId::ECOTONE) { + 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: Some(l1_fee_overhead), + l1_base_fee_scalar: l1_fee_scalar, + ..Default::default() + }) + } else { + let l1_blob_base_fee = db.storage(L1_BLOCK_CONTRACT, ECOTONE_L1_BLOB_BASE_FEE_SLOT)?; + let l1_fee_scalars = db + .storage(L1_BLOCK_CONTRACT, ECOTONE_L1_FEE_SCALARS_SLOT)? + .to_be_bytes::<32>(); + + let l1_base_fee_scalar = U256::from_be_slice( + l1_fee_scalars[BASE_FEE_SCALAR_OFFSET..BASE_FEE_SCALAR_OFFSET + 4].as_ref(), + ); + let l1_blob_base_fee_scalar = U256::from_be_slice( + l1_fee_scalars[BLOB_BASE_FEE_SCALAR_OFFSET..BLOB_BASE_FEE_SCALAR_OFFSET + 4] + .as_ref(), + ); + + // 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 == U256::ZERO + && l1_fee_scalars[BASE_FEE_SCALAR_OFFSET..BLOB_BASE_FEE_SCALAR_OFFSET + 4] + == EMPTY_SCALARS; + let l1_fee_overhead = empty_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, + }) + } } /// Calculate the data gas for posting the transaction on L1. Calldata costs 16 gas per non-zero @@ -74,19 +132,62 @@ impl L1BlockInfo { rollup_data_gas_cost } - /// Calculate the gas cost of a transaction based on L1 block data posted on L2 + /// Calculate the gas cost of a transaction based on L1 block data posted on L2, depending on the [SpecId] passed. pub fn calculate_tx_l1_cost(&self, input: &[u8], spec_id: SpecId) -> U256 { - // If the input is not a deposit transaction, the default value is zero. + // If the input is a deposit transaction or empty, the default value is zero. if input.is_empty() || input.first() == Some(&0x7F) { return U256::ZERO; } + if spec_id.is_enabled_in(SpecId::ECOTONE) { + self.calculate_tx_l1_cost_ecotone(input, spec_id) + } else { + self.calculate_tx_l1_cost_bedrock(input, spec_id) + } + } + + /// Calculate the gas cost of a transaction based on L1 block data posted on L2, pre-Ecotone. + fn calculate_tx_l1_cost_bedrock(&self, input: &[u8], spec_id: SpecId) -> U256 { let rollup_data_gas_cost = self.data_gas(input, spec_id); rollup_data_gas_cost - .saturating_add(self.l1_fee_overhead) + .saturating_add(self.l1_fee_overhead.unwrap_or_default()) .saturating_mul(self.l1_base_fee) - .saturating_mul(self.l1_fee_scalar) - / U256::from(1_000_000) + .saturating_mul(self.l1_base_fee_scalar) + .wrapping_div(U256::from(1_000_000)) + } + + /// Calculate the gas cost of a transaction based on L1 block data posted on L2, post-Ecotone. + /// + /// [SpecId::ECOTONE] L1 cost function: + /// `(calldataGas/16)*(l1BaseFee*16*l1BaseFeeScalar + l1BlobBaseFee*l1BlobBaseFeeScalar)/1e6` + /// + /// We divide "calldataGas" by 16 to change from units of calldata gas to "estimated # of bytes when compressed". + /// Known as "compressedTxSize" in the spec. + /// + /// Function is actually computed as follows for better precision under integer arithmetic: + /// `calldataGas*(l1BaseFee*16*l1BaseFeeScalar + l1BlobBaseFee*l1BlobBaseFeeScalar)/16e6` + fn calculate_tx_l1_cost_ecotone(&self, input: &[u8], spec_id: SpecId) -> U256 { + // 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 { + return self.calculate_tx_l1_cost_bedrock(input, spec_id); + } + + let rollup_data_gas_cost = self.data_gas(input, spec_id); + let calldata_cost_per_byte = self + .l1_base_fee + .saturating_mul(U256::from(16)) + .saturating_mul(self.l1_base_fee_scalar); + let blob_cost_per_byte = self + .l1_blob_base_fee + .unwrap_or_default() + .saturating_mul(self.l1_blob_base_fee_scalar.unwrap_or_default()); + + calldata_cost_per_byte + .saturating_add(blob_cost_per_byte) + .saturating_mul(rollup_data_gas_cost) + .wrapping_div(U256::from(1_000_000 * 16)) } } @@ -99,8 +200,9 @@ mod tests { 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), + l1_fee_overhead: Some(U256::from(1_000_000)), + l1_base_fee_scalar: U256::from(1_000_000), + ..Default::default() }; // 0xFACADE = 6 nibbles = 3 bytes @@ -123,8 +225,9 @@ mod tests { 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), + l1_fee_overhead: Some(U256::from(1_000_000)), + l1_base_fee_scalar: U256::from(1_000_000), + ..Default::default() }; // 0xFA00CA00DE = 10 nibbles = 5 bytes @@ -147,8 +250,9 @@ mod tests { 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), + l1_fee_overhead: Some(U256::from(1_000)), + l1_base_fee_scalar: U256::from(1_000), + ..Default::default() }; let input = bytes!("FACADE"); @@ -165,4 +269,39 @@ mod tests { let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::REGOLITH); assert_eq!(gas_cost, U256::ZERO); } + + #[test] + fn test_calculate_tx_l1_cost_ecotone() { + let mut l1_block_info = L1BlockInfo { + l1_base_fee: U256::from(1_000), + l1_base_fee_scalar: U256::from(1_000), + l1_blob_base_fee: Some(U256::from(1_000)), + l1_blob_base_fee_scalar: Some(U256::from(1_000)), + l1_fee_overhead: Some(U256::from(1_000)), + ..Default::default() + }; + + // calldataGas * (l1BaseFee * 16 * l1BaseFeeScalar + l1BlobBaseFee * l1BlobBaseFeeScalar) / (16 * 1e6) + // = (16 * 3) * (1000 * 16 * 1000 + 1000 * 1000) / (16 * 1e6) + // = 51 + let input = bytes!("FACADE"); + let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); + assert_eq!(gas_cost, U256::from(51)); + + // Zero rollup data gas cost should result in zero + let input = bytes!(""); + let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); + assert_eq!(gas_cost, U256::ZERO); + + // Deposit transactions with the EIP-2718 type of 0x7F should result in zero + let input = bytes!("7FFACADE"); + let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); + assert_eq!(gas_cost, U256::ZERO); + + // If the scalars are empty, the bedrock cost function should be used. + l1_block_info.empty_scalars = true; + let input = bytes!("FACADE"); + let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); + assert_eq!(gas_cost, U256::from(1048)); + } }