Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/context/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,5 @@ optional_eip3541 = []
optional_eip3607 = []
optional_no_base_fee = []
optional_priority_fee_check = []
optional_gasless = []
optional_fee_charge = []
1 change: 1 addition & 0 deletions crates/context/interface/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ serde = [

# Deprecated, please use `serde` feature instead.
serde-json = ["serde"]
optional_gasless = []
5 changes: 5 additions & 0 deletions crates/context/interface/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ 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
}
/// Returns whether the fee charge is disabled.
fn is_fee_charge_disabled(&self) -> bool;
}
Expand Down
15 changes: 15 additions & 0 deletions crates/context/interface/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,18 @@ pub trait Transaction {
Ok(effective_balance_spending)
}
}

/// Returns true if the transaction is a zero-fee transaction, independent of any config.
///
/// Rules:
/// - Legacy: gas_price == 0
/// - EIP-1559: max_fee_per_gas == 0 && max_priority_fee_per_gas == 0
pub fn is_gasless<T: Transaction>(tx: &T) -> bool {
match TransactionType::from(tx.tx_type()) {
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,
}
}
15 changes: 15 additions & 0 deletions crates/context/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ pub struct CfgEnv<SPEC = SpecId> {
/// 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,
/// Disables fee charging for transactions.
/// This is useful when executing `eth_call` for example, on OP-chains where setting the base fee
/// to 0 isn't sufficient.
Expand Down Expand Up @@ -165,6 +170,8 @@ impl<SPEC> CfgEnv<SPEC> {
disable_base_fee: false,
#[cfg(feature = "optional_priority_fee_check")]
disable_priority_fee_check: false,
#[cfg(feature = "optional_gasless")]
allow_gasless: false,
#[cfg(feature = "optional_fee_charge")]
disable_fee_charge: false,
}
Expand Down Expand Up @@ -214,6 +221,8 @@ impl<SPEC> CfgEnv<SPEC> {
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,
#[cfg(feature = "optional_fee_charge")]
disable_fee_charge: self.disable_fee_charge,
}
Expand Down Expand Up @@ -283,6 +292,12 @@ impl<SPEC: Into<SpecId> + Copy> Cfg for CfgEnv<SPEC> {
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)
Expand Down
6 changes: 6 additions & 0 deletions crates/handler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,9 @@ serde = [

# Deprecated, please use `serde` feature instead.
serde-json = ["serde"]

# lightlink features
optional_gasless = [
"context/optional_gasless",
"context-interface/optional_gasless",
]
185 changes: 185 additions & 0 deletions crates/handler/src/gasless.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use crate::EvmTr;
#[cfg(feature = "optional_gasless")]
use context_interface::Cfg;
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");

/// Storage slots used by the GasStation contract for a given `to` (recipient)
/// and optional `from`.
#[derive(Clone, Debug)]
pub struct GasStationStorageSlots {
/// 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.
/// 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)
}

/// 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: EvmTr>(
evm: &mut EVM,
gas_used: u64,
) -> Result<(), String> {
if !is_gasless_effective(evm.ctx_ref()) {
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(())
}

#[inline]
/// Returns true if gasless transactions are allowed by config and the tx is zero-fee (gasless)
pub fn is_gasless_effective<C: ContextTr>(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
}
}
11 changes: 9 additions & 2 deletions crates/handler/src/handler.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::{
evm::FrameTr, execution, 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;
use context_interface::context::ContextError;

use context_interface::ContextTr;
use context_interface::{
result::{HaltReasonTr, InvalidHeader, InvalidTransaction},
Expand Down Expand Up @@ -228,6 +229,12 @@ pub trait Handler {
self.reimburse_caller(evm, exec_result)?;
// Pay transaction fees to beneficiary
self.reward_beneficiary(evm, exec_result)?;

// 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(())
}

Expand Down
6 changes: 6 additions & 0 deletions crates/handler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -36,6 +38,10 @@ 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};
Expand Down
Loading