Skip to content
Merged
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ tracing.workspace = true

[dev-dependencies]
eyre.workspace = true
reth-chainspec.workspace = true
tempo-precompiles = { workspace = true, features = ["test-utils"] }
89 changes: 79 additions & 10 deletions crates/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use reth_evm::{
use reth_primitives_traits::{SealedBlock, SealedHeader};
use std::{cell::RefCell, rc::Rc, sync::Arc};
use tempo_alloy::TempoNetwork;
use tempo_chainspec::TempoChainSpec;
use tempo_chainspec::{TempoChainSpec, hardfork::TempoHardfork, spec::TempoHardforks};
use tempo_evm::{
TempoBlockAssembler, TempoBlockEnv, TempoBlockExecutionCtx, TempoEvmConfig, TempoEvmError,
TempoHaltReason, TempoNextBlockEnvAttributes,
Expand Down Expand Up @@ -238,7 +238,7 @@ impl BlockAssembler<ZoneEvmConfig> for ZoneBlockAssembler {
}
}

/// Zone EVM configuration — wraps [`TempoEvmConfig`] with a [`ZoneEvmFactory`].
/// Zone EVM configuration with Zone precompiles and parent Tempo hardfork conditions.
#[derive(Debug, Clone)]
pub struct ZoneEvmConfig {
inner: TempoEvmConfig,
Expand All @@ -247,9 +247,29 @@ pub struct ZoneEvmConfig {
}

impl ZoneEvmConfig {
/// Create a new zone EVM config with the given chain spec, L1 state
/// provider.
pub fn new(chain_spec: Arc<TempoChainSpec>, l1_provider: L1StateProvider) -> Self {
/// Creates a Zone EVM config using Tempo hardfork conditions from the parent L1 spec.
pub fn new(
zone_chain_spec: Arc<TempoChainSpec>,
tempo_chain_spec: Arc<TempoChainSpec>,
l1_provider: L1StateProvider,
) -> Self {
let chain_spec = Self::compose_chain_spec(&zone_chain_spec, &tempo_chain_spec);
Self::from_chain_spec(chain_spec, l1_provider)
}

/// Copies the Zone chain spec and applies the Tempo hardfork conditions from its parent chain.
fn compose_chain_spec(zone: &TempoChainSpec, tempo: &TempoChainSpec) -> Arc<TempoChainSpec> {
let mut chain_spec = zone.clone();
for &hardfork in TempoHardfork::VARIANTS {
chain_spec
.inner
.hardforks
.insert(hardfork, tempo.tempo_fork_activation(hardfork));
}
Arc::new(chain_spec)
}
Comment thread
0xKitsune marked this conversation as resolved.

fn from_chain_spec(chain_spec: Arc<TempoChainSpec>, l1_provider: L1StateProvider) -> Self {
let zone_factory = ZoneEvmFactory::new(l1_provider);
let inner = TempoEvmConfig::new(chain_spec.clone());
let block_assembler = ZoneBlockAssembler::new(chain_spec);
Expand All @@ -260,11 +280,12 @@ impl ZoneEvmConfig {
}
}

/// Create a zone EVM config without a usable L1 provider.
/// Creates a Zone EVM config without a usable L1 provider.
///
/// Intended for CLI subcommands (import, stage, re-execute) that need a type-compatible
/// EVM config but don't have access to an L1 RPC connection. The portal address defaults to
/// the zero address in this mode, so sequencer reads are treated as unavailable.
/// EVM config but don't have access to an L1 RPC connection. Tempo hardfork conditions come
/// from `chain_spec` because the parent L1 spec cannot be resolved in this mode. The portal
/// address defaults to zero, so sequencer reads are unavailable.
pub fn new_without_l1(chain_spec: Arc<TempoChainSpec>) -> Self {
let cache = L1StateCache::default();
let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
Expand All @@ -273,7 +294,7 @@ impl ZoneEvmConfig {
let runtime_handle = tokio::runtime::Handle::current();
let config = L1StateProviderConfig::default();
let l1_provider = L1StateProvider::new_raw(config, cache, provider, runtime_handle);
Self::new(chain_spec, l1_provider)
Self::from_chain_spec(chain_spec, l1_provider)
}

/// Set the policy provider for the TIP-403 proxy precompile.
Expand All @@ -282,7 +303,7 @@ impl ZoneEvmConfig {
self
}

/// Returns the chain spec.
/// Returns the chain spec used by Tempo execution.
pub fn chain_spec(&self) -> &Arc<TempoChainSpec> {
self.inner.chain_spec()
}
Expand Down Expand Up @@ -403,3 +424,51 @@ impl ConfigureEngineEvm<TempoExecutionData> for ZoneEvmConfig {
self.inner.tx_iterator_for_payload(payload)
}
}

#[cfg(test)]
mod tests {
use super::*;
use reth_chainspec::{EthChainSpec, ForkCondition};
use tempo_chainspec::spec::{DEV, MODERATO};

#[test]
fn composed_chain_spec_uses_zone_identity_and_parent_tempo_forks() {
let composed = ZoneEvmConfig::compose_chain_spec(&DEV, &MODERATO);

assert_eq!(composed.chain().id(), DEV.chain().id());
assert_eq!(composed.genesis_hash(), DEV.genesis_hash());
for &hardfork in TempoHardfork::VARIANTS {
assert_eq!(
composed.tempo_fork_activation(hardfork),
MODERATO.tempo_fork_activation(hardfork)
);
}
}

#[test]
fn tempo_evm_selects_parent_fork_from_zone_block_timestamp() {
let composed = ZoneEvmConfig::compose_chain_spec(&DEV, &MODERATO);
let activation_timestamp = TempoHardfork::VARIANTS
.iter()
.find_map(|&hardfork| match MODERATO.tempo_fork_activation(hardfork) {
ForkCondition::Timestamp(timestamp) if timestamp > 0 => Some(timestamp),
_ => None,
})
.expect("Moderato must have a post-genesis Tempo hardfork");
let header = TempoHeader {
inner: alloy_consensus::Header {
timestamp: activation_timestamp,
..Default::default()
},
..Default::default()
};

let config = TempoEvmConfig::new(composed);
let env = config.evm_env(&header).expect("valid EVM environment");

assert_eq!(
env.cfg_env.spec,
MODERATO.tempo_hardfork_at(activation_timestamp)
);
}
}
15 changes: 15 additions & 0 deletions crates/l1/src/state/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use crate::{abi::PORTAL_SEQUENCER_SLOT, rpc::rpc_connection_config};
/// Configuration for the [`L1StateProvider`].
#[derive(Debug, Clone)]
pub struct L1StateProviderConfig {
/// Optional known L1 chain ID, avoiding an RPC lookup when configured.
pub chain_id: Option<u64>,
/// HTTP RPC endpoint for Tempo L1.
pub l1_rpc_url: String,
/// Zone portal address on Tempo L1, used for sequencer lookups.
Expand All @@ -44,6 +46,7 @@ pub struct L1StateProviderConfig {
impl Default for L1StateProviderConfig {
fn default() -> Self {
Self {
chain_id: None,
l1_rpc_url: String::new(),
portal_address: Address::ZERO,
max_retries: 10,
Expand Down Expand Up @@ -71,6 +74,8 @@ impl Default for L1StateProviderConfig {
/// `spawn_blocking`). Calling it from within an async task on the same runtime will panic.
#[derive(Debug, Clone)]
pub struct L1StateProvider {
/// Known L1 chain ID, if configured.
chain_id: Option<u64>,
/// In-memory cache of L1 contract storage slots, checked before any RPC call.
cache: L1StateCache,
/// Zone portal address on Tempo L1 used for sequencer lookups.
Expand All @@ -84,6 +89,14 @@ pub struct L1StateProvider {
}

impl L1StateProvider {
/// Returns the chain ID reported by the configured L1 provider.
pub async fn chain_id(&self) -> Result<u64> {
match self.chain_id {
Some(chain_id) => Ok(chain_id),
None => Ok(self.provider.get_chain_id().await?),
}
}

/// Create a new provider.
///
/// The provider is created eagerly from [`L1StateProviderConfig::l1_rpc_url`] and reused
Expand Down Expand Up @@ -116,6 +129,7 @@ impl L1StateProvider {
.erased();

Ok(Self {
chain_id: config.chain_id,
cache,
portal_address: config.portal_address,
provider,
Expand All @@ -134,6 +148,7 @@ impl L1StateProvider {
runtime_handle: tokio::runtime::Handle,
) -> Self {
Self {
chain_id: config.chain_id,
cache,
portal_address: config.portal_address,
provider,
Expand Down
38 changes: 34 additions & 4 deletions crates/node/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use reth_transaction_pool::{
};
use std::{collections::HashSet, sync::Arc, time::Duration};
use tempo_alloy::TempoNetwork;
use tempo_chainspec::spec::TempoChainSpec;
use tempo_chainspec::spec::{DEV, TempoChainSpec, chainspec_from_chain_id};
use tempo_evm::TempoEvmConfig;
use tempo_node::{
DEFAULT_AA_VALID_AFTER_MAX_SECS, engine::TempoEngineValidator, rpc::TempoEthApiBuilder,
Expand Down Expand Up @@ -71,6 +71,16 @@ use zone_payload::{
};
use zone_sequencer::{BatchAnchorConfig, ZoneSequencerConfig, spawn_zone_sequencer};

/// Returns a known Tempo chain spec for an L1 chain ID.
///
/// Tempo Anvil uses chain ID 31337 and the same hardfork schedule as Tempo DEV (1337).
fn tempo_chain_spec_for_l1(chain_id: u64) -> Option<Arc<TempoChainSpec>> {
chainspec_from_chain_id(chain_id).or_else(|| match chain_id {
1337 | 31337 => Some(DEV.clone()),
_ => None,
})
}

/// Network primitives for Zone Nodes
type ZoneNetworkPrimitives = BasicNetworkPrimitives<TempoPrimitives, TempoTxEnvelope>;

Expand Down Expand Up @@ -258,6 +268,12 @@ impl ZoneNode {
self
}

/// Set the parent L1 chain ID, avoiding a startup RPC lookup.
pub fn with_l1_chain_id(mut self, chain_id: u64) -> Self {
self.l1_state_provider_config.chain_id = Some(chain_id);
self
}

/// Set the number of zone blocks between empty withdrawal batch
/// finalization.
pub fn with_withdrawal_batch_interval_blocks(mut self, interval_blocks: u64) -> Self {
Expand Down Expand Up @@ -881,7 +897,11 @@ where
)
.await?;

let mut evm_config = ZoneEvmConfig::new(ctx.chain_spec(), l1_provider);
let l1_chain_id = l1_provider.chain_id().await?;
let tempo_chain_spec = tempo_chain_spec_for_l1(l1_chain_id)
.ok_or_else(|| eyre::eyre!("unsupported parent Tempo chain ID {l1_chain_id}"))?;
// Keep the Zone chain settings and use the parent L1 schedule for Tempo hardforks.
let mut evm_config = ZoneEvmConfig::new(ctx.chain_spec(), tempo_chain_spec, l1_provider);

// Create PolicyProvider for the TIP-403 proxy precompile.
let policy_l1 = alloy_provider::ProviderBuilder::new_with_network::<TempoNetwork>()
Expand Down Expand Up @@ -930,14 +950,14 @@ where
async fn build_pool(
self,
ctx: &BuilderContext<Node>,
_evm_config: ZoneEvmConfig,
evm_config: ZoneEvmConfig,
) -> eyre::Result<Self::Pool> {
let mut pool_config = ctx.pool_config();
pool_config.max_inflight_delegated_slot_limit = pool_config.max_account_slots;

// this store is effectively a noop
let blob_store = InMemoryBlobStore::default();
let tempo_evm_config = TempoEvmConfig::new(ctx.chain_spec());
let tempo_evm_config = TempoEvmConfig::new(evm_config.chain_spec().clone());
let additional_tasks = ctx.config().txpool.additional_validation_tasks;
let task_executor = ctx.task_executor().clone();
let mut validator = TransactionValidationTaskExecutor::eth_builder(
Expand Down Expand Up @@ -1013,6 +1033,7 @@ mod tests {
use super::*;
use alloy_consensus::{Signed, TxEip1559};
use alloy_primitives::{Bytes, Signature, TxKind, U256};
use reth_chainspec::EthChainSpec;
use reth_primitives_traits::Recovered;
use tempo_primitives::transaction::{
AASigned, Call, PrimitiveSignature, TempoSignature, TempoTransaction,
Expand All @@ -1035,6 +1056,15 @@ mod tests {
)
}

#[test]
fn resolves_public_and_local_tempo_l1_specs() {
assert_eq!(tempo_chain_spec_for_l1(4217).unwrap().chain().id(), 4217);
assert_eq!(tempo_chain_spec_for_l1(42431).unwrap().chain().id(), 42431);
assert_eq!(tempo_chain_spec_for_l1(1337).unwrap().chain().id(), 1337);
assert_eq!(tempo_chain_spec_for_l1(31337).unwrap().chain().id(), 1337);
assert!(tempo_chain_spec_for_l1(999_999).is_none());
}

#[test]
fn pool_policy_allows_allowlisted_plain_create() {
let sender = Address::repeat_byte(0x11);
Expand Down
13 changes: 12 additions & 1 deletion crates/node/tests/assets/test-genesis.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@
"terminalTotalDifficultyPassed": true,
"epochLength": 302400,
"t0Time": 0,
"t1Time": 0,
"t1aTime": 0,
"t1bTime": 0,
"t1cTime": 0,
"t2Time": 0,
"t3Time": 0,
"t4Time": 0,
"t5Time": 0,
"t6Time": 0,
"t7Time": 0,
"t8Time": 0,
"t9Time": 0,
"depositContractAddress": "0x0000000000000000000000000000000000000000"
},
"nonce": "0x42",
Expand Down Expand Up @@ -372,4 +383,4 @@
}
},
"baseFeePerGas": "0x2540be400"
}
}
10 changes: 8 additions & 2 deletions crates/node/tests/it/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use crate::utils::{
local_dev_zone_account, poll_until, seed_fixture_for_zone, start_local_zone_with_fixture,
};

const CONTRACT_CREATION_TX_GAS: u64 = 1_000_000;

/// Self-contained test: inject a deposit via the queue and verify the zone
/// mints the corresponding pathUSD balance on L2.
///
Expand Down Expand Up @@ -84,7 +86,7 @@ async fn test_contract_creation_transaction_is_rejected() -> eyre::Result<()> {

let mut request = TransactionRequest::default().input(Bytes::from_static(&[0x00]).into());
request.to = Some(TxKind::Create);
request.gas = Some(100_000);
request.gas = Some(CONTRACT_CREATION_TX_GAS);
request.gas_price = Some(TEMPO_T0_BASE_FEE as u128);

let err = provider
Expand Down Expand Up @@ -554,7 +556,11 @@ async fn submit_withdrawal(
.await?;
fixture.inject_empty_block(zone.deposit_queue());
let receipt = pending.get_receipt().await?;
assert!(receipt.status(), "withdrawal should succeed");
assert!(
receipt.status(),
"withdrawal should succeed (gas used: {})",
receipt.gas_used
);
receipt
.block_number
.ok_or_else(|| eyre::eyre!("withdrawal receipt missing block number"))
Expand Down
11 changes: 6 additions & 5 deletions crates/node/tests/it/private_rpc_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
//! - Method tier enforcement (restricted/disabled/unknown methods)

use crate::utils::{
DEFAULT_TIMEOUT, TEST_MNEMONIC, ZoneAccount, now_secs, start_zone_with_private_rpc,
start_zone_with_private_rpc_l1, start_zone_with_private_rpc_l1_with_encryption,
DEFAULT_TIMEOUT, TEST_MNEMONIC, TIP20_TX_GAS, ZoneAccount, now_secs,
start_zone_with_private_rpc, start_zone_with_private_rpc_l1,
start_zone_with_private_rpc_l1_with_encryption,
};
use alloy::{
primitives::{Address, B256, U256, address, hex},
Expand Down Expand Up @@ -558,7 +559,7 @@ async fn test_tip20_eth_call_privacy() -> eyre::Result<()> {
let approve_pending = ContractTip20::new(PATH_USD_ADDRESS, &owner_provider)
.approve(spender, U256::from(allowance_amount))
.gas_price(TEMPO_T0_BASE_FEE as u128)
.gas(150_000)
.gas(TIP20_TX_GAS)
.send()
.await?;
ctx.fixture.inject_empty_block(ctx.zone.deposit_queue());
Expand Down Expand Up @@ -1016,13 +1017,13 @@ async fn test_ws_logs_subscription_is_sender_scoped() -> eyre::Result<()> {
let owner_pending = ContractTip20::new(PATH_USD_ADDRESS, &owner_provider)
.approve(spender, U256::from(111u64))
.gas_price(TEMPO_T0_BASE_FEE as u128)
.gas(150_000)
.gas(TIP20_TX_GAS)
.send()
.await?;
let outsider_pending = ContractTip20::new(PATH_USD_ADDRESS, &outsider_provider)
.approve(spender, U256::from(222u64))
.gas_price(TEMPO_T0_BASE_FEE as u128)
.gas(150_000)
.gas(TIP20_TX_GAS)
.send()
.await?;

Expand Down
Loading
Loading