diff --git a/src/handler.rs b/src/handler.rs index 71aadb0..d6f5768 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -105,8 +105,10 @@ where "[SCROLL] Failed to load transaction rlp_bytes.".to_string(), )); }; + // Deduct l1 fee from caller. - let tx_l1_cost = l1_block_info.calculate_tx_l1_cost(rlp_bytes, spec); + let tx_l1_cost = + l1_block_info.calculate_tx_l1_cost(rlp_bytes, spec, ctx.tx().compression_ratio()); let caller_account = ctx.journal().load_account(caller)?; if tx_l1_cost.gt(&caller_account.info.balance) { return Err(InvalidTransaction::LackOfFundForMaxFee { @@ -224,7 +226,11 @@ where "[SCROLL] Failed to load transaction rlp_bytes.".to_string(), )); }; - l1_block_info.calculate_tx_l1_cost(rlp_bytes, ctx.cfg().spec()) + l1_block_info.calculate_tx_l1_cost( + rlp_bytes, + ctx.cfg().spec(), + ctx.tx().compression_ratio(), + ) } else { U256::from(0) }; @@ -269,7 +275,8 @@ mod tests { use crate::{ builder::ScrollBuilder, test_utils::{ - context, context_with_funds, BENEFICIARY, CALLER, L1_DATA_COST, MIN_TRANSACTION_COST, + context, ScrollContextTestUtils, BENEFICIARY, CALLER, L1_DATA_COST, + MIN_TRANSACTION_COST, }, }; use std::boxed::Box; @@ -299,7 +306,7 @@ mod tests { #[test] fn test_load_account() -> Result<(), Box> { - let ctx = context_with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); + let ctx = context().with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); let mut evm = ctx.build_scroll(); let handler = ScrollHandler::<_, EVMError<_>, EthFrame<_, _, _>>::new(); handler.pre_execution(&mut evm)?; @@ -312,7 +319,7 @@ mod tests { #[test] fn test_deduct_caller() -> Result<(), Box> { - let ctx = context_with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); + let ctx = context().with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); let mut evm = ctx.build_scroll(); let handler = ScrollHandler::<_, EVMError<_>, EthFrame<_, _, _>>::new(); @@ -376,7 +383,7 @@ mod tests { #[test] fn test_reward_beneficiary() -> Result<(), Box> { - let ctx = context_with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); + let ctx = context().with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); let mut evm = ctx.build_scroll(); let handler = ScrollHandler::<_, EVMError<_>, EthFrame<_, _, _>>::new(); @@ -400,7 +407,7 @@ mod tests { #[test] fn test_transaction_pre_execution() -> Result<(), Box> { - let ctx = context_with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); + let ctx = context().with_funds(MIN_TRANSACTION_COST + L1_DATA_COST); let mut evm = ctx.build_scroll(); let handler = ScrollHandler::<_, EVMError<_>, EthFrame<_, _, _>>::new(); diff --git a/src/l1block.rs b/src/l1block.rs index 973e865..70ce446 100644 --- a/src/l1block.rs +++ b/src/l1block.rs @@ -18,29 +18,38 @@ const NON_ZERO_BYTE_COST: u64 = 16; const TX_L1_COMMIT_EXTRA_COST: U256 = U256::from_limbs([64u64, 0, 0, 0]); /// The precision used for L1 fee calculations. -const TX_L1_FEE_PRECISION: U256 = U256::from_limbs([1_000_000_000u64, 0, 0, 0]); +pub const TX_L1_FEE_PRECISION: u64 = 1_000_000_000u64; +pub const TX_L1_FEE_PRECISION_U256: U256 = U256::from_limbs([TX_L1_FEE_PRECISION, 0, 0, 0]); /// The L1 gas price oracle address. pub const L1_GAS_PRICE_ORACLE_ADDRESS: Address = address!("5300000000000000000000000000000000000002"); /// The L1 base fee storage slot. -const L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1u64, 0, 0, 0]); +pub const L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1u64, 0, 0, 0]); /// The L1 fee overhead storage slot. -const L1_OVERHEAD_SLOT: U256 = U256::from_limbs([2u64, 0, 0, 0]); +pub const L1_OVERHEAD_SLOT: U256 = U256::from_limbs([2u64, 0, 0, 0]); /// The L1 fee scalar storage slot. -const L1_SCALAR_SLOT: U256 = U256::from_limbs([3u64, 0, 0, 0]); +pub const L1_SCALAR_SLOT: U256 = U256::from_limbs([3u64, 0, 0, 0]); /// The L1 blob base fee storage slot. -const L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([5u64, 0, 0, 0]); +pub const L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([5u64, 0, 0, 0]); /// The L1 commit scalar storage slot. -const L1_COMMIT_SCALAR_SLOT: U256 = U256::from_limbs([6u64, 0, 0, 0]); +/// +/// Post-FEYNMAN this represents the exec_scalar. +pub const L1_COMMIT_SCALAR_SLOT: U256 = U256::from_limbs([6u64, 0, 0, 0]); /// The L1 blob scalar storage slot. -const L1_BLOB_SCALAR_SLOT: U256 = U256::from_limbs([7u64, 0, 0, 0]); +pub const L1_BLOB_SCALAR_SLOT: U256 = U256::from_limbs([7u64, 0, 0, 0]); + +/// The compression penalty threshold storage slot. +pub const PENALTY_THRESHOLD_SLOT: U256 = U256::from_limbs([9u64, 0, 0, 0]); + +/// The compression penalty factor storage slot. +pub const PENALTY_FACTOR_SLOT: U256 = U256::from_limbs([10u64, 0, 0, 0]); const U64_MAX: U256 = U256::from_limbs([u64::MAX, 0, 0, 0]); @@ -67,6 +76,10 @@ pub struct L1BlockInfo { pub l1_blob_scalar: Option, /// The current call data gas (l1_blob_scalar * l1_base_fee), None if before Curie. pub calldata_gas: Option, + /// The current compression penalty threshold, None if before Feynman. + pub penalty_threshold: Option, + /// The current compression penalty factor, None if before Feynman. + pub penalty_factor: Option, } impl L1BlockInfo { @@ -94,6 +107,23 @@ impl L1BlockInfo { let l1_blob_scalar = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_BLOB_SCALAR_SLOT)?; let calldata_gas = l1_commit_scalar.saturating_mul(l1_base_fee); + // If Feynman is not enabled, return the L1 block info without Feynman fields. + if !spec_id.is_enabled_in(ScrollSpecId::FEYNMAN) { + return Ok(L1BlockInfo { + l1_base_fee, + l1_fee_overhead, + l1_base_fee_scalar, + l1_blob_base_fee: Some(l1_blob_base_fee), + l1_commit_scalar: Some(l1_commit_scalar), + l1_blob_scalar: Some(l1_blob_scalar), + calldata_gas: Some(calldata_gas), + ..Default::default() + }); + } + + let penalty_threshold = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, PENALTY_THRESHOLD_SLOT)?; + let penalty_factor = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, PENALTY_FACTOR_SLOT)?; + Ok(L1BlockInfo { l1_base_fee, l1_fee_overhead, @@ -102,6 +132,8 @@ impl L1BlockInfo { l1_commit_scalar: Some(l1_commit_scalar), l1_blob_scalar: Some(l1_blob_scalar), calldata_gas: Some(calldata_gas), + penalty_threshold: Some(penalty_threshold), + penalty_factor: Some(penalty_factor), }) } @@ -128,22 +160,110 @@ impl L1BlockInfo { tx_l1_gas .saturating_mul(self.l1_base_fee) .saturating_mul(self.l1_base_fee_scalar) - .wrapping_div(TX_L1_FEE_PRECISION) + .wrapping_div(TX_L1_FEE_PRECISION_U256) } fn calculate_tx_l1_cost_curie(&self, input: &[u8], spec_id: ScrollSpecId) -> U256 { // "commitScalar * l1BaseFee + blobScalar * _data.length * l1BlobBaseFee" let blob_gas = self.data_gas(input, spec_id); - self.calldata_gas.unwrap().saturating_add(blob_gas).wrapping_div(TX_L1_FEE_PRECISION) + self.calldata_gas.unwrap().saturating_add(blob_gas).wrapping_div(TX_L1_FEE_PRECISION_U256) + } + + fn calculate_tx_l1_cost_feynman( + &self, + input: &[u8], + spec_id: ScrollSpecId, + compression_ratio: U256, + ) -> U256 { + // rollup_fee(tx) = size(tx) * (component_exec + component_blob) * penalty(tx) + // + // - size(tx): denotes the size of the signed tx. + // + // - component_exec: The component that accounts towards committing this tx as part of a L2 + // batch as well as gas costs for the eventual on-chain proof verification. + // + // => component_exec = exec_scalar * l1_base_fee + // where exec_scalar = compression_scalar * (commit_scalar + verification_scalar) + // + // - component_blob: The component that accounts the costs associated with data + // availability, i.e. the costs of posting this tx's data in the EIP-4844 blob. + // + // => component_blob = compressed_blob_scalar * l1_blob_base_fee + // where compressed_blob_scalar = compression_scalar * blob_scalar + // + // Note that the same slots for L1_COMMIT_SCALAR_SLOT and L1_BLOB_SCALAR_SLOT are + // re-used for the new exec_scalar and compressed_blob_scalar values post-FEYNMAN. + // + // - penalty(tx): A compression penalty based on the transaction's compression ratio. + // + // => penalty(tx) = compression_ratio(tx) >= penalty_threshold ? 1 : penalty_factor + // where compression_ratio(tx) = size(tx) / size(zstd(tx)) + // + // Note that commit_scalar (exec_scalar), blob_scalar (compressed_blob_scalar), + // compression_ratio, penalty_threshold, penalty_factor, penalty are all scaled + // by TX_L1_FEE_PRECISION_U256 (1e9) to avoid losing precision. + + assert!( + compression_ratio >= TX_L1_FEE_PRECISION_U256, + "transaction compression ratio must be greater or equal to {TX_L1_FEE_PRECISION_U256:?} - compression ratio: {compression_ratio:?}" + ); + + let exec_scalar = self + .l1_commit_scalar + .unwrap_or_else(|| panic!("missing exec scalar in spec_id={:?}", spec_id)); + + let compressed_blob_scalar = self + .l1_blob_scalar + .unwrap_or_else(|| panic!("missing l1 blob scalar in spec_id={:?}", spec_id)); + + let l1_blob_base_fee = self + .l1_blob_base_fee + .unwrap_or_else(|| panic!("missing l1 blob base fee in spec_id={:?}", spec_id)); + + let penalty_threshold = self + .penalty_threshold + .unwrap_or_else(|| panic!("missing penalty threshold in spec_id={:?}", spec_id)); + + let penalty_factor = self + .penalty_factor + .unwrap_or_else(|| panic!("missing penalty factor in spec_id={:?}", spec_id)); + + let tx_size = U256::from(input.len()); + + let component_exec = exec_scalar.saturating_mul(self.l1_base_fee); + let component_blob = compressed_blob_scalar.saturating_mul(l1_blob_base_fee); + let fee_per_byte = component_exec.saturating_add(component_blob); + + let penalty = if compression_ratio >= penalty_threshold { + TX_L1_FEE_PRECISION_U256 + } else { + penalty_factor + }; + + tx_size + .saturating_mul(fee_per_byte) + .saturating_mul(penalty) + .wrapping_div(TX_L1_FEE_PRECISION_U256) // account for scalars + .wrapping_div(TX_L1_FEE_PRECISION_U256) // account for penalty } /// Calculate the gas cost of a transaction based on L1 block data posted on L2. - pub fn calculate_tx_l1_cost(&self, input: &[u8], spec_id: ScrollSpecId) -> U256 { + pub fn calculate_tx_l1_cost( + &self, + input: &[u8], + spec_id: ScrollSpecId, + compression_ratio: Option, + ) -> U256 { let l1_cost = if !spec_id.is_enabled_in(ScrollSpecId::CURIE) { self.calculate_tx_l1_cost_shanghai(input, spec_id) - } else { + } else if !spec_id.is_enabled_in(ScrollSpecId::FEYNMAN) { self.calculate_tx_l1_cost_curie(input, spec_id) + } else { + let compression_ratio = compression_ratio.unwrap_or_else(|| { + panic!("compression ratio should be set in spec_id={:?}", spec_id) + }); + self.calculate_tx_l1_cost_feynman(input, spec_id, compression_ratio) }; l1_cost.min(U64_MAX) } diff --git a/src/test_utils.rs b/src/test_utils.rs index 6854633..45cae46 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -7,7 +7,8 @@ use revm::{ state::AccountInfo, Context, }; -use revm_primitives::{address, bytes, Address, U256}; +use revm_primitives::{address, bytes, Address, Bytes, U256}; +use std::vec::Vec; pub const TX_L1_FEE_PRECISION: U256 = U256::from_limbs([1_000_000_000u64, 0, 0, 0]); pub const CALLER: Address = address!("0x000000000000000000000000000000000000dead"); @@ -41,16 +42,34 @@ pub fn context() -> ScrollContext { }) } -/// Returns a test [`ScrollContext`] which contains a basic transaction, a default block beneficiary -/// and a state with L1 gas oracle slots set and the provided funds for the caller. -pub fn context_with_funds(funds: U256) -> ScrollContext { - context().modify_db_chained(|db| { - db.cache.accounts.insert( - CALLER, - DbAccount { - info: AccountInfo { balance: funds, ..Default::default() }, - ..Default::default() - }, - ); - }) +pub trait ScrollContextTestUtils { + fn with_funds(self, funds: U256) -> Self; + fn with_gas_oracle_config(self, entries: Vec<(U256, U256)>) -> Self; + fn with_tx_payload(self, data: Bytes) -> Self; +} + +impl ScrollContextTestUtils for ScrollContext { + fn with_funds(self, funds: U256) -> Self { + self.modify_db_chained(|db| { + db.cache.accounts.insert( + CALLER, + DbAccount { + info: AccountInfo { balance: funds, ..Default::default() }, + ..Default::default() + }, + ); + }) + } + + fn with_gas_oracle_config(self, entries: Vec<(U256, U256)>) -> Self { + self.modify_db_chained(|db| { + for entry in entries { + let _ = db.insert_account_storage(L1_GAS_PRICE_ORACLE_ADDRESS, entry.0, entry.1); + } + }) + } + + fn with_tx_payload(self, data: Bytes) -> Self { + self.modify_tx_chained(|tx| tx.rlp_bytes = Some(data)) + } } diff --git a/src/tests/fees.rs b/src/tests/fees.rs index 2ac5829..da1c6d3 100644 --- a/src/tests/fees.rs +++ b/src/tests/fees.rs @@ -1,7 +1,8 @@ use crate::{ builder::ScrollBuilder, handler::ScrollHandler, - test_utils::{context, context_with_funds, BENEFICIARY, CALLER}, + l1block::*, + test_utils::{context, ScrollContextTestUtils, BENEFICIARY, CALLER}, transaction::SYSTEM_ADDRESS, ScrollSpecId, }; @@ -11,11 +12,12 @@ use revm::{ interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult}, }; use revm_primitives::U256; -use std::boxed::Box; +use std::{boxed::Box, vec}; #[test] fn test_should_deduct_correct_fees_bernoulli() -> Result<(), Box> { - let ctx = context_with_funds(U256::from(30_000)) + let ctx = context() + .with_funds(U256::from(30_000)) .modify_cfg_chained(|cfg| cfg.spec = ScrollSpecId::BERNOULLI); let mut evm = ctx.clone().build_scroll(); let handler = ScrollHandler::<_, EVMError<_>, EthFrame<_, _, _>>::new(); @@ -32,7 +34,8 @@ fn test_should_deduct_correct_fees_bernoulli() -> Result<(), Box Result<(), Box> { - let ctx = context_with_funds(U256::from(70_000)) + let ctx = context() + .with_funds(U256::from(70_000)) .modify_cfg_chained(|cfg| cfg.spec = ScrollSpecId::CURIE); let mut evm = ctx.clone().build_scroll(); let handler = ScrollHandler::<_, EVMError<_>, EthFrame<_, _, _>>::new(); @@ -49,7 +52,8 @@ fn test_should_deduct_correct_fees_curie() -> Result<(), Box Result<(), Box> { - let ctx = context_with_funds(U256::from(70_000)) + let ctx = context() + .with_funds(U256::from(70_000)) .modify_cfg_chained(|cfg| cfg.spec = ScrollSpecId::CURIE) .modify_tx_chained(|tx| { tx.base.caller = SYSTEM_ADDRESS; @@ -91,3 +95,39 @@ fn test_reward_beneficiary_system_tx() -> Result<(), Box Ok(()) } + +#[test] +fn test_should_deduct_correct_fees_feynman() -> Result<(), Box> { + let initial_funds = U256::from(70_000); + let compression_ratio = U256::from(5_000_000_000u64); + let tx_payload = vec![0u8; 100]; + + let gas_oracle = vec![ + (L1_BASE_FEE_SLOT, U256::from(1_000_000_000u64)), + (L1_BLOB_BASE_FEE_SLOT, U256::from(1_000_000_000u64)), + (L1_COMMIT_SCALAR_SLOT, U256::from(10)), + (L1_BLOB_SCALAR_SLOT, U256::from(20)), + (PENALTY_THRESHOLD_SLOT, U256::from(6_000_000_000u64)), + (PENALTY_FACTOR_SLOT, U256::from(2_000_000_000u64)), + ]; + + let ctx = context() + .with_funds(initial_funds) + .modify_cfg_chained(|cfg| cfg.spec = ScrollSpecId::FEYNMAN) + .modify_tx_chained(|tx| tx.compression_ratio = Some(compression_ratio)) + .with_gas_oracle_config(gas_oracle) + .with_tx_payload(tx_payload.into()); + + let mut evm = ctx.clone().build_scroll(); + let handler = ScrollHandler::<_, EVMError<_>, EthFrame<_, _, _>>::new(); + + handler.pre_execution(&mut evm).unwrap(); + + let caller_account = evm.ctx().journal().load_account(CALLER)?; + + // cost is 21k + 6k (applying 2x penalty). + let balance_diff = initial_funds.saturating_sub(caller_account.data.info.balance); + assert_eq!(balance_diff, U256::from(27000)); + + Ok(()) +} diff --git a/src/transaction.rs b/src/transaction.rs index 49fd8ad..62e9906 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -18,6 +18,11 @@ pub trait ScrollTxTr: Transaction { /// The RLP encoded transaction bytes which are used to calculate the cost associated with /// posting the transaction on L1. fn rlp_bytes(&self) -> Option<&Bytes>; + + /// The compression ratio of the transaction which is used to calculate the cost associated + /// with posting the transaction on L1. + /// Note: compression_ratio(tx) = size(tx) * 1e9 / size(zstd(tx)) + fn compression_ratio(&self) -> Option; } /// A Scroll transaction. Wraps around a base transaction and provides the optional RLPed bytes for @@ -27,17 +32,18 @@ pub trait ScrollTxTr: Transaction { pub struct ScrollTransaction { pub base: T, pub rlp_bytes: Option, + pub compression_ratio: Option, } impl ScrollTransaction { - pub fn new(base: T, rlp_bytes: Option) -> Self { - Self { base, rlp_bytes } + pub fn new(base: T, rlp_bytes: Option, compression_ratio: Option) -> Self { + Self { base, rlp_bytes, compression_ratio } } } impl Default for ScrollTransaction { fn default() -> Self { - Self { base: TxEnv::default(), rlp_bytes: None } + Self { base: TxEnv::default(), rlp_bytes: None, compression_ratio: None } } } @@ -124,4 +130,8 @@ impl ScrollTxTr for ScrollTransaction { fn rlp_bytes(&self) -> Option<&Bytes> { self.rlp_bytes.as_ref() } + + fn compression_ratio(&self) -> Option { + self.compression_ratio + } }