From a9982736184a9c5a8c08af3aa4b48c1121889550 Mon Sep 17 00:00:00 2001 From: sledro Date: Sun, 17 Aug 2025 14:06:12 +0100 Subject: [PATCH 01/14] feat: add support for optional gasless transactions - Introduced `optional_gasless` feature across multiple crates, including context, handler, and revm. - Updated configuration structures and methods to handle gasless transaction logic. - Added tests to validate gasless transaction behavior for EIP-1559. - Enhanced transaction validation to accommodate gasless transactions. --- crates/context/Cargo.toml | 1 + crates/context/interface/Cargo.toml | 1 + crates/context/interface/src/cfg.rs | 4 ++ crates/context/interface/src/transaction.rs | 16 +++++ crates/context/src/cfg.rs | 14 ++++ crates/handler/Cargo.toml | 6 ++ crates/handler/src/validation.rs | 72 ++++++++++++++++++++- crates/op-revm/Cargo.toml | 1 + crates/op-revm/src/handler.rs | 15 ++++- crates/revm/Cargo.toml | 1 + 10 files changed, 128 insertions(+), 3 deletions(-) diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml index 77da37c7f0..bb45980c81 100644 --- a/crates/context/Cargo.toml +++ b/crates/context/Cargo.toml @@ -71,3 +71,4 @@ optional_eip3541 = [] optional_eip3607 = [] optional_no_base_fee = [] optional_priority_fee_check = [] +optional_gasless = [] diff --git a/crates/context/interface/Cargo.toml b/crates/context/interface/Cargo.toml index eeedf23050..06394acdb3 100644 --- a/crates/context/interface/Cargo.toml +++ b/crates/context/interface/Cargo.toml @@ -55,3 +55,4 @@ serde = [ "either/serde", ] serde-json = ["serde"] +optional_gasless = [] diff --git a/crates/context/interface/src/cfg.rs b/crates/context/interface/src/cfg.rs index 3212b81520..ee0a5974f8 100644 --- a/crates/context/interface/src/cfg.rs +++ b/crates/context/interface/src/cfg.rs @@ -58,6 +58,10 @@ pub trait Cfg { /// Returns whether the priority fee check is disabled. fn is_priority_fee_check_disabled(&self) -> bool; + + /// Returns whether gasless transactions are allowed. + #[cfg(feature = "optional_gasless")] + fn is_gasless_allowed(&self) -> bool { false } } /// What bytecode analysis to perform diff --git a/crates/context/interface/src/transaction.rs b/crates/context/interface/src/transaction.rs index 091512df3e..22b3ad9db4 100644 --- a/crates/context/interface/src/transaction.rs +++ b/crates/context/interface/src/transaction.rs @@ -211,3 +211,19 @@ pub trait Transaction { Ok(effective_balance_spending) } } + +#[cfg(feature = "optional_gasless")] +/// Returns true if the transaction is a zero-fee transaction, independent of any config. +/// +/// Rules: +/// - Legacy/EIP-2930: gas_price == 0 +/// - EIP-1559/EIP-4844/EIP-7702: max_fee_per_gas == 0 +pub fn is_gasless(tx: &T) -> bool { + match TransactionType::from(tx.tx_type()) { + TransactionType::Legacy | TransactionType::Eip2930 => tx.gas_price() == 0, + TransactionType::Eip1559 | TransactionType::Eip4844 | TransactionType::Eip7702 => { + tx.max_fee_per_gas() == 0 + } + _ => false, + } +} diff --git a/crates/context/src/cfg.rs b/crates/context/src/cfg.rs index 788b358cfc..5b0cbfc92b 100644 --- a/crates/context/src/cfg.rs +++ b/crates/context/src/cfg.rs @@ -104,6 +104,11 @@ pub struct CfgEnv { /// By default, it is set to `false`. #[cfg(feature = "optional_priority_fee_check")] pub disable_priority_fee_check: bool, + /// Allow gasless transactions + /// + /// By default, it is set to `false`. + #[cfg(feature = "optional_gasless")] + pub allow_gasless: bool, } impl CfgEnv { @@ -159,6 +164,8 @@ impl CfgEnv { disable_base_fee: false, #[cfg(feature = "optional_priority_fee_check")] disable_priority_fee_check: false, + #[cfg(feature = "optional_gasless")] + allow_gasless: false, } } @@ -206,6 +213,8 @@ impl CfgEnv { disable_base_fee: self.disable_base_fee, #[cfg(feature = "optional_priority_fee_check")] disable_priority_fee_check: self.disable_priority_fee_check, + #[cfg(feature = "optional_gasless")] + allow_gasless: self.allow_gasless, } } @@ -266,6 +275,10 @@ impl + Copy> Cfg for CfgEnv { self.max_blobs_per_tx } + #[inline] + #[cfg(feature = "optional_gasless")] + fn is_gasless_allowed(&self) -> bool { self.allow_gasless } + fn max_code_size(&self) -> usize { self.limit_contract_code_size .unwrap_or(eip170::MAX_CODE_SIZE) @@ -344,6 +357,7 @@ impl + Copy> Cfg for CfgEnv { } } } + } impl Default for CfgEnv { diff --git a/crates/handler/Cargo.toml b/crates/handler/Cargo.toml index 080c2d615f..5c63a0b614 100644 --- a/crates/handler/Cargo.toml +++ b/crates/handler/Cargo.toml @@ -73,3 +73,9 @@ serde = [ "derive-where/serde" ] serde-json = ["serde"] + +# lightlink features +optional_gasless = [ + "context/optional_gasless", + "context-interface/optional_gasless", +] \ No newline at end of file diff --git a/crates/handler/src/validation.rs b/crates/handler/src/validation.rs index 73fccedcf8..448b265773 100644 --- a/crates/handler/src/validation.rs +++ b/crates/handler/src/validation.rs @@ -92,12 +92,18 @@ pub fn validate_tx_env( let tx_type = context.tx().tx_type(); let tx = context.tx(); - let base_fee = if context.cfg().is_base_fee_check_disabled() { + #[cfg_attr(not(feature = "optional_gasless"), allow(unused_mut))] + let mut base_fee = if context.cfg().is_base_fee_check_disabled() { None } else { Some(context.block().basefee() as u128) }; + #[cfg(feature = "optional_gasless")] + if context.cfg().is_gasless_allowed() && context_interface::transaction::is_gasless(&tx) { + base_fee = None; + } + let tx_type = TransactionType::from(tx_type); // Check chain_id if config is enabled. @@ -604,4 +610,68 @@ mod tests { _ => panic!("execution result is not Success"), } } + + #[cfg(feature = "optional_gasless")] + #[test] + fn test_optional_gasless_eip1559_zero_fees_allowed() { + let caller = address!("0x0000000000000000000000000000000000100001"); + let to = address!("0x0000000000000000000000000000000000200002"); + + let ctx = Context::mainnet() + .modify_block_chained(|b| { + b.basefee = 100; + }) + .modify_cfg_chained(|c| { + c.allow_gasless = true; + }) + .with_db(CacheDB::::default()); + + let result = ctx + .build_mainnet() + .transact_commit( + TxEnv::builder() + .tx_type(Some(2)) // EIP-1559 + .caller(caller) + .kind(TxKind::Call(to)) + .gas_limit(21_000) + .gas_price(0) // max_fee_per_gas = 0 + .gas_priority_fee(Some(0)) + .build() + .unwrap(), + ); + + assert!(matches!(result, Ok(ExecutionResult::Success { .. }))); + } + + #[cfg(feature = "optional_gasless")] + #[test] + fn test_optional_gasless_eip1559_zero_fees_disallowed() { + let caller = address!("0x0000000000000000000000000000000000300003"); + let to = address!("0x0000000000000000000000000000000000400004"); + + let ctx = Context::mainnet() + .modify_block_chained(|b| { + b.basefee = 100; + }) + .with_db(CacheDB::::default()); + + let result = ctx + .build_mainnet() + .transact_commit( + TxEnv::builder() + .tx_type(Some(2)) // EIP-1559 + .caller(caller) + .kind(TxKind::Call(to)) + .gas_limit(21_000) + .gas_price(0) // max_fee_per_gas = 0 + .gas_priority_fee(Some(0)) + .build() + .unwrap(), + ); + + assert!(matches!( + result, + Err(EVMError::Transaction(InvalidTransaction::GasPriceLessThanBasefee)) + )); + } } diff --git a/crates/op-revm/Cargo.toml b/crates/op-revm/Cargo.toml index cbb058e8ed..8074510946 100644 --- a/crates/op-revm/Cargo.toml +++ b/crates/op-revm/Cargo.toml @@ -49,6 +49,7 @@ std = [ hashbrown = ["revm/hashbrown"] serde = ["dep:serde", "revm/serde", "alloy-primitives/serde"] portable = ["revm/portable"] +optional_gasless = ["revm/optional_gasless"] dev = [ "memory_limit", diff --git a/crates/op-revm/src/handler.rs b/crates/op-revm/src/handler.rs index 69a471f98b..88c7fee66d 100644 --- a/crates/op-revm/src/handler.rs +++ b/crates/op-revm/src/handler.rs @@ -311,9 +311,14 @@ where let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE; let is_regolith = evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH); + #[cfg(feature = "optional_gasless")] + let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); + #[cfg(not(feature = "optional_gasless"))] + let is_gasless_tx = false; + // Prior to Regolith, deposit transactions did not receive gas refunds. let is_gas_refund_disabled = is_deposit && !is_regolith; - if !is_gas_refund_disabled { + if !is_gas_refund_disabled && !is_gasless_tx { frame_result.gas_mut().set_final_refund( evm.ctx() .cfg() @@ -332,7 +337,13 @@ where let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE; // Transfer fee to coinbase/beneficiary. - if is_deposit { + // When optional_gasless is enabled, also treat gasless transactions as free. + #[cfg(feature = "optional_gasless")] + let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); + #[cfg(not(feature = "optional_gasless"))] + let is_gasless_tx = false; + + if is_deposit || is_gasless_tx { return Ok(()); } diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 5a950e4301..7ca243f386 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -89,6 +89,7 @@ optional_block_gas_limit = ["context/optional_block_gas_limit"] optional_eip3541 = ["context/optional_eip3541"] optional_eip3607 = ["context/optional_eip3607"] optional_no_base_fee = ["context/optional_no_base_fee"] +optional_gasless = ["context/optional_gasless", "handler/optional_gasless"] # Precompiles features From 2e88de6de49a48ac8db42693fa88f0d7f218d0c1 Mon Sep 17 00:00:00 2001 From: sledro Date: Wed, 20 Aug 2025 00:32:53 +0100 Subject: [PATCH 02/14] feat: enhance operator fee reimbursement logic for gasless transactions - Added conditional handling for gasless transactions in the operator fee reimbursement logic. - Introduced a new configuration flag to determine if a transaction is gasless, improving flexibility in transaction processing. --- crates/op-revm/src/handler.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/op-revm/src/handler.rs b/crates/op-revm/src/handler.rs index 88c7fee66d..f6586e867a 100644 --- a/crates/op-revm/src/handler.rs +++ b/crates/op-revm/src/handler.rs @@ -289,7 +289,13 @@ where ) -> Result<(), Self::Error> { let mut additional_refund = U256::ZERO; - if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE { + #[cfg(feature = "optional_gasless")] + let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); + #[cfg(not(feature = "optional_gasless"))] + let is_gasless_tx = false; + + // If not a deposit transaction and not a gasless transaction, reimburse the operator fee. + if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE && !is_gasless_tx { let spec = evm.ctx().cfg().spec(); additional_refund = evm .ctx() From 95632f2d0596ce4dda3e0f687917de339545adfd Mon Sep 17 00:00:00 2001 From: sledro Date: Wed, 20 Aug 2025 00:33:01 +0100 Subject: [PATCH 03/14] chore: remove unnecessary blank line in cfg.rs --- crates/context/src/cfg.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/context/src/cfg.rs b/crates/context/src/cfg.rs index 5b0cbfc92b..fe27b0cf4b 100644 --- a/crates/context/src/cfg.rs +++ b/crates/context/src/cfg.rs @@ -357,7 +357,6 @@ impl + Copy> Cfg for CfgEnv { } } } - } impl Default for CfgEnv { From 65356b4471b80d4f3d6ee7bf7c0ec0a790af519d Mon Sep 17 00:00:00 2001 From: sledro Date: Wed, 20 Aug 2025 20:25:37 +0100 Subject: [PATCH 04/14] feat: integrate gasless transaction handling in fee computation - Added conditional logic to check for gasless transactions in the operator fee reimbursement process. - Enhanced the existing fee calculation to account for gasless transactions when determining L1 costs. --- crates/op-revm/src/handler.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/op-revm/src/handler.rs b/crates/op-revm/src/handler.rs index f6586e867a..d9bb4d19db 100644 --- a/crates/op-revm/src/handler.rs +++ b/crates/op-revm/src/handler.rs @@ -115,10 +115,15 @@ where 0 }; + #[cfg(feature = "optional_gasless")] + let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); + #[cfg(not(feature = "optional_gasless"))] + let is_gasless_tx = false; + let mut additional_cost = U256::ZERO; // The L1-cost fee is only computed for Optimism non-deposit transactions. - if !is_deposit { + if !is_deposit && !is_gasless_tx { // L1 block info is stored in the context for later use. // and it will be reloaded from the database if it is not for the current block. if ctx.chain().l2_block != block_number { From 719638ae2d51dc7f2816090da8e1760721c78781 Mon Sep 17 00:00:00 2001 From: CryptoKass Date: Wed, 20 Aug 2025 21:29:52 +0100 Subject: [PATCH 05/14] added gasless helper --- crates/handler/src/gasless.rs | 101 ++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 crates/handler/src/gasless.rs diff --git a/crates/handler/src/gasless.rs b/crates/handler/src/gasless.rs new file mode 100644 index 0000000000..506b36a3a4 --- /dev/null +++ b/crates/handler/src/gasless.rs @@ -0,0 +1,101 @@ +use primitives::{Address, B256, U256, b256, keccak256, address}; + + +// pub const CREDITS_USED_TOPIC0: B256 = keccak256(b"CreditsUsed(address,address,uint256,uint256)"); + +/// Topic0 for CreditsUsed event. +pub fn credits_used_topic0() -> B256 { + // GUESS WE CAN PRECOMPUTE THIS AND HAVE IT A CONSTANT + keccak256(b"CreditsUsed(address,address,uint256,uint256)") +} + + +/// predeploy local for GasStation by default +pub const GAS_STATION_PREDEPLOY: Address = address!("0x4300000000000000000000000000000000000001"); + + +/// Result of keccak256(abi.encode(uint256(keccak256("gasstation.main")) - 1)) & ~bytes32(uint256(0xff)); +pub const GAS_STATION_STORAGE_LOCATION: B256 = + b256!("0x64d1d9a8a451551a9514a2c08ad4e1552ed316d7dd2778a4b9494de741d8e000"); + +/// Storage slots used by the GasStation contract for a given `to` (recipient) +/// and optional `from`. +#[derive(Clone, Debug)] +pub struct GasStationStorageSlots { + pub registered_slot: B256, // struct base slot (has registered/active packed) + pub active_slot: B256, // TODO REMOVE THIS + pub credits_slot: B256, // credits slot + pub nested_whitelist_map_base_slot: B256, // base slot for the nested whitelist mapping + pub whitelist_enabled_slot: B256, // whitelist enabled flag + pub single_use_enabled_slot: B256, // single use enabled flag + pub used_addresses_map_base_slot: B256, // base slot for the nested usedAddresses mapping +} + +/// calculates the storage slot hashes for a specific registered contract within the GasStation's `contracts` mapping. +/// it returns the base slot for the struct (holding packed fields), the slot for credits, +/// the slot for whitelistEnabled, and the base slot for the nested whitelist mapping. +pub fn calculate_gas_station_slots(registered_contract_address: Address) -> GasStationStorageSlots { + // The 'contracts' mapping is at offset 1 from the storage location + // (dao is at offset 0, contracts is at offset 1) + let contracts_map_slot = U256::from_be_bytes(GAS_STATION_STORAGE_LOCATION.0) + U256::from(1); + + // Calculate the base slot for the struct entry in the mapping + // - left pad the address to 32 bytes + let mut key_padded = [0u8; 32]; + key_padded[12..].copy_from_slice(registered_contract_address.as_slice()); // Left-pad 20-byte address to 32 bytes + // - I expect this is left padded because big endian etc + let map_slot_padded = contracts_map_slot.to_be_bytes::<32>(); + // - keccak256(append(keyPadded, mapSlotPadded...)) + let combined = [key_padded, map_slot_padded].concat(); + let struct_base_slot_hash = keccak256(combined); + + // Calculate subsequent slots by adding offsets to the base slot hash + // New struct layout: bool registered, bool active, address admin (all packed in slot 0) + // uint256 credits (slot 1), bool whitelistEnabled (slot 2), mapping whitelist (slot 3) + // bool singleUseEnabled (slot 4), mapping usedAddresses (slot 5) + let struct_base_slot_u256 = U256::from_be_bytes(struct_base_slot_hash.0); + + // Slot for 'credits' (offset 1 from base - after the packed bools and address) + let credits_slot_u256 = struct_base_slot_u256 + U256::from(1); + let credit_slot_hash = B256::from_slice(&credits_slot_u256.to_be_bytes::<32>()); + + // Slot for 'whitelistEnabled' (offset 2 from base) + let whitelist_enabled_slot_u256 = struct_base_slot_u256 + U256::from(2); + let whitelist_enabled_slot_hash = B256::from_slice(&whitelist_enabled_slot_u256.to_be_bytes::<32>()); + + // Base slot for the nested 'whitelist' mapping (offset 3 from base) + let nested_whitelist_map_base_slot_u256 = struct_base_slot_u256 + U256::from(3); + let nested_whitelist_map_base_slot_hash = B256::from_slice(&nested_whitelist_map_base_slot_u256.to_be_bytes::<32>()); + + // Slot for 'singleUseEnabled' (offset 4 from base) + let single_use_enabled_slot_u256 = struct_base_slot_u256 + U256::from(4); + let single_use_enabled_slot_hash = B256::from_slice(&single_use_enabled_slot_u256.to_be_bytes::<32>()); + + // Base slot for the nested 'usedAddresses' mapping (offset 5 from base) + let used_addresses_map_base_slot_u256 = struct_base_slot_u256 + U256::from(5); + let used_addresses_map_base_slot_hash = B256::from_slice(&used_addresses_map_base_slot_u256.to_be_bytes::<32>()); + + GasStationStorageSlots { + registered_slot: struct_base_slot_hash, + active_slot: struct_base_slot_hash, + credits_slot: credit_slot_hash, + whitelist_enabled_slot: whitelist_enabled_slot_hash, + single_use_enabled_slot: single_use_enabled_slot_hash, + nested_whitelist_map_base_slot: nested_whitelist_map_base_slot_hash, + used_addresses_map_base_slot: used_addresses_map_base_slot_hash, + } +} + +/// Computes the storage slot hash for a nested mapping +pub fn calculate_nested_mapping_slot(key: Address, base_slot: B256) -> B256 { + // Left-pad the address to 32 bytes + let mut key_padded = [0u8; 32]; + key_padded[12..].copy_from_slice(key.as_slice()); // Left-pad 20-byte address to 32 bytes + + // The base_slot is already 32 bytes (B256) + let map_base_slot_padded = base_slot.0; + + // Combine: key first, then base slot + let combined = [key_padded, map_base_slot_padded].concat(); + keccak256(combined) +} \ No newline at end of file From b75ce65296c311f97ce16f2641c0b4765f4cda5e Mon Sep 17 00:00:00 2001 From: CryptoKass Date: Wed, 20 Aug 2025 21:30:16 +0100 Subject: [PATCH 06/14] handle gasless post execution --- crates/handler/src/handler.rs | 39 +++++++++++++++++++++++++++++++---- crates/handler/src/lib.rs | 3 +++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/crates/handler/src/handler.rs b/crates/handler/src/handler.rs index d4634f4663..93b7ceae83 100644 --- a/crates/handler/src/handler.rs +++ b/crates/handler/src/handler.rs @@ -1,18 +1,20 @@ +use crate::GAS_STATION_PREDEPLOY; use crate::{ - evm::FrameTr, execution, post_execution, pre_execution, validation, EvmTr, FrameResult, - ItemOrResult, + evm::FrameTr, execution, gasless::calculate_gas_station_slots, post_execution, pre_execution, + validation, EvmTr, FrameResult, ItemOrResult, }; use context::result::{ExecutionResult, FromStringError}; use context::LocalContextTr; use context_interface::context::ContextError; +use context_interface::transaction::TransactionType; use context_interface::ContextTr; use context_interface::{ result::{HaltReasonTr, InvalidHeader, InvalidTransaction}, Cfg, Database, JournalTr, Transaction, }; use interpreter::interpreter_action::FrameInit; -use interpreter::{Gas, InitialAndFloorGas, SharedMemory}; -use primitives::U256; +use interpreter::{Gas, Host, InitialAndFloorGas, SharedMemory}; +use primitives::{TxKind, U256}; use state::EvmState; /// Trait for errors that can occur during EVM execution. @@ -229,6 +231,35 @@ pub trait Handler { self.reimburse_caller(evm, exec_result)?; // Pay transaction fees to beneficiary self.reward_beneficiary(evm, exec_result)?; + + // Calculate gas station storage slots for legacy and EIP-1559 transactions that call contracts + let tx_type = TransactionType::from(evm.ctx().tx().tx_type()); + if (tx_type == TransactionType::Legacy || tx_type == TransactionType::Eip1559) + && evm.ctx().tx().gas_price() == 0 + { + let ctx = evm.ctx_mut(); + + if let TxKind::Call(target_address) = ctx.tx().kind() { + let gas_station_storage_slots = calculate_gas_station_slots(target_address); + let credits_slot = gas_station_storage_slots.credits_slot.into(); + let available_credits = ctx + .db_mut() + .storage(GAS_STATION_PREDEPLOY, credits_slot) + .unwrap_or_default(); + let gas_used = U256::from(exec_result.gas().used()); + let new_credits = available_credits.saturating_sub(gas_used); + + let res = + ctx.journal_mut() + .sstore(GAS_STATION_PREDEPLOY, credits_slot, new_credits); + if res.is_err() { + return Err(Self::Error::from_string( + "Failed to update credits slot".to_string(), + )); + } + } + } + Ok(()) } diff --git a/crates/handler/src/lib.rs b/crates/handler/src/lib.rs index a220439545..0a25dfa8c6 100644 --- a/crates/handler/src/lib.rs +++ b/crates/handler/src/lib.rs @@ -30,6 +30,8 @@ mod precompile_provider; pub mod system_call; /// Transaction and environment validation utilities. pub mod validation; +/// Gasless execution utilities and types. +pub mod gasless; // Public exports pub use api::{ExecuteCommitEvm, ExecuteEvm}; @@ -42,3 +44,4 @@ pub use mainnet_builder::{MainBuilder, MainContext, MainnetContext, MainnetEvm}; pub use mainnet_handler::MainnetHandler; pub use precompile_provider::{EthPrecompiles, PrecompileProvider}; pub use system_call::{SystemCallCommitEvm, SystemCallEvm, SystemCallTx, SYSTEM_ADDRESS}; +pub use gasless::{GasStationStorageSlots, GAS_STATION_PREDEPLOY, GAS_STATION_STORAGE_LOCATION, calculate_gas_station_slots, calculate_nested_mapping_slot, credits_used_topic0}; From 6f8cb6597bc544a782d5272c3e3e8a8d13c5d3ba Mon Sep 17 00:00:00 2001 From: CryptoKass Date: Wed, 20 Aug 2025 21:44:42 +0100 Subject: [PATCH 07/14] try sload --- crates/handler/src/handler.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/handler/src/handler.rs b/crates/handler/src/handler.rs index 93b7ceae83..d86dcd47cf 100644 --- a/crates/handler/src/handler.rs +++ b/crates/handler/src/handler.rs @@ -243,8 +243,8 @@ pub trait Handler { let gas_station_storage_slots = calculate_gas_station_slots(target_address); let credits_slot = gas_station_storage_slots.credits_slot.into(); let available_credits = ctx - .db_mut() - .storage(GAS_STATION_PREDEPLOY, credits_slot) + .journal_mut() + .sload(GAS_STATION_PREDEPLOY, credits_slot) .unwrap_or_default(); let gas_used = U256::from(exec_result.gas().used()); let new_credits = available_credits.saturating_sub(gas_used); From a14ac682e48def04c994829bf5ad41b9f4743f5f Mon Sep 17 00:00:00 2001 From: sledro Date: Thu, 21 Aug 2025 02:01:22 +0100 Subject: [PATCH 08/14] *(WIP) Refactor gasless transaction handling in the handler - Introduced conditional logic to determine if a transaction is gasless based on the new configuration flag. - Updated the gas station credits management to use the transaction journal correctly, ensuring proper access and account management. - Enhanced error handling for loading the gas station account and updating credits. --- crates/handler/src/handler.rs | 42 +++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/crates/handler/src/handler.rs b/crates/handler/src/handler.rs index d86dcd47cf..0964c39cdd 100644 --- a/crates/handler/src/handler.rs +++ b/crates/handler/src/handler.rs @@ -6,14 +6,14 @@ use crate::{ use context::result::{ExecutionResult, FromStringError}; use context::LocalContextTr; use context_interface::context::ContextError; -use context_interface::transaction::TransactionType; + use context_interface::ContextTr; use context_interface::{ result::{HaltReasonTr, InvalidHeader, InvalidTransaction}, Cfg, Database, JournalTr, Transaction, }; use interpreter::interpreter_action::FrameInit; -use interpreter::{Gas, Host, InitialAndFloorGas, SharedMemory}; +use interpreter::{Gas, InitialAndFloorGas, SharedMemory}; use primitives::{TxKind, U256}; use state::EvmState; @@ -232,26 +232,40 @@ pub trait Handler { // Pay transaction fees to beneficiary self.reward_beneficiary(evm, exec_result)?; + #[cfg(feature = "optional_gasless")] + let is_gasless_tx = context_interface::transaction::is_gasless(&evm.ctx().tx()); + #[cfg(not(feature = "optional_gasless"))] + let is_gasless_tx = false; + // Calculate gas station storage slots for legacy and EIP-1559 transactions that call contracts - let tx_type = TransactionType::from(evm.ctx().tx().tx_type()); - if (tx_type == TransactionType::Legacy || tx_type == TransactionType::Eip1559) - && evm.ctx().tx().gas_price() == 0 - { - let ctx = evm.ctx_mut(); + if is_gasless_tx { - if let TxKind::Call(target_address) = ctx.tx().kind() { + if let TxKind::Call(target_address) = evm.ctx().tx().kind() { let gas_station_storage_slots = calculate_gas_station_slots(target_address); let credits_slot = gas_station_storage_slots.credits_slot.into(); - let available_credits = ctx - .journal_mut() + + // Use tx_journal_mut() to get proper access to both transaction and journal + let (_, journal) = evm.ctx().tx_journal_mut(); + + // Load the gas station account first before accessing its storage + let _account_load = journal.load_account(GAS_STATION_PREDEPLOY); + if _account_load.is_err() { + return Err(Self::Error::from_string( + "Failed to load gas station account".to_string(), + )); + } + + // Mark the account as touched since we're accessing its storage + journal.touch_account(GAS_STATION_PREDEPLOY); + + let available_credits = journal .sload(GAS_STATION_PREDEPLOY, credits_slot) - .unwrap_or_default(); + .unwrap_or_default() + .data; let gas_used = U256::from(exec_result.gas().used()); let new_credits = available_credits.saturating_sub(gas_used); - let res = - ctx.journal_mut() - .sstore(GAS_STATION_PREDEPLOY, credits_slot, new_credits); + let res = journal.sstore(GAS_STATION_PREDEPLOY, credits_slot, new_credits); if res.is_err() { return Err(Self::Error::from_string( "Failed to update credits slot".to_string(), From 274490444e0b51f6e65ff3481dba1b0c733b83b0 Mon Sep 17 00:00:00 2001 From: sledro Date: Tue, 26 Aug 2025 00:50:57 +0100 Subject: [PATCH 09/14] Implement gasless post-execution handling in the gasless module - Added `apply_gasless_post_execution` function to manage gasless transaction accounting after execution. - Updated the handler to utilize the new gasless function, improving code organization and readability. - Enhanced error handling for gasless transactions, ensuring proper credit updates and single-use address tracking. - Refactored gasless transaction checks to streamline the process and reduce redundancy. --- crates/handler/src/gasless.rs | 99 +++++++++++++++++++++++++++++++---- crates/handler/src/handler.rs | 50 +++--------------- 2 files changed, 96 insertions(+), 53 deletions(-) diff --git a/crates/handler/src/gasless.rs b/crates/handler/src/gasless.rs index 506b36a3a4..7d667f0ae8 100644 --- a/crates/handler/src/gasless.rs +++ b/crates/handler/src/gasless.rs @@ -1,5 +1,8 @@ -use primitives::{Address, B256, U256, b256, keccak256, address}; - +use primitives::{Address, B256, U256, b256, keccak256, address, TxKind}; +use crate::EvmTr; +use context_interface::ContextTr; +use context_interface::JournalTr; +use context_interface::Transaction; // pub const CREDITS_USED_TOPIC0: B256 = keccak256(b"CreditsUsed(address,address,uint256,uint256)"); @@ -22,13 +25,20 @@ pub const GAS_STATION_STORAGE_LOCATION: B256 = /// and optional `from`. #[derive(Clone, Debug)] pub struct GasStationStorageSlots { - pub registered_slot: B256, // struct base slot (has registered/active packed) - pub active_slot: B256, // TODO REMOVE THIS - pub credits_slot: B256, // credits slot - pub nested_whitelist_map_base_slot: B256, // base slot for the nested whitelist mapping - pub whitelist_enabled_slot: B256, // whitelist enabled flag - pub single_use_enabled_slot: B256, // single use enabled flag - pub used_addresses_map_base_slot: B256, // base slot for the nested usedAddresses mapping + /// Struct base slot (has registered/active packed). + pub registered_slot: B256, + /// TODO REMOVE THIS. + pub active_slot: B256, + /// Slot for `credits` balance. + pub credits_slot: B256, + /// Base slot for nested `whitelist` mapping. + pub nested_whitelist_map_base_slot: B256, + /// Slot for `whitelistEnabled` flag. + pub whitelist_enabled_slot: B256, + /// Slot for `singleUseEnabled` flag. + pub single_use_enabled_slot: B256, + /// Base slot for nested `usedAddresses` mapping. + pub used_addresses_map_base_slot: B256, } /// calculates the storage slot hashes for a specific registered contract within the GasStation's `contracts` mapping. @@ -98,4 +108,75 @@ pub fn calculate_nested_mapping_slot(key: Address, base_slot: B256) -> B256 { // Combine: key first, then base slot let combined = [key_padded, map_base_slot_padded].concat(); keccak256(combined) +} + + + +/// Applies gasless accounting after execution if the transaction is marked gasless and is a call. +/// Updates credits and single-use flag as needed. Returns an error string on failure. +pub fn apply_gasless_post_execution( + evm: &mut EVM, + gas_used: u64, +) -> Result<(), String> { + #[cfg(feature = "optional_gasless")] + let is_gasless_tx = context_interface::transaction::is_gasless(&evm.ctx().tx()); + #[cfg(not(feature = "optional_gasless"))] + let is_gasless_tx = false; + + if !is_gasless_tx { + return Ok(()); + } + + if let TxKind::Call(target_address) = evm.ctx().tx().kind() { + let gas_station_storage_slots = calculate_gas_station_slots(target_address); + let (tx, journal) = evm.ctx().tx_journal_mut(); + let caller = tx.caller(); + + // Load gas station account and mark as touched + if journal.load_account(GAS_STATION_PREDEPLOY).is_err() { + return Err("Failed to load gas station account".to_string()); + } + journal.touch_account(GAS_STATION_PREDEPLOY); + + // Load available credits and update them based on gas used + let credits_slot = gas_station_storage_slots.credits_slot.into(); + let available_credits = journal + .sload(GAS_STATION_PREDEPLOY, credits_slot) + .unwrap_or_default() + .data; + let gas_used_u256 = U256::from(gas_used); + let new_credits = available_credits.saturating_sub(gas_used_u256); + + if journal + .sstore(GAS_STATION_PREDEPLOY, credits_slot, new_credits) + .is_err() + { + return Err("Failed to update credits slot".to_string()); + } + + // Check if the contract is single-use and mark caller as used if so + let single_use_slot = gas_station_storage_slots.single_use_enabled_slot.into(); + let is_single_use = !journal + .sload(GAS_STATION_PREDEPLOY, single_use_slot) + .unwrap_or_default() + .data + .is_zero(); + + if is_single_use { + // Mark caller as used in the usedAddresses mapping + let used_slot_b256 = calculate_nested_mapping_slot( + caller, + gas_station_storage_slots.used_addresses_map_base_slot, + ); + let used_slot = used_slot_b256.into(); + if journal + .sstore(GAS_STATION_PREDEPLOY, used_slot, U256::ONE) + .is_err() + { + return Err("Failed to update usedAddresses slot".to_string()); + } + } + } + + Ok(()) } \ No newline at end of file diff --git a/crates/handler/src/handler.rs b/crates/handler/src/handler.rs index 0964c39cdd..8657127645 100644 --- a/crates/handler/src/handler.rs +++ b/crates/handler/src/handler.rs @@ -1,7 +1,6 @@ -use crate::GAS_STATION_PREDEPLOY; use crate::{ - evm::FrameTr, execution, gasless::calculate_gas_station_slots, post_execution, pre_execution, - validation, EvmTr, FrameResult, ItemOrResult, + evm::FrameTr, execution, gasless, post_execution, pre_execution, validation, EvmTr, + FrameResult, ItemOrResult, }; use context::result::{ExecutionResult, FromStringError}; use context::LocalContextTr; @@ -14,7 +13,7 @@ use context_interface::{ }; use interpreter::interpreter_action::FrameInit; use interpreter::{Gas, InitialAndFloorGas, SharedMemory}; -use primitives::{TxKind, U256}; +use primitives::U256; use state::EvmState; /// Trait for errors that can occur during EVM execution. @@ -232,46 +231,9 @@ pub trait Handler { // Pay transaction fees to beneficiary self.reward_beneficiary(evm, exec_result)?; - #[cfg(feature = "optional_gasless")] - let is_gasless_tx = context_interface::transaction::is_gasless(&evm.ctx().tx()); - #[cfg(not(feature = "optional_gasless"))] - let is_gasless_tx = false; - - // Calculate gas station storage slots for legacy and EIP-1559 transactions that call contracts - if is_gasless_tx { - - if let TxKind::Call(target_address) = evm.ctx().tx().kind() { - let gas_station_storage_slots = calculate_gas_station_slots(target_address); - let credits_slot = gas_station_storage_slots.credits_slot.into(); - - // Use tx_journal_mut() to get proper access to both transaction and journal - let (_, journal) = evm.ctx().tx_journal_mut(); - - // Load the gas station account first before accessing its storage - let _account_load = journal.load_account(GAS_STATION_PREDEPLOY); - if _account_load.is_err() { - return Err(Self::Error::from_string( - "Failed to load gas station account".to_string(), - )); - } - - // Mark the account as touched since we're accessing its storage - journal.touch_account(GAS_STATION_PREDEPLOY); - - let available_credits = journal - .sload(GAS_STATION_PREDEPLOY, credits_slot) - .unwrap_or_default() - .data; - let gas_used = U256::from(exec_result.gas().used()); - let new_credits = available_credits.saturating_sub(gas_used); - - let res = journal.sstore(GAS_STATION_PREDEPLOY, credits_slot, new_credits); - if res.is_err() { - return Err(Self::Error::from_string( - "Failed to update credits slot".to_string(), - )); - } - } + // Apply gasless accounting if applicable + if let Err(e) = gasless::apply_gasless_post_execution(evm, exec_result.gas().used()) { + return Err(Self::Error::from_string(e)); } Ok(()) From 50c948945b58198e141ce69633c6b74dfb167f3b Mon Sep 17 00:00:00 2001 From: sledro Date: Tue, 26 Aug 2025 00:53:33 +0100 Subject: [PATCH 10/14] - Removed the precomputed `credits_used_topic0` function and its associated comment. - Updated comments for clarity and consistency in the gasless module, including the `GAS_STATION_PREDEPLOY` constant and the `calculate_gas_station_slots` function. --- crates/handler/src/gasless.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/crates/handler/src/gasless.rs b/crates/handler/src/gasless.rs index 7d667f0ae8..67b848abf8 100644 --- a/crates/handler/src/gasless.rs +++ b/crates/handler/src/gasless.rs @@ -4,16 +4,7 @@ use context_interface::ContextTr; use context_interface::JournalTr; use context_interface::Transaction; -// pub const CREDITS_USED_TOPIC0: B256 = keccak256(b"CreditsUsed(address,address,uint256,uint256)"); - -/// Topic0 for CreditsUsed event. -pub fn credits_used_topic0() -> B256 { - // GUESS WE CAN PRECOMPUTE THIS AND HAVE IT A CONSTANT - keccak256(b"CreditsUsed(address,address,uint256,uint256)") -} - - -/// predeploy local for GasStation by default +/// Predeploy local for GasStation by default pub const GAS_STATION_PREDEPLOY: Address = address!("0x4300000000000000000000000000000000000001"); @@ -41,7 +32,7 @@ pub struct GasStationStorageSlots { pub used_addresses_map_base_slot: B256, } -/// calculates the storage slot hashes for a specific registered contract within the GasStation's `contracts` mapping. +/// Calculates the storage slot hashes for a specific registered contract within the GasStation's `contracts` mapping. /// it returns the base slot for the struct (holding packed fields), the slot for credits, /// the slot for whitelistEnabled, and the base slot for the nested whitelist mapping. pub fn calculate_gas_station_slots(registered_contract_address: Address) -> GasStationStorageSlots { From 82741a06e57d08980961e1e532d0b906b0d29a73 Mon Sep 17 00:00:00 2001 From: sledro Date: Tue, 26 Aug 2025 00:53:42 +0100 Subject: [PATCH 11/14] Remove unused `credits_used_topic0` function from gasless module exports --- crates/handler/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/handler/src/lib.rs b/crates/handler/src/lib.rs index 0a25dfa8c6..8a80d71cbe 100644 --- a/crates/handler/src/lib.rs +++ b/crates/handler/src/lib.rs @@ -44,4 +44,4 @@ pub use mainnet_builder::{MainBuilder, MainContext, MainnetContext, MainnetEvm}; pub use mainnet_handler::MainnetHandler; pub use precompile_provider::{EthPrecompiles, PrecompileProvider}; pub use system_call::{SystemCallCommitEvm, SystemCallEvm, SystemCallTx, SYSTEM_ADDRESS}; -pub use gasless::{GasStationStorageSlots, GAS_STATION_PREDEPLOY, GAS_STATION_STORAGE_LOCATION, calculate_gas_station_slots, calculate_nested_mapping_slot, credits_used_topic0}; +pub use gasless::{GasStationStorageSlots, GAS_STATION_PREDEPLOY, GAS_STATION_STORAGE_LOCATION, calculate_gas_station_slots, calculate_nested_mapping_slot}; From ae612482e8058a63eb2d5c0feef98227cd359e08 Mon Sep 17 00:00:00 2001 From: sledro Date: Tue, 26 Aug 2025 01:01:18 +0100 Subject: [PATCH 12/14] run cargo fmt --- crates/context/interface/src/cfg.rs | 4 +- crates/context/src/cfg.rs | 6 ++- crates/handler/src/gasless.rs | 39 +++++++++--------- crates/handler/src/lib.rs | 9 +++-- crates/handler/src/validation.rs | 62 ++++++++++++++--------------- 5 files changed, 63 insertions(+), 57 deletions(-) diff --git a/crates/context/interface/src/cfg.rs b/crates/context/interface/src/cfg.rs index ee0a5974f8..172daf6e1a 100644 --- a/crates/context/interface/src/cfg.rs +++ b/crates/context/interface/src/cfg.rs @@ -61,7 +61,9 @@ pub trait Cfg { /// Returns whether gasless transactions are allowed. #[cfg(feature = "optional_gasless")] - fn is_gasless_allowed(&self) -> bool { false } + fn is_gasless_allowed(&self) -> bool { + false + } } /// What bytecode analysis to perform diff --git a/crates/context/src/cfg.rs b/crates/context/src/cfg.rs index fe27b0cf4b..eed6f7ca2d 100644 --- a/crates/context/src/cfg.rs +++ b/crates/context/src/cfg.rs @@ -277,8 +277,10 @@ impl + Copy> Cfg for CfgEnv { #[inline] #[cfg(feature = "optional_gasless")] - fn is_gasless_allowed(&self) -> bool { self.allow_gasless } - + fn is_gasless_allowed(&self) -> bool { + self.allow_gasless + } + fn max_code_size(&self) -> usize { self.limit_contract_code_size .unwrap_or(eip170::MAX_CODE_SIZE) diff --git a/crates/handler/src/gasless.rs b/crates/handler/src/gasless.rs index 67b848abf8..869cde3c72 100644 --- a/crates/handler/src/gasless.rs +++ b/crates/handler/src/gasless.rs @@ -1,13 +1,12 @@ -use primitives::{Address, B256, U256, b256, keccak256, address, TxKind}; use crate::EvmTr; use context_interface::ContextTr; use context_interface::JournalTr; use context_interface::Transaction; +use primitives::{address, b256, keccak256, Address, TxKind, B256, U256}; /// Predeploy local for GasStation by default pub const GAS_STATION_PREDEPLOY: Address = address!("0x4300000000000000000000000000000000000001"); - /// Result of keccak256(abi.encode(uint256(keccak256("gasstation.main")) - 1)) & ~bytes32(uint256(0xff)); pub const GAS_STATION_STORAGE_LOCATION: B256 = b256!("0x64d1d9a8a451551a9514a2c08ad4e1552ed316d7dd2778a4b9494de741d8e000"); @@ -36,24 +35,24 @@ pub struct GasStationStorageSlots { /// it returns the base slot for the struct (holding packed fields), the slot for credits, /// the slot for whitelistEnabled, and the base slot for the nested whitelist mapping. pub fn calculate_gas_station_slots(registered_contract_address: Address) -> GasStationStorageSlots { - // The 'contracts' mapping is at offset 1 from the storage location - // (dao is at offset 0, contracts is at offset 1) + // The 'contracts' mapping is at offset 1 from the storage location + // (dao is at offset 0, contracts is at offset 1) let contracts_map_slot = U256::from_be_bytes(GAS_STATION_STORAGE_LOCATION.0) + U256::from(1); // Calculate the base slot for the struct entry in the mapping // - left pad the address to 32 bytes let mut key_padded = [0u8; 32]; key_padded[12..].copy_from_slice(registered_contract_address.as_slice()); // Left-pad 20-byte address to 32 bytes - // - I expect this is left padded because big endian etc + // - I expect this is left padded because big endian etc let map_slot_padded = contracts_map_slot.to_be_bytes::<32>(); // - keccak256(append(keyPadded, mapSlotPadded...)) let combined = [key_padded, map_slot_padded].concat(); let struct_base_slot_hash = keccak256(combined); - // Calculate subsequent slots by adding offsets to the base slot hash - // New struct layout: bool registered, bool active, address admin (all packed in slot 0) - // uint256 credits (slot 1), bool whitelistEnabled (slot 2), mapping whitelist (slot 3) - // bool singleUseEnabled (slot 4), mapping usedAddresses (slot 5) + // Calculate subsequent slots by adding offsets to the base slot hash + // New struct layout: bool registered, bool active, address admin (all packed in slot 0) + // uint256 credits (slot 1), bool whitelistEnabled (slot 2), mapping whitelist (slot 3) + // bool singleUseEnabled (slot 4), mapping usedAddresses (slot 5) let struct_base_slot_u256 = U256::from_be_bytes(struct_base_slot_hash.0); // Slot for 'credits' (offset 1 from base - after the packed bools and address) @@ -62,23 +61,27 @@ pub fn calculate_gas_station_slots(registered_contract_address: Address) -> GasS // Slot for 'whitelistEnabled' (offset 2 from base) let whitelist_enabled_slot_u256 = struct_base_slot_u256 + U256::from(2); - let whitelist_enabled_slot_hash = B256::from_slice(&whitelist_enabled_slot_u256.to_be_bytes::<32>()); + let whitelist_enabled_slot_hash = + B256::from_slice(&whitelist_enabled_slot_u256.to_be_bytes::<32>()); // Base slot for the nested 'whitelist' mapping (offset 3 from base) let nested_whitelist_map_base_slot_u256 = struct_base_slot_u256 + U256::from(3); - let nested_whitelist_map_base_slot_hash = B256::from_slice(&nested_whitelist_map_base_slot_u256.to_be_bytes::<32>()); + let nested_whitelist_map_base_slot_hash = + B256::from_slice(&nested_whitelist_map_base_slot_u256.to_be_bytes::<32>()); // Slot for 'singleUseEnabled' (offset 4 from base) let single_use_enabled_slot_u256 = struct_base_slot_u256 + U256::from(4); - let single_use_enabled_slot_hash = B256::from_slice(&single_use_enabled_slot_u256.to_be_bytes::<32>()); + let single_use_enabled_slot_hash = + B256::from_slice(&single_use_enabled_slot_u256.to_be_bytes::<32>()); // Base slot for the nested 'usedAddresses' mapping (offset 5 from base) let used_addresses_map_base_slot_u256 = struct_base_slot_u256 + U256::from(5); - let used_addresses_map_base_slot_hash = B256::from_slice(&used_addresses_map_base_slot_u256.to_be_bytes::<32>()); + let used_addresses_map_base_slot_hash = + B256::from_slice(&used_addresses_map_base_slot_u256.to_be_bytes::<32>()); GasStationStorageSlots { registered_slot: struct_base_slot_hash, - active_slot: struct_base_slot_hash, + active_slot: struct_base_slot_hash, credits_slot: credit_slot_hash, whitelist_enabled_slot: whitelist_enabled_slot_hash, single_use_enabled_slot: single_use_enabled_slot_hash, @@ -92,17 +95,15 @@ pub fn calculate_nested_mapping_slot(key: Address, base_slot: B256) -> B256 { // Left-pad the address to 32 bytes let mut key_padded = [0u8; 32]; key_padded[12..].copy_from_slice(key.as_slice()); // Left-pad 20-byte address to 32 bytes - + // The base_slot is already 32 bytes (B256) let map_base_slot_padded = base_slot.0; - + // Combine: key first, then base slot let combined = [key_padded, map_base_slot_padded].concat(); keccak256(combined) } - - /// Applies gasless accounting after execution if the transaction is marked gasless and is a call. /// Updates credits and single-use flag as needed. Returns an error string on failure. pub fn apply_gasless_post_execution( @@ -170,4 +171,4 @@ pub fn apply_gasless_post_execution( } Ok(()) -} \ No newline at end of file +} diff --git a/crates/handler/src/lib.rs b/crates/handler/src/lib.rs index 8a80d71cbe..b766819058 100644 --- a/crates/handler/src/lib.rs +++ b/crates/handler/src/lib.rs @@ -15,6 +15,8 @@ pub mod evm; pub mod execution; mod frame; mod frame_data; +/// Gasless execution utilities and types. +pub mod gasless; /// Handler implementation for orchestrating EVM execution. pub mod handler; /// EVM instruction set implementations and tables. @@ -30,18 +32,19 @@ mod precompile_provider; pub mod system_call; /// Transaction and environment validation utilities. pub mod validation; -/// Gasless execution utilities and types. -pub mod gasless; // Public exports pub use api::{ExecuteCommitEvm, ExecuteEvm}; pub use evm::{EvmTr, FrameTr}; pub use frame::{return_create, ContextTrDbError, EthFrame}; pub use frame_data::{CallFrame, CreateFrame, FrameData, FrameResult}; +pub use gasless::{ + calculate_gas_station_slots, calculate_nested_mapping_slot, GasStationStorageSlots, + GAS_STATION_PREDEPLOY, GAS_STATION_STORAGE_LOCATION, +}; pub use handler::{EvmTrError, Handler}; pub use item_or_result::{FrameInitOrResult, ItemOrResult}; pub use mainnet_builder::{MainBuilder, MainContext, MainnetContext, MainnetEvm}; pub use mainnet_handler::MainnetHandler; pub use precompile_provider::{EthPrecompiles, PrecompileProvider}; pub use system_call::{SystemCallCommitEvm, SystemCallEvm, SystemCallTx, SYSTEM_ADDRESS}; -pub use gasless::{GasStationStorageSlots, GAS_STATION_PREDEPLOY, GAS_STATION_STORAGE_LOCATION, calculate_gas_station_slots, calculate_nested_mapping_slot}; diff --git a/crates/handler/src/validation.rs b/crates/handler/src/validation.rs index 448b265773..10abb1a7a7 100644 --- a/crates/handler/src/validation.rs +++ b/crates/handler/src/validation.rs @@ -92,17 +92,17 @@ pub fn validate_tx_env( let tx_type = context.tx().tx_type(); let tx = context.tx(); - #[cfg_attr(not(feature = "optional_gasless"), allow(unused_mut))] + #[cfg_attr(not(feature = "optional_gasless"), allow(unused_mut))] let mut base_fee = if context.cfg().is_base_fee_check_disabled() { None } else { Some(context.block().basefee() as u128) }; - #[cfg(feature = "optional_gasless")] - if context.cfg().is_gasless_allowed() && context_interface::transaction::is_gasless(&tx) { - base_fee = None; - } + #[cfg(feature = "optional_gasless")] + if context.cfg().is_gasless_allowed() && context_interface::transaction::is_gasless(&tx) { + base_fee = None; + } let tx_type = TransactionType::from(tx_type); @@ -626,19 +626,17 @@ mod tests { }) .with_db(CacheDB::::default()); - let result = ctx - .build_mainnet() - .transact_commit( - TxEnv::builder() - .tx_type(Some(2)) // EIP-1559 - .caller(caller) - .kind(TxKind::Call(to)) - .gas_limit(21_000) - .gas_price(0) // max_fee_per_gas = 0 - .gas_priority_fee(Some(0)) - .build() - .unwrap(), - ); + let result = ctx.build_mainnet().transact_commit( + TxEnv::builder() + .tx_type(Some(2)) // EIP-1559 + .caller(caller) + .kind(TxKind::Call(to)) + .gas_limit(21_000) + .gas_price(0) // max_fee_per_gas = 0 + .gas_priority_fee(Some(0)) + .build() + .unwrap(), + ); assert!(matches!(result, Ok(ExecutionResult::Success { .. }))); } @@ -655,23 +653,23 @@ mod tests { }) .with_db(CacheDB::::default()); - let result = ctx - .build_mainnet() - .transact_commit( - TxEnv::builder() - .tx_type(Some(2)) // EIP-1559 - .caller(caller) - .kind(TxKind::Call(to)) - .gas_limit(21_000) - .gas_price(0) // max_fee_per_gas = 0 - .gas_priority_fee(Some(0)) - .build() - .unwrap(), - ); + let result = ctx.build_mainnet().transact_commit( + TxEnv::builder() + .tx_type(Some(2)) // EIP-1559 + .caller(caller) + .kind(TxKind::Call(to)) + .gas_limit(21_000) + .gas_price(0) // max_fee_per_gas = 0 + .gas_priority_fee(Some(0)) + .build() + .unwrap(), + ); assert!(matches!( result, - Err(EVMError::Transaction(InvalidTransaction::GasPriceLessThanBasefee)) + Err(EVMError::Transaction( + InvalidTransaction::GasPriceLessThanBasefee + )) )); } } From 91b723903595275d747939e20783401baa27b59f Mon Sep 17 00:00:00 2001 From: sledro Date: Tue, 26 Aug 2025 16:09:02 +0100 Subject: [PATCH 13/14] Refactor gasless transaction handling across modules - Removed direct calls to `is_gasless` in favor of a new `is_gasless_effective` function for improved clarity and maintainability. - Updated transaction validation and fee computation logic to utilize the new gasless check, ensuring consistent handling of gasless transactions. - Enhanced code organization by consolidating gasless transaction checks, reducing redundancy and improving readability. --- crates/context/interface/src/transaction.rs | 1 - crates/handler/src/gasless.rs | 23 +++++++++++---- crates/handler/src/validation.rs | 3 +- crates/op-revm/src/handler.rs | 31 +++++---------------- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/crates/context/interface/src/transaction.rs b/crates/context/interface/src/transaction.rs index 22b3ad9db4..6413b346a4 100644 --- a/crates/context/interface/src/transaction.rs +++ b/crates/context/interface/src/transaction.rs @@ -212,7 +212,6 @@ pub trait Transaction { } } -#[cfg(feature = "optional_gasless")] /// Returns true if the transaction is a zero-fee transaction, independent of any config. /// /// Rules: diff --git a/crates/handler/src/gasless.rs b/crates/handler/src/gasless.rs index 869cde3c72..6494863837 100644 --- a/crates/handler/src/gasless.rs +++ b/crates/handler/src/gasless.rs @@ -1,4 +1,6 @@ use crate::EvmTr; +#[cfg(feature = "optional_gasless")] +use context_interface::Cfg; use context_interface::ContextTr; use context_interface::JournalTr; use context_interface::Transaction; @@ -110,12 +112,7 @@ pub fn apply_gasless_post_execution( evm: &mut EVM, gas_used: u64, ) -> Result<(), String> { - #[cfg(feature = "optional_gasless")] - let is_gasless_tx = context_interface::transaction::is_gasless(&evm.ctx().tx()); - #[cfg(not(feature = "optional_gasless"))] - let is_gasless_tx = false; - - if !is_gasless_tx { + if !is_gasless_effective(evm.ctx_ref()) { return Ok(()); } @@ -172,3 +169,17 @@ pub fn apply_gasless_post_execution( Ok(()) } + +#[inline] +/// Returns true if gasless transactions are allowed by config and the tx is zero-fee (gasless) +pub fn is_gasless_effective(ctx: &C) -> bool { + #[cfg(feature = "optional_gasless")] + { + ctx.cfg().is_gasless_allowed() && context_interface::transaction::is_gasless(ctx.tx()) + } + #[cfg(not(feature = "optional_gasless"))] + { + let _ = ctx; + false + } +} diff --git a/crates/handler/src/validation.rs b/crates/handler/src/validation.rs index 10abb1a7a7..e79d891a6f 100644 --- a/crates/handler/src/validation.rs +++ b/crates/handler/src/validation.rs @@ -99,8 +99,7 @@ pub fn validate_tx_env( Some(context.block().basefee() as u128) }; - #[cfg(feature = "optional_gasless")] - if context.cfg().is_gasless_allowed() && context_interface::transaction::is_gasless(&tx) { + if crate::gasless::is_gasless_effective(&context) { base_fee = None; } diff --git a/crates/op-revm/src/handler.rs b/crates/op-revm/src/handler.rs index d9bb4d19db..71f07e5544 100644 --- a/crates/op-revm/src/handler.rs +++ b/crates/op-revm/src/handler.rs @@ -14,6 +14,7 @@ use revm::{ }, handler::{ evm::FrameTr, + gasless::is_gasless_effective, handler::EvmTrError, post_execution::{self, reimburse_caller}, pre_execution::validate_account_nonce_and_code, @@ -115,15 +116,10 @@ where 0 }; - #[cfg(feature = "optional_gasless")] - let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); - #[cfg(not(feature = "optional_gasless"))] - let is_gasless_tx = false; - let mut additional_cost = U256::ZERO; // The L1-cost fee is only computed for Optimism non-deposit transactions. - if !is_deposit && !is_gasless_tx { + if !is_deposit && !is_gasless_effective(ctx) { // L1 block info is stored in the context for later use. // and it will be reloaded from the database if it is not for the current block. if ctx.chain().l2_block != block_number { @@ -294,13 +290,10 @@ where ) -> Result<(), Self::Error> { let mut additional_refund = U256::ZERO; - #[cfg(feature = "optional_gasless")] - let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); - #[cfg(not(feature = "optional_gasless"))] - let is_gasless_tx = false; - // If not a deposit transaction and not a gasless transaction, reimburse the operator fee. - if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE && !is_gasless_tx { + if evm.ctx().tx().tx_type() != DEPOSIT_TRANSACTION_TYPE + && !is_gasless_effective(evm.ctx_ref()) + { let spec = evm.ctx().cfg().spec(); additional_refund = evm .ctx() @@ -322,14 +315,9 @@ where let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE; let is_regolith = evm.ctx().cfg().spec().is_enabled_in(OpSpecId::REGOLITH); - #[cfg(feature = "optional_gasless")] - let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); - #[cfg(not(feature = "optional_gasless"))] - let is_gasless_tx = false; - // Prior to Regolith, deposit transactions did not receive gas refunds. let is_gas_refund_disabled = is_deposit && !is_regolith; - if !is_gas_refund_disabled && !is_gasless_tx { + if !is_gas_refund_disabled && !is_gasless_effective(evm.ctx_ref()) { frame_result.gas_mut().set_final_refund( evm.ctx() .cfg() @@ -349,12 +337,7 @@ where // Transfer fee to coinbase/beneficiary. // When optional_gasless is enabled, also treat gasless transactions as free. - #[cfg(feature = "optional_gasless")] - let is_gasless_tx = revm::context_interface::transaction::is_gasless(&evm.ctx().tx()); - #[cfg(not(feature = "optional_gasless"))] - let is_gasless_tx = false; - - if is_deposit || is_gasless_tx { + if is_deposit || is_gasless_effective(evm.ctx_ref()) { return Ok(()); } From a8b99b33689de7efe2645218594c318f19bf8306 Mon Sep 17 00:00:00 2001 From: sledro Date: Tue, 26 Aug 2025 20:09:03 +0100 Subject: [PATCH 14/14] Refine gasless transaction logic in `is_gasless` function - Updated the `is_gasless` function to include a check for `max_priority_fee_per_gas` in EIP-1559 transactions, ensuring accurate identification of gasless transactions. - Simplified match arms for better readability and maintainability. --- crates/context/interface/src/transaction.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/context/interface/src/transaction.rs b/crates/context/interface/src/transaction.rs index 6413b346a4..5996c7d745 100644 --- a/crates/context/interface/src/transaction.rs +++ b/crates/context/interface/src/transaction.rs @@ -215,13 +215,13 @@ pub trait Transaction { /// Returns true if the transaction is a zero-fee transaction, independent of any config. /// /// Rules: -/// - Legacy/EIP-2930: gas_price == 0 -/// - EIP-1559/EIP-4844/EIP-7702: max_fee_per_gas == 0 +/// - Legacy: gas_price == 0 +/// - EIP-1559: max_fee_per_gas == 0 && max_priority_fee_per_gas == 0 pub fn is_gasless(tx: &T) -> bool { match TransactionType::from(tx.tx_type()) { - TransactionType::Legacy | TransactionType::Eip2930 => tx.gas_price() == 0, - TransactionType::Eip1559 | TransactionType::Eip4844 | TransactionType::Eip7702 => { - tx.max_fee_per_gas() == 0 + TransactionType::Legacy => tx.gas_price() == 0, + TransactionType::Eip1559 => { + tx.max_fee_per_gas() == 0 && tx.max_priority_fee_per_gas().unwrap_or_default() == 0 } _ => false, }