diff --git a/crates/evm/src/evm/handler.rs b/crates/evm/src/evm/handler.rs index df1a6cdad..29aa499b3 100644 --- a/crates/evm/src/evm/handler.rs +++ b/crates/evm/src/evm/handler.rs @@ -52,6 +52,10 @@ const CODE_KEY_SIZE: usize = 39; /// The full calculation can be found here: https://github.com/chainwayxyz/citrea/blob/erce/l1-fee-overhead-calculations/l1_fee_overhead.md pub const L1_FEE_OVERHEAD: usize = 3; +/// The brotli average compression ratio (compressed size / uncompressed size) was calculated as 0.33 by measuring the size of state diffs of batches before and after brotli compression. +/// calculated diff size * BROTLI_COMPRESSION_PERCENTAGE/100 gives the estimated size of the state diff that is written to the da. +pub const BROTLI_COMPRESSION_PERCENTAGE: usize = 33; + /// We want to charge the user for the amount of data written as fairly as possible, the problem is at the time of when we write batch proof to the da we cannot know the exact state diff /// So we calculate the state diff created by a single transaction and use that to charge user /// However at the time of the batch proof some state diffs will be merged and some users will be overcharged. @@ -401,8 +405,17 @@ impl CitreaHandler, result: FrameResult, ) -> Result::Error>> { - let diff_size = - calc_diff_size::(context).map_err(EVMError::Database)? as u64; + let uncompressed_size = + calc_diff_size::(context).map_err(EVMError::Database)?; + + let compression_percentage = if SPEC::enabled(SpecId::CANCUN) { + // Estimate the size of the state diff after the brotli compression + BROTLI_COMPRESSION_PERCENTAGE + } else { + 100 + }; + let diff_size = (uncompressed_size * compression_percentage / 100) as u64; + let l1_fee_rate = context.external.l1_fee_rate(); let l1_fee = U256::from(l1_fee_rate) * (U256::from(diff_size) + U256::from(L1_FEE_OVERHEAD)); diff --git a/crates/evm/src/tests/call_tests.rs b/crates/evm/src/tests/call_tests.rs index ef3106c1b..090e7a588 100644 --- a/crates/evm/src/tests/call_tests.rs +++ b/crates/evm/src/tests/call_tests.rs @@ -18,7 +18,7 @@ use sov_rollup_interface::spec::SpecId as SovSpecId; use crate::call::CallMessage; use crate::evm::primitive_types::Receipt; use crate::evm::DbAccount; -use crate::handler::L1_FEE_OVERHEAD; +use crate::handler::{BROTLI_COMPRESSION_PERCENTAGE, L1_FEE_OVERHEAD}; use crate::smart_contracts::{ BlockHashContract, InfiniteLoopContract, LogsContract, SelfDestructorContract, SimpleStorageContract, TestContract, @@ -1445,6 +1445,166 @@ fn test_l1_fee_halt() { ); } +#[test] +fn test_l1_fee_compression_discount() { + let (config, dev_signer, _) = + get_evm_config_starting_base_fee(U256::from_str("100000000000000").unwrap(), None, 1); + + let (mut evm, mut working_set) = get_evm(&config); + let l1_fee_rate = 1; + + let soft_confirmation_info = HookSoftConfirmationInfo { + l2_height: 2, + da_slot_hash: [5u8; 32], + da_slot_height: 1, + da_slot_txs_commitment: [42u8; 32], + pre_state_root: [10u8; 32].to_vec(), + current_spec: SovSpecId::Genesis, + pub_key: vec![], + deposit_data: vec![], + l1_fee_rate, + timestamp: 0, + }; + + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + { + let sender_address = generate_address::("sender"); + let context = C::new(sender_address, 2, SovSpecId::Genesis, l1_fee_rate); + let call_tx = dev_signer + .sign_default_transaction_with_priority_fee( + TxKind::Call(Address::random()), + vec![], + 0, + 1000, + 20000000, + 1, + ) + .unwrap(); + + evm.call( + CallMessage { txs: vec![call_tx] }, + &context, + &mut working_set, + ) + .unwrap(); + } + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[99u8; 32].into(), &mut working_set.accessory_state()); + + let db_account = evm + .accounts + .get(&dev_signer.address(), &mut working_set) + .unwrap(); + + let base_fee_vault = evm.accounts.get(&BASE_FEE_VAULT, &mut working_set).unwrap(); + let l1_fee_vault = evm.accounts.get(&L1_FEE_VAULT, &mut working_set).unwrap(); + + let coinbase_account = evm + .accounts + .get(&config.coinbase, &mut working_set) + .unwrap(); + assert_eq!(config.coinbase, PRIORITY_FEE_VAULT); + + let gas_fee_paid = 21000; + let tx1_diff_size = 140; + + let mut expected_db_balance = U256::from( + 100000000000000u64 + - 1000 + - gas_fee_paid * 10000001 + - tx1_diff_size + - L1_FEE_OVERHEAD as u64, + ); + let mut expected_base_fee_vault_balance = U256::from(gas_fee_paid * 10000000); + let mut expected_coinbase_balance = U256::from(gas_fee_paid); + let mut expected_l1_fee_vault_balance = U256::from(tx1_diff_size + L1_FEE_OVERHEAD as u64); + + assert_eq!(db_account.balance, expected_db_balance); + assert_eq!(base_fee_vault.balance, expected_base_fee_vault_balance); + assert_eq!(coinbase_account.balance, expected_coinbase_balance); + assert_eq!(l1_fee_vault.balance, expected_l1_fee_vault_balance); + + // Set up the next transaction with the fork 1 activated + let soft_confirmation_info = HookSoftConfirmationInfo { + l2_height: 3, + da_slot_hash: [5u8; 32], + da_slot_height: 1, + da_slot_txs_commitment: [42u8; 32], + pre_state_root: [99u8; 32].to_vec(), + current_spec: SovSpecId::Fork1, // Compression discount is enabled + pub_key: vec![], + deposit_data: vec![], + l1_fee_rate, + timestamp: 0, + }; + + evm.begin_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + { + let sender_address = generate_address::("sender"); + let context = C::new(sender_address, 3, SovSpecId::Fork1, l1_fee_rate); + let simple_tx = dev_signer + .sign_default_transaction_with_priority_fee( + TxKind::Call(Address::random()), + vec![], + 1, + 1000, + 20000000, + 1, + ) + .unwrap(); + evm.call( + CallMessage { + txs: vec![simple_tx], + }, + &context, + &mut working_set, + ) + .unwrap(); + } + evm.end_soft_confirmation_hook(&soft_confirmation_info, &mut working_set); + evm.finalize_hook(&[98u8; 32].into(), &mut working_set.accessory_state()); + + let db_account = evm + .accounts + .get(&dev_signer.address(), &mut working_set) + .unwrap(); + let base_fee_vault = evm.accounts.get(&BASE_FEE_VAULT, &mut working_set).unwrap(); + let l1_fee_vault = evm.accounts.get(&L1_FEE_VAULT, &mut working_set).unwrap(); + + let coinbase_account = evm + .accounts + .get(&config.coinbase, &mut working_set) + .unwrap(); + + // gas fee remains the same + let tx2_diff_size = 46; + + expected_db_balance -= + U256::from(gas_fee_paid * 10000001 + 1000 + tx2_diff_size + L1_FEE_OVERHEAD as u64); + expected_base_fee_vault_balance += U256::from(gas_fee_paid * 10000000); + expected_coinbase_balance += U256::from(gas_fee_paid); + expected_l1_fee_vault_balance += U256::from(tx2_diff_size + L1_FEE_OVERHEAD as u64); + + assert_eq!(db_account.balance, expected_db_balance); + assert_eq!(base_fee_vault.balance, expected_base_fee_vault_balance); + assert_eq!(coinbase_account.balance, expected_coinbase_balance); + assert_eq!(l1_fee_vault.balance, expected_l1_fee_vault_balance); + + // assert comression discount + assert_eq!( + tx1_diff_size * BROTLI_COMPRESSION_PERCENTAGE as u64 / 100, + tx2_diff_size + ); + + assert_eq!( + evm.receipts + .iter(&mut working_set.accessory_state()) + .map(|r| r.l1_diff_size) + .collect::>(), + [255, 561, 1019, 561, tx1_diff_size, tx2_diff_size] + ); +} + #[test] fn test_call_with_block_overrides() { let (config, dev_signer, contract_addr) =