diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index f0fdb80b0..7d10435e8 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -19,7 +19,7 @@ pub use zone_evm::{ZoneEvm, validate_transaction}; use crate::{ fee_manager::ZoneProtocolFeeManager, - precompiles::{L1State, L1StorageReader, extend_zone_precompiles}, + precompiles::{L1State, L1StorageReader, ZonePrecompileEnv, extend_zone_precompiles}, }; use alloy_evm::{ Database, Evm, EvmEnv, EvmFactory, @@ -89,18 +89,14 @@ where zone_hardfork: zone_hardfork::ZoneHardfork, ) -> TempoEvm, I> { let mut evm = evm.with_fee_manager(ZoneProtocolFeeManager::new()); - let cfg = evm.ctx().cfg.clone(); - let actions = StorageActions::disabled(); - let non_creditable_slots = evm.non_creditable_slots(); - let (_, _, precompiles) = evm.components_mut(); - extend_zone_precompiles( - precompiles, - &cfg, + let env = ZonePrecompileEnv::new( + &evm.ctx().cfg, zone_hardfork, - l1, - actions, - non_creditable_slots, + StorageActions::disabled(), + evm.non_creditable_slots(), ); + let (_, _, precompiles) = evm.components_mut(); + extend_zone_precompiles(precompiles, env, l1); evm } } diff --git a/crates/precompiles/src/execution.rs b/crates/precompiles/src/execution.rs index ab8701bed..d17377e0a 100644 --- a/crates/precompiles/src/execution.rs +++ b/crates/precompiles/src/execution.rs @@ -22,7 +22,10 @@ use core::cell::RefCell; use alloy_evm::precompiles::DynPrecompile; use alloy_primitives::{Address, Bytes}; use alloy_sol_types::SolError; -use revm::precompile::{PrecompileHalt, PrecompileId, PrecompileOutput, PrecompileResult}; +use revm::{ + context_interface::cfg::GasParams, + precompile::{PrecompileHalt, PrecompileId, PrecompileOutput, PrecompileResult}, +}; use tempo_chainspec::hardfork::TempoHardfork; use tempo_precompiles::{ DelegateCallNotAllowed, charge_input_cost, @@ -38,9 +41,20 @@ use tempo_precompiles::{ use zone_hardfork::ZoneHardfork; /// Shared EVM configuration and accounting state installed for every Zone precompile wrapper. +/// +/// The dynamic precompile lookup builds a fresh wrapper for every call frame that targets a Zone +/// precompile, so this is a single [`Rc`] handle: cloning it costs one refcount bump instead of a +/// full [`CfgEnv`](revm::context::CfgEnv) copy. #[derive(Clone)] pub struct ZonePrecompileEnv { - cfg: revm::context::CfgEnv, + inner: Rc, +} + +/// The parts of the EVM configuration and transaction-local state the wrappers actually read. +struct ZonePrecompileEnvInner { + spec: TempoHardfork, + enable_amsterdam_eip8037: bool, + gas_params: GasParams, zone_hardfork: ZoneHardfork, actions: StorageActions, non_creditable_slots: Rc>, @@ -55,16 +69,20 @@ impl ZonePrecompileEnv { non_creditable_slots: Rc>, ) -> Self { Self { - cfg: cfg.clone(), - zone_hardfork, - actions, - non_creditable_slots, + inner: Rc::new(ZonePrecompileEnvInner { + spec: cfg.spec, + enable_amsterdam_eip8037: cfg.enable_amsterdam_eip8037, + gas_params: cfg.gas_params.clone(), + zone_hardfork, + actions, + non_creditable_slots, + }), } } /// Returns the active Zone-owned protocol revision. - pub const fn zone_hardfork(&self) -> ZoneHardfork { - self.zone_hardfork + pub fn zone_hardfork(&self) -> ZoneHardfork { + self.inner.zone_hardfork } } @@ -104,7 +122,7 @@ pub(crate) fn create_precompile( rules: impl CallRules, execute: impl Fn(&[u8], Address) -> PrecompileResult + 'static, ) -> DynPrecompile { - let env = env.clone(); + let env = env.inner.clone(); DynPrecompile::new_stateful(PrecompileId::Custom(id.into()), move |input| { if !input.is_direct_call() { return Ok(PrecompileOutput::revert( @@ -134,10 +152,10 @@ pub(crate) fn create_precompile( input.internals, fixed_gas.map_or(input.gas, |_| u64::MAX), input.reservoir, - env.cfg.spec, - env.cfg.enable_amsterdam_eip8037, + env.spec, + env.enable_amsterdam_eip8037, input.is_static, - env.cfg.gas_params.clone(), + env.gas_params.clone(), ) .with_actions(env.actions.clone()) .with_non_creditable_slots(env.non_creditable_slots.clone()); diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index e0a4ae50e..df52f0fe1 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -90,29 +90,22 @@ pub use tempo_contracts::precompiles::TIP403_REGISTRY_ADDRESS; pub use tempo_state::TempoState; pub use zone_fee_manager::{ZONE_FEE_MANAGER_ADDRESS, ZoneFeeManager}; -use alloc::rc::Rc; -use core::cell::RefCell; - use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap}; use alloy_primitives::Address; use alloy_sol_types::SolError; -use revm::context::CfgEnv; -use tempo_chainspec::hardfork::TempoHardfork; use tempo_precompiles::{ ACCOUNT_KEYCHAIN_ADDRESS, NONCE_PRECOMPILE_ADDRESS, Precompile as _, RECEIVE_POLICY_GUARD_ADDRESS, STORAGE_CREDITS_ADDRESS, account_keychain::AccountKeychain, nonce::NonceManager, receive_policy_guard::ReceivePolicyGuard, - storage::actions::StorageActions, - storage_credits::{NonCreditableSlots, StorageCredits}, + storage_credits::StorageCredits, tip20::{ITIP20::InsufficientBalance as TIP20InsufficientBalance, TIP20Token, is_tip20_prefix}, tip403_registry::TIP403Registry, }; #[cfg(feature = "std")] use tempo_zone_contracts::ZONE_OUTBOX_ADDRESS; use tempo_zone_contracts::{TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS}; -use zone_hardfork::ZoneHardfork; /// Registers every precompile that is available to a Zone EVM. /// @@ -123,16 +116,11 @@ use zone_hardfork::ZoneHardfork; /// Existing Tempo precompiles that are not supported by Zones are explicitly removed here. pub fn extend_zone_precompiles

( precompiles: &mut PrecompilesMap, - cfg: &CfgEnv, - zone_hardfork: ZoneHardfork, + env: ZonePrecompileEnv, l1: L1State

, - actions: StorageActions, - non_creditable_slots: Rc>, ) where P: L1StorageReader, { - let env = ZonePrecompileEnv::new(cfg, zone_hardfork, actions, non_creditable_slots); - precompiles.set_precompile_lookup(move |address: &Address| { #[cfg(feature = "std")] if *address == ZONE_OUTBOX_ADDRESS { @@ -224,3 +212,59 @@ pub fn create_tip20_precompile(address: Address, env: &ZonePrecompileEnv) -> Dyn #[cfg(any(test, feature = "test-utils"))] #[doc(hidden)] pub mod test_utils; + +#[cfg(test)] +mod tests { + use super::*; + use alloc::rc::Rc; + use alloy_primitives::{address, map::AddressSet}; + use core::cell::RefCell; + use revm::{context::CfgEnv, handler::PrecompileProvider, precompile::Precompiles}; + use tempo_chainspec::hardfork::TempoHardfork; + use tempo_precompiles::{ + storage::actions::StorageActions, storage_credits::NonCreditableSlots, + }; + + use crate::test_utils::{MockL1Reader, TestContext}; + + /// Zone precompiles must stay out of the warm address set: revm warms + /// [`PrecompileProvider::warm_addresses`] at the start of every transaction, so registering + /// them there would turn cold CALLs into warm ones and change consensus gas. + #[test] + fn zone_precompiles_are_resolved_without_warming_their_addresses() { + let mut precompiles = PrecompilesMap::from_static(Precompiles::latest()); + let before: AddressSet = + PrecompileProvider::::warm_addresses(&precompiles).clone(); + + let env = ZonePrecompileEnv::new( + &CfgEnv::::default(), + zone_hardfork::ZoneHardfork::Z0, + StorageActions::disabled(), + Rc::new(RefCell::new(NonCreditableSlots::empty())), + ); + extend_zone_precompiles( + &mut precompiles, + env, + L1State::new(MockL1Reader::default(), Address::ZERO), + ); + + let after = PrecompileProvider::::warm_addresses(&precompiles); + assert_eq!(&before, after); + + for address in [ + TEMPO_STATE_ADDRESS, + ZONE_INBOX_ADDRESS, + ZONE_OUTBOX_ADDRESS, + ZONE_FEE_MANAGER_ADDRESS, + TIP403_REGISTRY_ADDRESS, + NONCE_PRECOMPILE_ADDRESS, + ACCOUNT_KEYCHAIN_ADDRESS, + RECEIVE_POLICY_GUARD_ADDRESS, + STORAGE_CREDITS_ADDRESS, + address!("0x20C0000000000000000000000000000000000001"), + ] { + assert!(precompiles.get(&address).is_some(), "{address} unresolved"); + assert!(!after.contains(&address), "{address} became warm"); + } + } +} diff --git a/crates/precompiles/src/storage.rs b/crates/precompiles/src/storage.rs index 8638d624d..1029b53ec 100644 --- a/crates/precompiles/src/storage.rs +++ b/crates/precompiles/src/storage.rs @@ -60,53 +60,69 @@ pub trait L1StorageReader: Clone + Send + Sync + 'static { /// Clones share the selected anchor and provider handle so the Zone database adapter, `TempoState`, /// and other L1-backed precompiles enforce one view of L1 state. A new `L1State` must be created /// for each EVM execution context; it must not be shared across independent EVMs. -#[derive(Clone)] +/// +/// The Zone precompile lookup clones this handle for every call frame that resolves an L1-backed +/// precompile, so the shared state sits behind a single [`Rc`]: cloning is one refcount bump. pub struct L1State

{ + inner: Rc>, +} + +struct L1StateInner

{ /// Tempo block number selected for the current transaction attempt. - anchor: Rc>>, + anchor: Cell>, /// `(account, slot)` keys successfully accessed during the current transaction attempt. /// /// Used for cold/warm gas accounting. Unlike REVM's journal, this access set is not rolled back /// when a subcall reverts, preserving charges for potentially incurred L1 fetch work and /// simplifying the accounting model. - access_set: Rc>>, + access_set: RefCell>, /// Underlying cache/RPC-backed reader for storage at an explicit Tempo block number. provider: P, /// ZonePortal read through the L1 provider by explicit storage operations. portal_address: Address, } +impl

Clone for L1State

{ + fn clone(&self) -> Self { + Self { + inner: Rc::clone(&self.inner), + } + } +} + impl

L1State

{ /// Creates execution-local L1 state backed by `provider` for `portal_address`. pub fn new(provider: P, portal_address: Address) -> Self { Self { - anchor: Rc::new(Cell::new(None)), - access_set: Rc::new(RefCell::new(HashSet::default())), - provider, - portal_address, + inner: Rc::new(L1StateInner { + anchor: Cell::new(None), + access_set: RefCell::new(HashSet::default()), + provider, + portal_address, + }), } } /// Clears bookkeeping after the current transaction attempt completes. pub fn reset_transaction_state(&self) { - self.anchor.set(None); - self.access_set.borrow_mut().clear(); + self.inner.anchor.set(None); + self.inner.access_set.borrow_mut().clear(); } /// Returns the anchor selected for the current transaction, if any. pub fn get_anchor(&self) -> Option { - self.anchor.get() + self.inner.anchor.get() } /// Returns the configured ZonePortal address. - pub const fn portal(&self) -> Address { - self.portal_address + pub fn portal(&self) -> Address { + self.inner.portal_address } fn set_anchor(&self, new: u64) -> Result<(), L1StateError> { match self.get_anchor() { None => { - self.anchor.set(Some(new)); + self.inner.anchor.set(Some(new)); Ok(()) } Some(current) if current == new => Ok(()), @@ -125,7 +141,7 @@ impl

L1State

{ return Err(L1StateError::AnchorConflict { current, new: to }); } - self.anchor.set(Some(to)); + self.inner.anchor.set(Some(to)); Ok(()) } } @@ -143,7 +159,9 @@ impl L1State

{ block_number: u64, ) -> Result { self.set_anchor(block_number)?; - self.provider.read_l1_storage(account, slot, block_number) + self.inner + .provider + .read_l1_storage(account, slot, block_number) } /// Reads L1 storage, with gas metering, after selecting or validating `block_number` as this @@ -155,7 +173,7 @@ impl L1State

{ block_number: u64, ) -> tempo_precompiles::Result { let key = (account, slot); - let gas_cost = if self.access_set.borrow().contains(&key) { + let gas_cost = if self.inner.access_set.borrow().contains(&key) { WARM_STORAGE_READ_COST } else { COLD_SLOAD_COST @@ -164,7 +182,7 @@ impl L1State

{ let value = self .read_l1_storage_unmetered(account, slot, block_number) .map_err(|err| TempoPrecompileError::Fatal(err.to_string()))?; - self.access_set.borrow_mut().insert(key); + self.inner.access_set.borrow_mut().insert(key); Ok(value) } @@ -185,7 +203,7 @@ impl L1State

{ &self, select_slot: impl for<'a> FnOnce(&'a ZonePortal) -> &'a Slot, ) -> tempo_precompiles::Result { - let portal = ZonePortal::new(self.portal_address); + let portal = ZonePortal::new(self.portal()); self.read_l1(select_slot(&portal)) } @@ -203,8 +221,8 @@ impl

fmt::Debug for L1State

{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("L1State") .field("anchor", &self.get_anchor()) - .field("warm_l1_slots", &self.access_set.borrow().len()) - .field("portal_address", &self.portal_address) + .field("warm_l1_slots", &self.inner.access_set.borrow().len()) + .field("portal_address", &self.portal()) .finish_non_exhaustive() } }