Skip to content
Closed
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
18 changes: 7 additions & 11 deletions crates/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -89,18 +89,14 @@ where
zone_hardfork: zone_hardfork::ZoneHardfork,
) -> TempoEvm<L1OverlayDB<DB, L1>, 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
}
}
Expand Down
42 changes: 30 additions & 12 deletions crates/precompiles/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<TempoHardfork>,
inner: Rc<ZonePrecompileEnvInner>,
}

/// 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<RefCell<NonCreditableSlots>>,
Expand All @@ -55,16 +69,20 @@ impl ZonePrecompileEnv {
non_creditable_slots: Rc<RefCell<NonCreditableSlots>>,
) -> 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
}
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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());
Expand Down
72 changes: 58 additions & 14 deletions crates/precompiles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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<P>(
precompiles: &mut PrecompilesMap,
cfg: &CfgEnv<TempoHardfork>,
zone_hardfork: ZoneHardfork,
env: ZonePrecompileEnv,
l1: L1State<P>,
actions: StorageActions,
non_creditable_slots: Rc<RefCell<NonCreditableSlots>>,
) 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 {
Expand Down Expand Up @@ -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::<TestContext>::warm_addresses(&precompiles).clone();

let env = ZonePrecompileEnv::new(
&CfgEnv::<TempoHardfork>::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::<TestContext>::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");
}
}
}
58 changes: 38 additions & 20 deletions crates/precompiles/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<P> {
inner: Rc<L1StateInner<P>>,
}

struct L1StateInner<P> {
/// Tempo block number selected for the current transaction attempt.
anchor: Rc<Cell<Option<u64>>>,
anchor: Cell<Option<u64>>,
/// `(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<RefCell<HashSet<(Address, B256)>>>,
access_set: RefCell<HashSet<(Address, B256)>>,
/// 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<P> Clone for L1State<P> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}

impl<P> L1State<P> {
/// 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<u64> {
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(()),
Expand All @@ -125,7 +141,7 @@ impl<P> L1State<P> {
return Err(L1StateError::AnchorConflict { current, new: to });
}

self.anchor.set(Some(to));
self.inner.anchor.set(Some(to));
Ok(())
}
}
Expand All @@ -143,7 +159,9 @@ impl<P: L1StorageReader> L1State<P> {
block_number: u64,
) -> Result<B256, L1StateError> {
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
Expand All @@ -155,7 +173,7 @@ impl<P: L1StorageReader> L1State<P> {
block_number: u64,
) -> tempo_precompiles::Result<B256> {
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
Expand All @@ -164,7 +182,7 @@ impl<P: L1StorageReader> L1State<P> {
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)
}

Expand All @@ -185,7 +203,7 @@ impl<P: L1StorageReader> L1State<P> {
&self,
select_slot: impl for<'a> FnOnce(&'a ZonePortal) -> &'a Slot<T>,
) -> tempo_precompiles::Result<T> {
let portal = ZonePortal::new(self.portal_address);
let portal = ZonePortal::new(self.portal());
self.read_l1(select_slot(&portal))
}

Expand All @@ -203,8 +221,8 @@ impl<P> fmt::Debug for L1State<P> {
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()
}
}
Expand Down
Loading