From 1e8499c3f6826fc967001700a740005a76911bf1 Mon Sep 17 00:00:00 2001 From: AshinGau Date: Tue, 28 Jul 2026 00:43:56 +0800 Subject: [PATCH] chore(deps): restore revm 40 and alloy-evm 0.36 upgrade --- Cargo.toml | 16 ++-- docs/use-with-reth.md | 6 +- rust-toolchain.toml | 3 +- src/bin/fetch_block.rs | 9 ++- src/bin/fetch_continuous.rs | 10 ++- src/bin/replay_mainnet.rs | 11 ++- src/bundle.rs | 4 +- src/delegated_safety/config.rs | 37 ++++++++- src/delegated_safety/handler.rs | 50 ++++++++---- src/delegated_safety/instructions.rs | 28 +++---- src/delegated_safety/reserve.rs | 4 +- src/parallel_state.rs | 91 ++++------------------ src/scheduler.rs | 5 +- src/scheduler/executor.rs | 36 ++++++++- src/scheduler/fallback.rs | 5 +- src/scheduler/ordered_commit.rs | 42 +++++----- src/scheduler/tests.rs | 34 +++++++-- src/test_utils/common/account.rs | 9 ++- src/test_utils/common/execute.rs | 36 +++++---- src/test_utils/common/mainnet.rs | 101 ++++++++++++++++++++++--- src/test_utils/common/storage.rs | 8 +- src/test_utils/erc20/erc20_contract.rs | 1 + src/test_utils/uniswap/contract.rs | 5 ++ tests/delegated_safety.rs | 42 ++++++++-- tests/eip-7702.rs | 5 ++ 25 files changed, 391 insertions(+), 207 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26ade3a..0bef3e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,16 +9,16 @@ license = "MIT OR Apache-2.0" autobins = false [dependencies] -# Upstream revm v29.0.1 (no fork). The miner reward is skipped during parallel execution via a +# Upstream revm v40 (no fork). The miner reward is skipped during parallel execution via a # `GrevmHandler` in deferred-beneficiary mode and self-computed at commit time, so the # previous `Galxe/revm` fork (which only added a `lazy_reward` cfg flag + `ResultAndState` field) # is no longer needed. -revm = { version = "29.0.1", default-features = false } -revm-database = { version = "7.0.5", default-features = false } -revm-state = { version = "7.0.5", default-features = false } -revm-primitives = { version = "20.2.1", default-features = false } -revm-inspector = { version = "10.0.1", default-features = false } -revm-context = { version = "9.1.0", default-features = false } +revm = { version = "40.0.3", default-features = false } +revm-database = { version = "15.0.0", default-features = false } +revm-state = { version = "12.0.0", default-features = false } +revm-primitives = { version = "24.0.0", default-features = false } +revm-inspector = { version = "21.0.3", default-features = false } +revm-context = { version = "18.0.3", default-features = false } ahash = "0.8" rayon = "1.10.0" @@ -26,7 +26,7 @@ parking_lot = "0.12" # Alloy — upstream (the `Galxe/alloy-evm` fork only repointed revm to the gravity fork and added a # `..` to one `ResultAndState` destructure; both are unnecessary against upstream revm). -alloy-evm = { version = "0.21.3", default-features = false } +alloy-evm = { version = "0.36.0", default-features = false } # async futures = "0.3" diff --git a/docs/use-with-reth.md b/docs/use-with-reth.md index 4129221..a3a125c 100644 --- a/docs/use-with-reth.md +++ b/docs/use-with-reth.md @@ -55,7 +55,7 @@ where for outcome in &results { match outcome { TxExecutionOutcome::Executed(result) => { - let _gas_used = result.gas_used(); + let _gas_used = result.tx_gas_used(); } TxExecutionOutcome::Skipped(reason) => { eprintln!("transaction skipped: {reason:?}"); @@ -133,7 +133,9 @@ end-to-end harness (`src/test_utils/common/execute.rs`). ## Optional delegated-account policy `DelegatedSafetyConfig` contains two Grevm/Gravity-specific, opt-in EIP-7702 policies. Both are -disabled by default to preserve stock revm/Ethereum execution semantics: +disabled by default to preserve stock revm/Ethereum execution semantics. They are automatically +inactive before Prague, so one block-scoped policy configuration can safely be reused while +replaying historical blocks: - `forbid_delegated_create` makes `CREATE` and `CREATE2` halt as not activated while executing in a delegated account's context. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6263b2e..51048bd 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,7 +1,6 @@ [toolchain] -channel = "1.88.0" +channel = "1.93.0" # Note: we don't specify cargofmt in our toolchain because we rely on # the nightly version of cargofmt and verify formatting in CI/CD. components = ["cargo", "clippy", "rustc", "rust-docs", "rust-std", "rust-analyzer"] - diff --git a/src/bin/fetch_block.rs b/src/bin/fetch_block.rs index 9cd10e0..b02bceb 100644 --- a/src/bin/fetch_block.rs +++ b/src/bin/fetch_block.rs @@ -27,7 +27,7 @@ mod rpc; use std::path::PathBuf; use grevm::test_utils::common::mainnet::{ - self, BlockFixture, PreState, TxFixture, spec_for_timestamp, write_mainnet_block, + self, BlockFixture, PreState, TxFixture, spec_for_mainnet_block, write_mainnet_block, }; use rpc::{Rpc, parse_block_number}; use serde_json::{Value, json}; @@ -62,8 +62,11 @@ fn main() -> Result<(), Error> { } let spec_id = spec_override.unwrap_or_else(|| { let ts = block.get("timestamp").and_then(Value::as_str).unwrap_or("0x0"); - spec_for_timestamp(u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0)) - .to_string() + spec_for_mainnet_block( + block_number, + u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0), + ) + .to_string() }); let mut block_fixture = BlockFixture::from_rpc(block_number, chain_id, spec_id, &block)?; // prestateTracer can't report block hashes (not account state); fetch the last 256 for diff --git a/src/bin/fetch_continuous.rs b/src/bin/fetch_continuous.rs index 225d782..5cd9298 100644 --- a/src/bin/fetch_continuous.rs +++ b/src/bin/fetch_continuous.rs @@ -26,7 +26,7 @@ use std::path::PathBuf; use grevm::test_utils::common::{ execute, - mainnet::{self, BlockFixture, PreState, TxFixture, spec_for_timestamp, write_block}, + mainnet::{self, BlockFixture, PreState, TxFixture, spec_for_mainnet_block, write_block}, }; use revm_context::TxEnv; use rpc::{Rpc, parse_block_number}; @@ -69,9 +69,11 @@ fn main() -> Result<(), Error> { return Err(format!("block {hex} not found").into()); } let ts = block.get("timestamp").and_then(Value::as_str).unwrap_or("0x0"); - let spec = - spec_for_timestamp(u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0)) - .to_string(); + let spec = spec_for_mainnet_block( + bn, + u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0), + ) + .to_string(); let bf = BlockFixture::from_rpc(bn, chain_id, spec, &block)?; let n_tx = if let Some(arr) = block.get("transactions").and_then(Value::as_array) { diff --git a/src/bin/replay_mainnet.rs b/src/bin/replay_mainnet.rs index adc89c6..925235e 100644 --- a/src/bin/replay_mainnet.rs +++ b/src/bin/replay_mainnet.rs @@ -40,7 +40,8 @@ use std::{ use grevm::test_utils::common::{ execute, mainnet::{ - self, AccountFixture, BlockFixture, MainnetBlock, PreState, TxFixture, spec_for_timestamp, + self, AccountFixture, BlockFixture, MainnetBlock, PreState, TxFixture, + spec_for_mainnet_block, }, }; use revm_primitives::{Address, B256}; @@ -265,9 +266,11 @@ fn build_block( caches: &mut Caches, ) -> Result { let ts = block.get("timestamp").and_then(Value::as_str).unwrap_or("0x0"); - let spec = - spec_for_timestamp(u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0)) - .to_string(); + let spec = spec_for_mainnet_block( + number, + u64::from_str_radix(ts.trim_start_matches("0x"), 16).unwrap_or(0), + ) + .to_string(); let mut bf = BlockFixture::from_rpc(number, chain_id, spec, block)?; // Block hashes for BLOCKHASH, reusing the cross-block cache (fetches only the few new entries). let (lo, hi) = (number.saturating_sub(256), number.saturating_sub(1)); diff --git a/src/bundle.rs b/src/bundle.rs index ff36413..b94fa0e 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -125,7 +125,7 @@ impl ParallelTakeBundle for ParallelState { mod tests { use super::*; use revm_database::{AccountStatus, TransitionAccount}; - use revm_primitives::{Bytes, HashMap}; + use revm_primitives::{AddressMap, Bytes}; use revm_state::AccountInfo; fn transitions(count: usize) -> TransitionState { @@ -142,7 +142,7 @@ mod tests { }, ) }) - .collect::>(), + .collect::>(), } } diff --git a/src/delegated_safety/config.rs b/src/delegated_safety/config.rs index 2501c9b..b45c63e 100644 --- a/src/delegated_safety/config.rs +++ b/src/delegated_safety/config.rs @@ -2,7 +2,8 @@ /// /// The switches are independent because delegated CREATE changes an EOA's nonce, while the /// balance guard handles value movement that admission filtering cannot see without execution. -/// Both are opt-in and disabled by [`Default`], preserving upstream revm behavior. +/// Both are opt-in and disabled by [`Default`], preserving upstream revm behavior. Before Prague, +/// where EIP-7702 is not active, both switches are treated as disabled. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct DelegatedSafetyConfig { /// Halts CREATE and CREATE2 in an EIP-7702 delegated account's execution context with @@ -39,4 +40,38 @@ impl DelegatedSafetyConfig { pub const fn enabled() -> Self { Self { forbid_delegated_create: true, reserve_delegated_balance: true } } + + /// Returns the effective policy for `spec`. + /// + /// EIP-7702 activates in Prague. Keeping a policy configured while replaying older blocks is + /// supported, but it must not alter their instruction table or transaction handler. + pub const fn for_spec(self, spec: revm_primitives::hardfork::SpecId) -> Self { + if spec.is_enabled_in(revm_primitives::hardfork::SpecId::PRAGUE) { + self + } else { + Self::disabled() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use revm_primitives::hardfork::SpecId; + + #[test] + fn delegated_safety_is_inert_before_eip7702_activation() { + assert_eq!( + DelegatedSafetyConfig::enabled().for_spec(SpecId::CANCUN), + DelegatedSafetyConfig::disabled() + ); + assert_eq!( + DelegatedSafetyConfig::enabled().for_spec(SpecId::PRAGUE), + DelegatedSafetyConfig::enabled() + ); + assert_eq!( + DelegatedSafetyConfig::reserve_only().for_spec(SpecId::AMSTERDAM), + DelegatedSafetyConfig::reserve_only() + ); + } } diff --git a/src/delegated_safety/handler.rs b/src/delegated_safety/handler.rs index a4c8cd3..fdd057c 100644 --- a/src/delegated_safety/handler.rs +++ b/src/delegated_safety/handler.rs @@ -1,6 +1,7 @@ use super::{ReserveJournalExt, ReservePlanner}; use metrics::counter; use revm::{ + context_interface::journaled_state::account::JournaledAccountTr, handler::{EvmTr, EvmTrError, FrameResult, FrameTr, Handler, post_execution}, interpreter::{ CallOutcome, InitialAndFloorGas, InstructionResult, InterpreterResult, @@ -10,7 +11,7 @@ use revm::{ use revm_context::{ BlockEnv, ContextTr, JournalTr, Transaction, TxEnv, journaled_state::JournalCheckpoint, - result::{ExecutionResult, HaltReason, InvalidTransaction}, + result::{ExecutionResult, HaltReason, InvalidTransaction, ResultGas}, }; use revm_primitives::Bytes; use revm_state::EvmState; @@ -217,13 +218,17 @@ where type Error = ERROR; type HaltReason = HaltReason; - fn pre_execution(&self, evm: &mut Self::Evm) -> Result { + fn pre_execution( + &self, + evm: &mut Self::Evm, + init_and_floor_gas: &mut InitialAndFloorGas, + ) -> Result { // This is revm's default `pre_execution` sequence. The trait has no hook after // authorization processing, so these three calls are repeated here solely to place the // reserve checkpoint at the transaction/execution boundary. - self.validate_against_state_and_deduct_caller(evm)?; + self.validate_against_state_and_deduct_caller(evm, init_and_floor_gas)?; self.load_accounts(evm)?; - let eip7702_refund = self.apply_eip7702_auth_list(evm)?; + let eip7702_refund = self.apply_eip7702_auth_list(evm, init_and_floor_gas)?; debug_assert!(self.execution_checkpoint.get().is_none()); self.execution_checkpoint.set(Some(evm.ctx().journal_mut().checkpoint())); @@ -237,23 +242,34 @@ where exec_result: &mut FrameResult, init_and_floor_gas: InitialAndFloorGas, eip7702_gas_refund: i64, - ) -> Result<(), Self::Error> { + ) -> Result { // Preserve the gas state before revm merges execution refunds with the authorization // refund. A reserve violation rolls back execution, so only the independently earned // EIP-7702 authorization refund may be reapplied to this snapshot. let execution_gas = *exec_result.gas(); self.refund(evm, exec_result, eip7702_gas_refund); + // Match revm's default lifecycle: capture the pre-floor gas components. `ResultGas` + // applies the EIP-7623 floor when deriving `tx_gas_used`, while retaining the actual + // pre-floor spend and refund for downstream consumers. + let mut result_gas = post_execution::build_result_gas( + exec_result.instruction_result().is_halt(), + exec_result.gas(), + init_and_floor_gas, + ); self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas); self.reimburse_caller(evm, exec_result)?; - self.enforce_reserve( + if let Some(reserve_result_gas) = self.enforce_reserve( evm, exec_result, execution_gas, init_and_floor_gas, eip7702_gas_refund, - )?; - self.beneficiary_mode.apply(evm, exec_result) + )? { + result_gas = reserve_result_gas; + } + self.beneficiary_mode.apply::(evm, exec_result)?; + Ok(result_gas) } } @@ -277,7 +293,7 @@ where execution_gas: revm::interpreter::Gas, init_and_floor_gas: InitialAndFloorGas, eip7702_gas_refund: i64, - ) -> Result<(), ERROR> { + ) -> Result, ERROR> { let execution_checkpoint = self .execution_checkpoint .take() @@ -301,12 +317,18 @@ where // from the pre-refund execution gas. The first reimbursement was also reverted, so // apply it again using this synthetic top-level REVERT result. self.refund(evm, exec_result, eip7702_gas_refund); + let result_gas = post_execution::build_result_gas( + exec_result.instruction_result().is_halt(), + exec_result.gas(), + init_and_floor_gas, + ); self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas); self.reimburse_caller(evm, exec_result)?; + return Ok(Some(result_gas)) } else { evm.ctx().journal_mut().checkpoint_commit(); } - Ok(()) + Ok(None) } fn has_reserve_violation( @@ -357,11 +379,9 @@ where // Reproduce the nonce bump normally performed by revm's top-level CREATE frame. It must be a // journaled mutation so a surrounding transaction/error rollback still has correct semantics. let sender = evm.ctx_ref().tx().caller(); - let account = evm.ctx().journal_mut().load_account(sender)?; - let Some(new_nonce) = account.data.info.nonce.checked_add(1) else { + let mut account = evm.ctx().journal_mut().load_account_mut(sender)?; + if !account.data.bump_nonce() { return Err(InvalidTransaction::NonceOverflowInTransaction.into()); - }; - account.data.info.nonce = new_nonce; - evm.ctx().journal_mut().nonce_bump_journal_entry(sender); + } Ok(()) } diff --git a/src/delegated_safety/instructions.rs b/src/delegated_safety/instructions.rs index 4e20599..b72ee55 100644 --- a/src/delegated_safety/instructions.rs +++ b/src/delegated_safety/instructions.rs @@ -2,7 +2,7 @@ use revm::{ bytecode::opcode::{CREATE, CREATE2}, handler::instructions::EthInstructions, interpreter::{ - Host, Instruction, InstructionContext, InstructionResult, + Host, Instruction, InstructionContext, InstructionExecResult, InstructionResult, instructions::contract, interpreter::EthInterpreter, interpreter_types::{InputsTr, InterpreterTypes, RuntimeFlag}, @@ -11,43 +11,39 @@ use revm::{ use revm_primitives::hardfork::SpecId; /// Mainnet instructions with grevm-local CREATE/CREATE2 delegated-context guard. -pub(crate) fn gravity_instructions() -> EthInstructions +pub(crate) fn gravity_instructions(spec: SpecId) -> EthInstructions where CTX: Host, { - let mut instructions = EthInstructions::new_mainnet(); - instructions.insert_instruction(CREATE, Instruction::new(guarded_create::<_, false, _>, 0)); - instructions.insert_instruction(CREATE2, Instruction::new(guarded_create::<_, true, _>, 0)); + let mut instructions = EthInstructions::new_mainnet_with_spec(spec); + instructions.insert_instruction(CREATE, Instruction::new(guarded_create::), 0); + instructions.insert_instruction(CREATE2, Instruction::new(guarded_create::), 0); instructions } -fn guarded_create( +fn guarded_create( context: InstructionContext<'_, H, WIRE>, -) { +) -> InstructionExecResult { if context.interpreter.runtime_flag.is_static() { - context.interpreter.halt(InstructionResult::StateChangeDuringStaticCall); - return; + return Err(InstructionResult::StateChangeDuringStaticCall) } if IS_CREATE2 && !context.interpreter.runtime_flag.spec_id().is_enabled_in(SpecId::PETERSBURG) { - context.interpreter.halt(InstructionResult::NotActivated); - return; + return Err(InstructionResult::NotActivated) } // `target_address` is the account owning this execution context. For a 7702 call it remains // the delegated EOA even though the interpreter executes bytecode loaded from its delegate. let recipient = context.interpreter.input.target_address(); let Some(load) = context.host.load_account_delegated(recipient) else { - context.interpreter.halt(InstructionResult::FatalExternalError); - return; + return Err(InstructionResult::FatalExternalError) }; // `Some(coldness)` means the target has an EIP-7702 delegation designator; the boolean itself // only reports whether loading the delegate was cold and is irrelevant to this policy. if load.is_delegate_account_cold.is_some() { - context.interpreter.halt(InstructionResult::NotActivated); - return; + return Err(InstructionResult::NotActivated) } - contract::create::(context); + contract::create::(context) } diff --git a/src/delegated_safety/reserve.rs b/src/delegated_safety/reserve.rs index 60b5757..3474af4 100644 --- a/src/delegated_safety/reserve.rs +++ b/src/delegated_safety/reserve.rs @@ -284,7 +284,9 @@ fn balance_before_entry( #[cfg(test)] mod tests { use super::*; - use revm_context::{JournalTr, journal::entry::SelfdestructionRevertStatus}; + use revm::{ + context::JournalTr, context_interface::journaled_state::entry::SelfdestructionRevertStatus, + }; use revm_database::EmptyDB; use revm_primitives::address; use revm_state::{Account, Bytecode}; diff --git a/src/parallel_state.rs b/src/parallel_state.rs index bf213b0..937678d 100644 --- a/src/parallel_state.rs +++ b/src/parallel_state.rs @@ -7,7 +7,7 @@ use revm_database::{ TransitionAccount, TransitionState, states::{CacheAccount, bundle_state::BundleRetention, plain_account::PlainStorage}, }; -use revm_primitives::{Address, B256, HashMap, U256}; +use revm_primitives::{Address, B256, U256}; use revm_state::{Account, AccountInfo, Bytecode, EvmState}; use std::{ fmt::Formatter, @@ -161,39 +161,6 @@ impl CacheAccountInfo { } } - /// Account got touched and before EIP161 state clear this account is considered created. - pub fn touch_create_pre_eip161( - &mut self, - storage: StorageWithOriginalValues, - ) -> (Option, PlainStorage) { - let previous_status = self.status; - - let had_no_info = self.account.as_ref().map(|info| info.is_empty()).unwrap_or_default(); - match self.status.on_touched_created_pre_eip161(had_no_info) { - None => return (None, PlainStorage::default()), - Some(new_status) => { - self.status = new_status; - } - } - - let plain_storage = storage.iter().map(|(k, v)| (*k, v.present_value)).collect(); - let previous_info = self.account.take(); - - self.account = Some(AccountInfo::default()); - - ( - Some(TransitionAccount { - info: Some(AccountInfo::default()), - status: self.status, - previous_info, - previous_status, - storage, - storage_was_destroyed: false, - }), - plain_storage, - ) - } - pub fn change( &mut self, new: AccountInfo, @@ -228,7 +195,7 @@ impl CacheAccountInfo { /// It loads all accounts from database and applies revm output to it. /// /// It generates transitions that is used to build BundleState. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct ParallelCacheState { /// Cached accounts pub accounts: DashMap, @@ -236,30 +203,17 @@ pub struct ParallelCacheState { pub storage: DashMap>, /// Cache contracts pub contracts: DashMap, - /// Has EIP-161 state clear enabled (Spurious Dragon hardfork). - pub has_state_clear: bool, -} - -impl Default for ParallelCacheState { - fn default() -> Self { - Self::new(true) - } } impl ParallelCacheState { /// New default state. - pub fn new(has_state_clear: bool) -> Self { - Self { - accounts: Default::default(), - storage: Default::default(), - contracts: Default::default(), - has_state_clear, - } + pub fn new() -> Self { + Self::default() } /// Copy the cached data and convert to CacheState pub fn as_cache_state(&self) -> CacheState { - let mut state = CacheState::new(self.has_state_clear); + let mut state = CacheState::new(); for kv in self.accounts.iter() { let info = kv.value(); state.accounts.insert( @@ -290,11 +244,6 @@ impl ParallelCacheState { state } - /// Set state clear flag. EIP-161. - pub fn set_state_clear_flag(&mut self, has_state_clear: bool) { - self.has_state_clear = has_state_clear; - } - /// Insert not existing account. pub fn insert_not_existing(&self, address: Address) { self.accounts @@ -387,19 +336,14 @@ impl ParallelCacheState { // Account is touched, but not selfdestructed or newly created. // Account can be touched and not changed. // And when empty account is touched it needs to be removed from database. - // EIP-161 state clear + // revm v40+ normalizes pre-EIP-161 empty-account semantics in the journal's + // `finalize()`: newly materialized empty accounts are marked as created, while + // pre-existing empty accounts are unmarked as touched. Therefore, an account that + // reaches the commit layer as touched, empty, and not created must be cleared. else if is_empty { self.storage.remove(&address); - if self.has_state_clear { - // touch empty account. - (self.get_account_mut(address).touch_empty_eip161(), None) - } else { - // if account is empty and state clear is not enabled we should save - // empty account. - let (transition, changed_slots) = - self.get_account_mut(address).touch_create_pre_eip161(changed_storage); - (transition, Some(changed_slots)) - } + drop(changed_storage); + (self.get_account_mut(address).touch_empty_eip161(), None) } else { let (transition, changed_slots) = self.get_account_mut(address).change(account.info, changed_storage); @@ -458,8 +402,8 @@ pub(crate) type BuildIdentityHasher = BuildHasherDefault; /// State of blockchain. /// -/// State clear flag is set inside CacheState and by default it is enabled. -/// If you want to disable it use `set_state_clear_flag` function. +/// Fork-sensitive account semantics, including EIP-161 state clearing, are resolved by revm's +/// journal according to `CfgEnv::spec` before the finalized state reaches this commit layer. /// /// Represents the state of a parallelized execution environment, managing /// cache, database interactions, and state transitions. @@ -718,7 +662,7 @@ impl DatabaseRef for ParallelStateCommit<'_, DB> { } impl DatabaseCommit for ParallelStateCommit<'_, DB> { - fn commit(&mut self, evm_state: HashMap) { + fn commit(&mut self, evm_state: revm_primitives::AddressMap) { let transitions = self.shared.cache.apply_evm_state_inner(evm_state); if let Some(state) = self.transition_state.as_mut() { state.add_transitions(transitions); @@ -833,11 +777,6 @@ impl ParallelState { Ok(balances) } - /// State clear EIP-161 is enabled in Spurious Dragon hardfork. - pub fn set_state_clear_flag(&mut self, has_state_clear: bool) { - self.cache.set_state_clear_flag(has_state_clear); - } - /// Insert non-existent account pub fn insert_not_existing(&self, address: Address) { self.cache.insert_not_existing(address) @@ -956,7 +895,7 @@ impl DatabaseRef for ParallelState { } impl DatabaseCommit for ParallelState { - fn commit(&mut self, evm_state: HashMap) { + fn commit(&mut self, evm_state: revm_primitives::AddressMap) { let transitions = self.cache.apply_evm_state(evm_state); self.apply_transition(transitions); } diff --git a/src/scheduler.rs b/src/scheduler.rs index 4aefee6..ffe8731 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -169,9 +169,12 @@ where txs: Arc>, state: ParallelState, custom_precompiles: Option>>, - config: GrevmConfig, + mut config: GrevmConfig, ) -> Self { let num_txs = txs.len(); + // The configuration may be shared across historical and current blocks. EIP-7702 safety + // policies become effective only once the selected EVM spec activates Prague. + config.delegated_safety = config.delegated_safety.for_spec(cfg.spec); // Reserve-planner construction is O(1): sender indexing and per-account maximum-cost // suffixes remain lazy until surviving delegated execution actually debits an account. let reserve_planner = config diff --git a/src/scheduler/executor.rs b/src/scheduler/executor.rs index e2433e4..9c51007 100644 --- a/src/scheduler/executor.rs +++ b/src/scheduler/executor.rs @@ -126,8 +126,10 @@ where .with_precompiles(PrecompilesMap::from_static(Precompiles::new( PrecompileSpecId::from_spec_id(spec), ))); - if forbid_delegated_create { - evm.instruction = gravity_instructions(); + // Keep this local gate as a defensive invariant for callers of this construction helper; + // Scheduler also normalizes the complete delegated-safety policy for the selected spec. + if forbid_delegated_create && spec.is_enabled_in(revm_primitives::hardfork::SpecId::PRAGUE) { + evm.instruction = gravity_instructions(spec); } for (address, precompile) in custom_precompiles { let precompile = precompile.clone(); @@ -135,3 +137,33 @@ where } evm } + +#[cfg(test)] +mod tests { + use super::*; + use revm_database::EmptyDB; + use revm_primitives::hardfork::SpecId; + + #[test] + fn delegated_create_guard_preserves_selected_spec_gas_table() { + for spec in [ + SpecId::FRONTIER, + SpecId::SPURIOUS_DRAGON, + SpecId::BERLIN, + SpecId::PRAGUE, + SpecId::OSAKA, + SpecId::AMSTERDAM, + ] { + let cfg = CfgEnv::new_with_spec(spec); + let block = BlockEnv::default(); + let standard = build_evm(EmptyDB::new(), cfg.clone(), block.clone(), &[], false); + let guarded = build_evm(EmptyDB::new(), cfg, block, &[], true); + + assert_eq!( + guarded.instruction.gas_table(), + standard.instruction.gas_table(), + "gas table mismatch for {spec:?}" + ); + } + } +} diff --git a/src/scheduler/fallback.rs b/src/scheduler/fallback.rs index d923444..5a217e9 100644 --- a/src/scheduler/fallback.rs +++ b/src/scheduler/fallback.rs @@ -154,7 +154,7 @@ mod tests { use crate::ParallelState; use revm_context::{ BlockEnv, CfgEnv, - result::{Output, SuccessReason}, + result::{Output, ResultGas, SuccessReason}, }; use revm_database::EmptyDB; use revm_primitives::{Bytes, hardfork::SpecId}; @@ -163,8 +163,7 @@ mod tests { fn success() -> ExecutionResult { ExecutionResult::Success { reason: SuccessReason::Stop, - gas_used: 21_000, - gas_refunded: 0, + gas: ResultGas::default().with_total_gas_spent(21_000), logs: Vec::new(), output: Output::Call(Bytes::new()), } diff --git a/src/scheduler/ordered_commit.rs b/src/scheduler/ordered_commit.rs index 6f15ac7..b4413f2 100644 --- a/src/scheduler/ordered_commit.rs +++ b/src/scheduler/ordered_commit.rs @@ -113,8 +113,9 @@ where /// `post_execution::reward_beneficiary`: from LONDON the basefee is burned and only the /// remainder of the effective gas price reaches the beneficiary (EIP-1559). /// - /// `result.gas_used()` is exactly the `gas.used()` (post-refund) that revm bills the reward on, - /// so deferred commit reproduces revm's immediate reward calculation. + /// `result.tx_gas_used()` is the post-refund, floor-aware transaction gas charge and matches + /// the effective gas amount revm uses for the beneficiary reward, excluding any unused + /// EIP-8037 reservoir. Deferred commit therefore reproduces revm's immediate calculation. fn compute_reward(&self, tx_env: &TxEnv, result: &ExecutionResult) -> u128 { let basefee = self.basefee as u128; let effective_gas_price = tx_env.effective_gas_price(basefee); @@ -123,7 +124,7 @@ where } else { effective_gas_price }; - coinbase_gas_price.saturating_mul(result.gas_used() as u128) + coinbase_gas_price.saturating_mul(result.tx_gas_used() as u128) } /// Commit one speculative result at the current ordered boundary. @@ -192,12 +193,12 @@ mod tests { use revm_context::{ DBErrorMarker, either::Either, - result::{Output, SuccessReason}, + result::{Output, ResultGas, SuccessReason}, transaction::{Authorization, RecoveredAuthority, RecoveredAuthorization}, }; use revm_database::EmptyDB; - use revm_primitives::{Address, B256, Bytes, HashMap, U256}; - use revm_state::{Account, AccountInfo, AccountStatus, Bytecode, EvmStorage}; + use revm_primitives::{Address, B256, Bytes, U256}; + use revm_state::{Account, AccountInfo, AccountStatus, Bytecode}; use std::fmt::{Display, Formatter}; #[derive(Clone, Debug, PartialEq, Eq)] @@ -241,25 +242,24 @@ mod tests { nonce, code_hash: B256::ZERO, code: None, + ..Default::default() } } + fn make_account(info: AccountInfo) -> Account { + let mut account = Account::default(); + account.info = info; + account.status = AccountStatus::Touched; + account + } + fn make_result_and_state(caller: Address, post_nonce: u64) -> ResultAndState { - let mut state: HashMap = HashMap::default(); - state.insert( - caller, - Account { - info: make_account_info(post_nonce), - transaction_id: 0, - storage: EvmStorage::default(), - status: AccountStatus::Touched, - }, - ); + let mut state = revm_primitives::AddressMap::default(); + state.insert(caller, make_account(make_account_info(post_nonce))); ResultAndState { result: ExecutionResult::Success { reason: SuccessReason::Stop, - gas_used: 21_000, - gas_refunded: 0, + gas: ResultGas::default().with_total_gas_spent(21_000), logs: Vec::new(), output: Output::Call(Bytes::new()), }, @@ -368,8 +368,7 @@ mod tests { let gas_used = 21_000u64; let result = ExecutionResult::Success { reason: SuccessReason::Stop, - gas_used, - gas_refunded: 0, + gas: ResultGas::default().with_total_gas_spent(gas_used), logs: Vec::new(), output: Output::Call(Bytes::new()), }; @@ -476,8 +475,7 @@ mod tests { assert_eq!(output.end(), CommittedPrefixEnd::ZERO); let committed = output.push(ExecutionResult::Success { reason: SuccessReason::Stop, - gas_used: 21_000, - gas_refunded: 0, + gas: ResultGas::default().with_total_gas_spent(21_000), logs: Vec::new(), output: Output::Call(Bytes::new()), }); diff --git a/src/scheduler/tests.rs b/src/scheduler/tests.rs index 144ab19..7c3188b 100644 --- a/src/scheduler/tests.rs +++ b/src/scheduler/tests.rs @@ -1,10 +1,8 @@ use super::*; -#[cfg(feature = "test-utils")] -use crate::DelegatedSafetyConfig; -use crate::InvalidTransaction; +use crate::{DelegatedSafetyConfig, InvalidTransaction}; use revm_context::{ DBErrorMarker, - result::{ExecutionResult, Output, ResultAndState, SuccessReason}, + result::{ExecutionResult, Output, ResultAndState, ResultGas, SuccessReason}, }; use revm_database::EmptyDB; use revm_primitives::{B256, Bytes, TxKind, U256, hardfork::SpecId}; @@ -90,6 +88,28 @@ fn empty_scheduler(num_txs: usize) -> Scheduler { ) } +#[test] +fn scheduler_activates_delegated_safety_only_from_prague() { + let make_scheduler = |spec| { + Scheduler::new_with_runtime_config( + CfgEnv::new_with_spec(spec), + BlockEnv::default(), + Arc::new(Vec::new()), + ParallelState::new(EmptyDB::default(), true, false), + None, + GrevmConfig::default().with_delegated_safety(DelegatedSafetyConfig::enabled()), + ) + }; + + let cancun = make_scheduler(SpecId::CANCUN); + assert_eq!(cancun.config.delegated_safety, DelegatedSafetyConfig::disabled()); + assert!(cancun.reserve_planner.is_none()); + + let prague = make_scheduler(SpecId::PRAGUE); + assert_eq!(prague.config.delegated_safety, DelegatedSafetyConfig::enabled()); + assert!(prague.reserve_planner.is_some()); +} + #[test] fn scheduler_returns_an_error_for_a_second_execution() { let scheduler = empty_scheduler(0); @@ -285,8 +305,7 @@ fn ordered_commit_database_error_aborts_and_returns_exact_error() { execute_result: Ok(ResultAndState { result: ExecutionResult::Success { reason: SuccessReason::Stop, - gas_used: 21_000, - gas_refunded: 0, + gas: ResultGas::default().with_total_gas_spent(21_000), logs: Vec::new(), output: Output::Call(Bytes::new()), }, @@ -338,8 +357,7 @@ fn ordered_commit_error_retains_the_successful_prefix() { execute_result: Ok(ResultAndState { result: ExecutionResult::Success { reason: SuccessReason::Stop, - gas_used: 21_000, - gas_refunded: 0, + gas: ResultGas::default().with_total_gas_spent(21_000), logs: Vec::new(), output: Output::Call(Bytes::new()), }, diff --git a/src/test_utils/common/account.rs b/src/test_utils/common/account.rs index 6910a8d..e520ed4 100644 --- a/src/test_utils/common/account.rs +++ b/src/test_utils/common/account.rs @@ -8,7 +8,13 @@ pub const MINER_ADDRESS: Address = address!("00000000000000000000000000000000000 pub fn mock_miner_account() -> (Address, PlainAccount) { let account = PlainAccount { - info: AccountInfo { balance: U256::from(0), nonce: 1, code_hash: KECCAK_EMPTY, code: None }, + info: AccountInfo { + balance: U256::from(0), + nonce: 1, + code_hash: KECCAK_EMPTY, + code: None, + ..Default::default() + }, storage: Default::default(), }; (MINER_ADDRESS, account) @@ -28,6 +34,7 @@ pub fn mock_eoa_account(idx: usize) -> (Address, PlainAccount) { nonce: 1, code_hash: KECCAK_EMPTY, code: None, + ..Default::default() }, storage: Default::default(), }; diff --git a/src/test_utils/common/execute.rs b/src/test_utils/common/execute.rs index 0629c25..0a8b7dd 100644 --- a/src/test_utils/common/execute.rs +++ b/src/test_utils/common/execute.rs @@ -6,8 +6,7 @@ use crate::{ TxExecutionOutcome, }; use revm::{ - Context, DatabaseCommit, DatabaseRef, MainBuilder, MainContext, - precompile::{PrecompileSpecId, Precompiles}, + Context, DatabaseCommit, DatabaseRef, MainBuilder, MainContext, handler::EthPrecompiles, }; use revm_context::{ BlockEnv, CfgEnv, TxEnv, @@ -298,14 +297,18 @@ where .with_cfg(cfg) .with_block(env) .build_mainnet_with_inspector(NoOpInspector {}) - .with_precompiles(PrecompilesMap::from_static(Precompiles::new( - PrecompileSpecId::from_spec_id(spec), - ))); + .with_precompiles(PrecompilesMap::from_static(EthPrecompiles::new(spec).precompiles)); let mut evm = EthEvm::new(evm, false); let mut results = Vec::with_capacity(txs.len()); for tx in txs { - let result_and_state = evm.transact_raw(tx.clone())?; + let result_and_state = evm.transact_raw(tx.clone()).map_err(|e| match e { + EVMError::Transaction(t) => EVMError::Transaction(t), + EVMError::Header(h) => EVMError::Header(h), + EVMError::Database(inner) => EVMError::Database(inner.into_external_error()), + EVMError::Custom(s) => EVMError::Custom(s), + EVMError::CustomAny(a) => EVMError::CustomAny(a), + })?; evm.db_mut().commit(result_and_state.state); results.push(result_and_state.result); } @@ -337,9 +340,7 @@ where .with_cfg(cfg) .with_block(env) .build_mainnet_with_inspector(NoOpInspector {}) - .with_precompiles(PrecompilesMap::from_static(Precompiles::new( - PrecompileSpecId::from_spec_id(spec), - ))); + .with_precompiles(PrecompilesMap::from_static(EthPrecompiles::new(spec).precompiles)); let mut evm = EthEvm::new(evm, false); let mut outcomes = Vec::with_capacity(txs.len()); @@ -347,7 +348,9 @@ where if !disable_nonce_check && tx.nonce == u64::MAX { let state_nonce = match evm.db_mut().basic_ref(tx.caller) { Ok(info) => info.map_or(0, |info| info.nonce), - Err(error) => return Err(EVMError::Database(error)), + Err(error) => { + return Err(EVMError::Database(error.into_external_error())); + } }; if state_nonce == u64::MAX { outcomes.push(TxExecutionOutcome::Skipped( @@ -364,7 +367,12 @@ where Err(EVMError::Transaction(error)) => { outcomes.push(TxExecutionOutcome::Skipped(error)); } - Err(error) => return Err(error), + Err(EVMError::Header(error)) => return Err(EVMError::Header(error)), + Err(EVMError::Database(error)) => { + return Err(EVMError::Database(error.into_external_error())); + } + Err(EVMError::Custom(error)) => return Err(EVMError::Custom(error)), + Err(EVMError::CustomAny(error)) => return Err(EVMError::CustomAny(error)), } } evm.db_mut().merge_transitions(BundleRetention::Reverts); @@ -400,9 +408,7 @@ where .with_cfg(cfg) .with_block(env) .build_mainnet_with_inspector(NoOpInspector {}) - .with_precompiles(PrecompilesMap::from_static(Precompiles::new( - PrecompileSpecId::from_spec_id(spec), - ))); + .with_precompiles(PrecompilesMap::from_static(EthPrecompiles::new(spec).precompiles)); let mut evm = EthEvm::new(evm, false); let mut kept = Vec::new(); @@ -410,7 +416,7 @@ where for (i, tx) in txs.iter().enumerate() { match evm.transact_raw(tx.clone()) { Ok(result_and_state) => { - total_gas = total_gas.saturating_add(result_and_state.result.gas_used()); + total_gas = total_gas.saturating_add(result_and_state.result.tx_gas_used()); evm.db_mut().commit(result_and_state.state); kept.push(i); if total_gas >= gas_cap { diff --git a/src/test_utils/common/mainnet.rs b/src/test_utils/common/mainnet.rs index e9ac7c7..f6fb557 100644 --- a/src/test_utils/common/mainnet.rs +++ b/src/test_utils/common/mainnet.rs @@ -75,8 +75,9 @@ pub struct BlockFixture { pub excess_blob_gas: Option, /// Chain id (1 for mainnet). pub chain_id: u64, - /// Hardfork name, parsed via [`SpecId::from_str`] (e.g. `"Prague"`). Falls back to - /// [`SpecId::PRAGUE`] if unrecognized. + /// Hardfork name, parsed via [`SpecId::from_str`] (e.g. `"Prague"`). Unrecognized values and + /// legacy fixtures that incorrectly labeled pre-merge blocks as `"Merge"` are resolved from + /// the mainnet block number and timestamp. pub spec_id: String, /// Recent block hashes for the `BLOCKHASH` opcode (block number -> hash), normally the last /// 256 ancestors. Not part of any account's state, so `prestateTracer` cannot provide them; @@ -86,9 +87,17 @@ pub struct BlockFixture { } impl BlockFixture { - /// Resolve the [`SpecId`] for this block, defaulting to [`SpecId::PRAGUE`]. + /// Resolve the [`SpecId`] for this block. pub fn spec_id(&self) -> SpecId { - SpecId::from_str(&self.spec_id).unwrap_or(SpecId::PRAGUE) + let historical = || { + SpecId::from_str(spec_for_mainnet_block(self.number, self.timestamp)) + .expect("mainnet hardfork names are valid SpecIds") + }; + match SpecId::from_str(&self.spec_id) { + Ok(SpecId::MERGE) if self.number < 15_537_394 => historical(), + Ok(stored) => stored, + Err(_) => historical(), + } } /// Build the revm [`CfgEnv`] for this block. `disable_nonce_check` is left `false`: real @@ -110,6 +119,8 @@ impl BlockFixture { difficulty: self.difficulty, prevrandao: self.prevrandao, blob_excess_gas_and_price: None, + // Amsterdam (EIP-7843) is not modeled in the test fixtures; leave slot_num at zero. + slot_num: 0, }; if let Some(excess) = self.excess_blob_gas { // Record the real `excess_blob_gas` but pin the blob gas price to the protocol minimum @@ -273,7 +284,13 @@ pub fn pre_state_to_db(pre_state: &PreState) -> InMemoryDB { } _ => (KECCAK_EMPTY, None), }; - let info = AccountInfo { balance: acc.balance, nonce: acc.nonce, code_hash, code }; + let info = AccountInfo { + balance: acc.balance, + nonce: acc.nonce, + code_hash, + code, + ..Default::default() + }; let storage = acc.storage.iter().map(|(k, v)| (*k, *v)).collect(); accounts.insert(*addr, PlainAccount { info, storage }); } @@ -466,18 +483,47 @@ fn write_json(path: &Path, value: &T) -> io::Result<()> { // the `src/bin/` fetchers. // --------------------------------------------------------------------------------------------- -/// Mainnet hardfork activation by block timestamp (recent forks only). The replay oracle is -/// parallel-vs-sequential, and this revm build models up to Prague, so we cap there; pass an -/// explicit spec to the fetcher to override. +/// Mainnet hardfork activation by timestamp when the block number is unavailable. +/// +/// This helper only distinguishes timestamp-activated forks. Use [`spec_for_mainnet_block`] when +/// the block number is known so pre-merge block-height activations are handled correctly. pub fn spec_for_timestamp(ts: u64) -> &'static str { match ts { + t if t >= 1_764_798_551 => "Osaka", // Fusaka, 2025-12-03 t if t >= 1_746_612_311 => "Prague", // Pectra, 2025-05-07 t if t >= 1_710_338_135 => "Cancun", // Dencun, 2024-03-13 - t if t >= 1_681_338_479 => "Shanghai", // Shapella, 2023-04-12 + t if t >= 1_681_338_455 => "Shanghai", // Shapella, 2023-04-12 _ => "Merge", } } +/// Resolve the closest revm EVM spec for an Ethereum mainnet block number and timestamp. +/// +/// Pre-merge forks activated by block height; Shanghai and later forks activate by timestamp. +/// revm intentionally collapses hardforks that did not change EVM execution rules (for example, +/// DAO, Muir Glacier, Arrow Glacier, and Gray Glacier) into the preceding supported spec. +/// Amsterdam has no mainnet activation yet, so automatic fixture generation is capped at Osaka. +pub fn spec_for_mainnet_block(number: u64, timestamp: u64) -> &'static str { + match timestamp { + t if t >= 1_764_798_551 => "Osaka", // Fusaka, 2025-12-03 + t if t >= 1_746_612_311 => "Prague", // Pectra, 2025-05-07 + t if t >= 1_710_338_135 => "Cancun", // Dencun, 2024-03-13 + t if t >= 1_681_338_455 => "Shanghai", // Shapella, 2023-04-12 + _ => match number { + n if n >= 15_537_394 => "Merge", + n if n >= 12_965_000 => "London", + n if n >= 12_244_000 => "Berlin", + n if n >= 9_069_000 => "Istanbul", + n if n >= 7_280_000 => "Petersburg", + n if n >= 4_370_000 => "Byzantium", + n if n >= 2_675_000 => "Spurious", + n if n >= 2_463_000 => "Tangerine", + n if n >= 1_150_000 => "Homestead", + _ => "Frontier", + }, + } +} + impl BlockFixture { /// Build a [`BlockFixture`] from an `eth_getBlockByNumber` result. pub fn from_rpc( @@ -650,3 +696,40 @@ fn parse_b256(s: &str) -> Result { fn parse_bytes(s: &str) -> Result { s.parse::().map_err(|e| e.to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mainnet_hardfork_resolution_covers_historical_and_timestamp_boundaries() { + assert_eq!(spec_for_mainnet_block(1_149_999, 0), "Frontier"); + assert_eq!(spec_for_mainnet_block(1_150_000, 0), "Homestead"); + assert_eq!(spec_for_mainnet_block(2_674_999, 0), "Tangerine"); + assert_eq!(spec_for_mainnet_block(2_675_000, 0), "Spurious"); + assert_eq!(spec_for_mainnet_block(15_537_394, 0), "Merge"); + assert_eq!(spec_for_mainnet_block(17_034_870, 1_681_338_455), "Shanghai"); + assert_eq!(spec_for_mainnet_block(23_935_694, 1_764_798_551), "Osaka"); + } + + #[test] + fn invalid_and_legacy_fixture_specs_are_resolved_from_block_metadata() { + let mut fixture = BlockFixture { + number: 1_124_576, + coinbase: Address::ZERO, + timestamp: 1_457_550_778, + gas_limit: 0, + basefee: 0, + difficulty: U256::ZERO, + prevrandao: None, + excess_blob_gas: None, + chain_id: 1, + spec_id: "Merge".to_string(), + block_hashes: BTreeMap::new(), + }; + + assert_eq!(fixture.spec_id(), SpecId::FRONTIER); + fixture.spec_id = "not-a-hardfork".to_string(); + assert_eq!(fixture.spec_id(), SpecId::FRONTIER); + } +} diff --git a/src/test_utils/common/storage.rs b/src/test_utils/common/storage.rs index 89476e0..5121cde 100644 --- a/src/test_utils/common/storage.rs +++ b/src/test_utils/common/storage.rs @@ -8,7 +8,7 @@ use revm::{ }; use revm_context::DBErrorMarker; use revm_database::PlainAccount; -use revm_primitives::HashMap; +use revm_primitives::{HashMap, StorageKeyMap}; use revm_state::{AccountInfo, Bytecode}; /// A DatabaseRef that stores chain data in memory. @@ -95,12 +95,12 @@ impl DatabaseRef for InMemoryDB { #[derive(Debug, Default)] pub struct StorageBuilder { - dict: HashMap, + dict: StorageKeyMap, } impl StorageBuilder { pub fn new() -> Self { - StorageBuilder { dict: HashMap::default() } + StorageBuilder { dict: StorageKeyMap::default() } } pub fn set(&mut self, slot: K, value: V) @@ -135,7 +135,7 @@ impl StorageBuilder { *entry = buffer.into(); } - pub fn build(self) -> HashMap { + pub fn build(self) -> StorageKeyMap { self.dict } } diff --git a/src/test_utils/erc20/erc20_contract.rs b/src/test_utils/erc20/erc20_contract.rs index 37e8d2c..8b58fb8 100644 --- a/src/test_utils/erc20/erc20_contract.rs +++ b/src/test_utils/erc20/erc20_contract.rs @@ -105,6 +105,7 @@ impl ERC20Token { nonce: 1, code_hash: bytecode.hash_slow(), code: Some(bytecode), + ..Default::default() }, storage: store.build().into_iter().collect(), } diff --git a/src/test_utils/uniswap/contract.rs b/src/test_utils/uniswap/contract.rs index f4ca195..1dafa1d 100644 --- a/src/test_utils/uniswap/contract.rs +++ b/src/test_utils/uniswap/contract.rs @@ -59,6 +59,7 @@ impl WETH9 { nonce: 1u64, code_hash: bytecode.hash_slow(), code: Some(bytecode), + ..Default::default() }, storage: store.build(), } @@ -128,6 +129,7 @@ impl UniswapV3Factory { nonce: 1u64, code_hash: bytecode.hash_slow(), code: Some(bytecode), + ..Default::default() }, storage: store.build(), } @@ -234,6 +236,7 @@ impl UniswapV3Pool { nonce: 1u64, code_hash: bytecode.hash_slow(), code: Some(bytecode), + ..Default::default() }, storage: store.build(), } @@ -294,6 +297,7 @@ impl SwapRouter { nonce: 1u64, code_hash: bytecode.hash_slow(), code: Some(bytecode), + ..Default::default() }, storage: store.build(), } @@ -335,6 +339,7 @@ impl SingleSwap { nonce: 1u64, code_hash: bytecode.hash_slow(), code: Some(bytecode), + ..Default::default() }, storage: store.build(), } diff --git a/tests/delegated_safety.rs b/tests/delegated_safety.rs index 62c0210..0a7ae9a 100644 --- a/tests/delegated_safety.rs +++ b/tests/delegated_safety.rs @@ -66,6 +66,7 @@ fn eoa_account(balance: u128, nonce: u64) -> PlainAccount { nonce, code_hash: KECCAK_EMPTY, code: None, + ..Default::default() }, storage: Default::default(), } @@ -355,7 +356,7 @@ fn retry_probe_precompiles( let blocker_executions = executions.clone(); let blocker_precompile = DynPrecompile::new_stateful( PrecompileId::Custom("grevm-test-retry-blocker".into()), - move |_| { + move |input| { if coordinate_parallel_attempt { while blocker_executions.load(Ordering::Acquire) == 0 { thread::yield_now(); @@ -364,14 +365,14 @@ fn retry_probe_precompiles( // for tx 1 to publish its stale speculative result before tx 0 publishes its write. thread::sleep(Duration::from_millis(50)); } - Ok(PrecompileOutput::new(0, Bytes::new())) + Ok(PrecompileOutput::new(0, Bytes::new(), input.reservoir)) }, ); let probe_precompile = DynPrecompile::new_stateful( PrecompileId::Custom("grevm-test-retry-probe".into()), - move |_| { + move |input| { executions.fetch_add(1, Ordering::AcqRel); - Ok(PrecompileOutput::new(0, Bytes::new())) + Ok(PrecompileOutput::new(0, Bytes::new(), input.reservoir)) }, ); Arc::new(vec![(blocker, blocker_precompile), (probe, probe_precompile)]) @@ -693,8 +694,8 @@ fn reserve_revert_preserves_eip7702_authorization_refund() { // policy changes the final outcome to REVERT, but the pre-execution authorization refund is // independent of the rolled-back balance transfer and therefore keeps gas usage identical. let (standard, _) = - execute_block(db.clone(), txs.clone(), DelegatedSafetyConfig::disabled(), false); - let (reserve, _) = execute_block(db, txs, DelegatedSafetyConfig::reserve_only(), false); + execute_in_both_modes(db.clone(), txs.clone(), DelegatedSafetyConfig::disabled()); + let (reserve, _) = execute_in_both_modes(db, txs, DelegatedSafetyConfig::reserve_only()); assert_outcome(&standard[10], OutcomeKind::Success, "reserve disabled"); assert_outcome(&reserve[10], OutcomeKind::Revert, "reserve enabled"); let TxExecutionOutcome::Executed(standard) = &standard[10] else { @@ -703,7 +704,7 @@ fn reserve_revert_preserves_eip7702_authorization_refund() { let TxExecutionOutcome::Executed(reserve) = &reserve[10] else { unreachable!("outcome checked above") }; - assert_eq!(standard.gas_used(), reserve.gas_used()); + assert_eq!(standard.gas(), reserve.gas()); } #[test] @@ -803,6 +804,31 @@ fn reserve_tracking_does_not_change_selfdestruct_gas() { let TxExecutionOutcome::Executed(reserve) = &reserve[10] else { panic!("selfdestruct transaction must execute on the reserve path") }; - assert_eq!(standard.gas_used(), reserve.gas_used()); + assert_eq!(standard.gas(), reserve.gas()); execute::compare_bundle_state(&standard_bundle, &reserve_bundle); } + +#[test] +fn reserve_handler_preserves_full_result_gas_when_eip7623_floor_applies() { + let db = database(Bytecode::new()); + let mut txs: Vec<_> = (0..BLOCK_SIZE).map(padding_tx).collect(); + txs[10] = TxEnv { + data: Bytes::from(vec![1; 1_000]), + gas_limit: 100_000, + value: U256::ZERO, + ..padding_tx(10) + }; + + let (standard, _) = + execute_in_both_modes(db.clone(), txs.clone(), DelegatedSafetyConfig::disabled()); + let (reserve, _) = execute_in_both_modes(db, txs, DelegatedSafetyConfig::reserve_only()); + let TxExecutionOutcome::Executed(standard) = &standard[10] else { + panic!("floor transaction must execute on the standard path") + }; + let TxExecutionOutcome::Executed(reserve) = &reserve[10] else { + panic!("floor transaction must execute on the reserve path") + }; + + assert_eq!(standard.tx_gas_used(), standard.gas().floor_gas()); + assert_eq!(standard.gas(), reserve.gas()); +} diff --git a/tests/eip-7702.rs b/tests/eip-7702.rs index d5c47d5..42dfda5 100644 --- a/tests/eip-7702.rs +++ b/tests/eip-7702.rs @@ -84,6 +84,7 @@ fn contract_account(code: &Bytecode) -> PlainAccount { nonce: 1, code_hash: code.hash_slow(), code: Some(code.clone()), + ..Default::default() }, storage: Default::default(), } @@ -96,6 +97,7 @@ fn eoa_account(balance: u128, nonce: u64) -> PlainAccount { nonce, code_hash: KECCAK_EMPTY, code: None, + ..Default::default() }, storage: Default::default(), } @@ -382,6 +384,7 @@ fn db_with_predelegated_a(nonce: u64, stored: u64) -> InMemoryDB { nonce, code_hash: base_designator.hash_slow(), code: Some(base_designator.clone()), + ..Default::default() }, storage: [(U256::from(0), U256::from(stored))].into_iter().collect(), }; @@ -472,6 +475,7 @@ fn selfdestruct_then_recreate_clears_storage() { nonce: 1, code_hash: selfdestruct_code.hash_slow(), code: Some(selfdestruct_code.clone()), + ..Default::default() }, storage: [(U256::from(0), U256::from(99))].into_iter().collect(), }; @@ -554,6 +558,7 @@ fn create2_target_redelegate_and_selfdestruct() { nonce: 5, code_hash: base_designator.hash_slow(), code: Some(base_designator.clone()), + ..Default::default() }, storage: [(U256::from(0), U256::from(42))].into_iter().collect(), },