From 7210f00321a38269078f73c28010530f3c52301b Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xKitsune@protonmail.com> Date: Wed, 24 Jun 2026 20:50:21 -0400 Subject: [PATCH 1/2] refactor(node): move node assembly in to zone-node crate --- Cargo.lock | 60 +- Cargo.toml | 2 + crates/node/Cargo.toml | 91 +++ crates/node/src/cli.rs | 201 ++++++ crates/node/src/engine.rs | 291 ++++++++ crates/node/src/lib.rs | 17 + crates/node/src/node.rs | 884 ++++++++++++++++++++++++ crates/node/src/rpc.rs | 1144 +++++++++++++++++++++++++++++++ crates/tempo-zone/Cargo.toml | 97 +-- crates/tempo-zone/src/cli.rs | 201 +----- crates/tempo-zone/src/engine.rs | 297 +------- crates/tempo-zone/src/lib.rs | 2 - crates/tempo-zone/src/node.rs | 883 +----------------------- crates/tempo-zone/src/rpc.rs | 1142 +----------------------------- 14 files changed, 2723 insertions(+), 2589 deletions(-) create mode 100644 crates/node/Cargo.toml create mode 100644 crates/node/src/cli.rs create mode 100644 crates/node/src/engine.rs create mode 100644 crates/node/src/lib.rs create mode 100644 crates/node/src/node.rs create mode 100644 crates/node/src/rpc.rs diff --git a/Cargo.lock b/Cargo.lock index 857d27a0..903eced4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13311,7 +13311,6 @@ dependencies = [ "alloy-signer-local", "alloy-sol-types", "base64 0.22.1", - "clap", "const-hex", "eyre", "futures", @@ -13320,11 +13319,8 @@ dependencies = [ "rand 0.8.6", "reqwest 0.13.4", "reth-chainspec", - "reth-consensus", "reth-eth-wire-types", "reth-ethereum", - "reth-evm", - "reth-metrics", "reth-node-api", "reth-node-builder", "reth-node-core", @@ -13332,7 +13328,6 @@ dependencies = [ "reth-payload-primitives", "reth-primitives-traits", "reth-provider", - "reth-rpc", "reth-rpc-builder", "reth-rpc-eth-api", "reth-rpc-eth-types", @@ -13362,9 +13357,9 @@ dependencies = [ "url", "zone-evm", "zone-l1", + "zone-node", "zone-payload", "zone-primitives", - "zone-rpc", "zone-sequencer", ] @@ -13441,6 +13436,59 @@ dependencies = [ "zone-primitives", ] +[[package]] +name = "zone-node" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-signer-local", + "alloy-sol-types", + "clap", + "eyre", + "futures", + "k256", + "reth-chainspec", + "reth-consensus", + "reth-eth-wire-types", + "reth-ethereum", + "reth-node-api", + "reth-node-builder", + "reth-payload-builder", + "reth-payload-primitives", + "reth-primitives-traits", + "reth-provider", + "reth-rpc", + "reth-rpc-builder", + "reth-rpc-eth-api", + "reth-rpc-eth-types", + "reth-storage-api", + "reth-tasks", + "reth-tracing", + "reth-transaction-pool", + "tempo-alloy", + "tempo-chainspec", + "tempo-contracts", + "tempo-evm", + "tempo-node", + "tempo-primitives", + "tempo-transaction-pool", + "tempo-zone-contracts", + "tokio", + "tracing", + "zone-evm", + "zone-l1", + "zone-payload", + "zone-primitives", + "zone-rpc", + "zone-sequencer", +] + [[package]] name = "zone-payload" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 85b74d13..b46822a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/contracts", "crates/evm", "crates/l1", + "crates/node", "crates/payload", "crates/precompiles", "crates/primitives", @@ -107,6 +108,7 @@ tempo-transaction-pool = { git = "https://github.com/tempoxyz/tempo", rev = "651 tempo-zone-contracts = { path = "crates/contracts", default-features = false } zone-evm = { path = "crates/evm" } zone-l1 = { path = "crates/l1" } +zone-node = { path = "crates/node" } zone-payload = { path = "crates/payload" } zone-precompiles = { path = "crates/precompiles", default-features = false } zone-primitives = { path = "crates/primitives", default-features = false } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml new file mode 100644 index 00000000..c25a7c00 --- /dev/null +++ b/crates/node/Cargo.toml @@ -0,0 +1,91 @@ +[package] +name = "zone-node" +description = "Tempo Zone node assembly" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +# tempo +tempo-chainspec.workspace = true +tempo-evm = { workspace = true, features = ["rpc", "engine"] } +tempo-node.workspace = true +tempo-primitives.workspace = true +tempo-transaction-pool.workspace = true +tempo-alloy.workspace = true +tempo-contracts.workspace = true +tempo-zone-contracts = { workspace = true, features = ["std", "serde", "rpc"] } +zone-evm.workspace = true +zone-l1.workspace = true +zone-payload.workspace = true +zone-primitives = { workspace = true, features = ["std", "serde"] } +zone-rpc.workspace = true +zone-sequencer.workspace = true + +# reth +reth-chainspec.workspace = true +reth-consensus = { workspace = true, optional = true } +reth-eth-wire-types.workspace = true +reth-ethereum = { workspace = true, features = [ + "node", + "cli", +], optional = true } +reth-node-api.workspace = true +reth-node-builder.workspace = true +reth-payload-builder.workspace = true +reth-payload-primitives.workspace = true +reth-primitives-traits.workspace = true +reth-provider.workspace = true +reth-rpc.workspace = true +reth-rpc-builder.workspace = true +reth-rpc-eth-api.workspace = true +reth-rpc-eth-types.workspace = true +reth-storage-api.workspace = true +reth-tasks.workspace = true +reth-tracing = { workspace = true, optional = true } +reth-transaction-pool.workspace = true + +# alloy +alloy-consensus.workspace = true +alloy-network.workspace = true +alloy-primitives.workspace = true +alloy-provider.workspace = true +alloy-rpc-client.workspace = true +alloy-rpc-types-engine.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-signer-local.workspace = true +alloy-sol-types.workspace = true + +# misc +clap = { workspace = true, optional = true } +eyre.workspace = true +futures.workspace = true +k256.workspace = true +tokio.workspace = true +tracing.workspace = true + +[features] +default = [] +cli = [ + "dep:clap", + "dep:reth-consensus", + "dep:reth-ethereum", + "dep:reth-tracing", + "tempo-chainspec/cli", +] +jemalloc = ["reth-ethereum?/jemalloc"] +test-utils = [ + "reth-chainspec/test-utils", + "reth-ethereum?/test-utils", + "reth-node-builder/test-utils", + "reth-payload-builder/test-utils", + "reth-primitives-traits/test-utils", + "reth-provider/test-utils", + "reth-transaction-pool/test-utils", + "tempo-transaction-pool/test-utils", +] diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs new file mode 100644 index 00000000..25731b2f --- /dev/null +++ b/crates/node/src/cli.rs @@ -0,0 +1,201 @@ +//! Tempo Zone CLI. + +use std::{sync::Arc, time::Duration}; + +use alloy_primitives::Address; +use alloy_signer_local::PrivateKeySigner; +use clap::{Args, Parser}; +use reth_consensus::noop::NoopConsensus; +use reth_ethereum::cli::Cli; +use reth_tracing::tracing::info; +use tempo_chainspec::spec::{TempoChainSpec, TempoChainSpecParser}; +use zone_evm::ZoneEvmConfig; + +use crate::{ + ZoneNode, ZonePrivateRpcConfig, ZoneSequencerAddOnsConfig, + rpc::auth::DEFAULT_MAX_AUTH_TOKEN_VALIDITY_SECS, +}; +use zone_sequencer::BatchAnchorConfig; + +const MAX_LOGS_PER_RESPONSE: u64 = 1_000_000; +const MAX_BLOCKS_PER_FILTER: u64 = 1_000_000; + +const ZONE_LOG_FILTER_DIRECTIVES: &str = concat!( + "tungstenite=warn,", + "alloy_pubsub=warn,", + "alloy_transport_ws=warn,", + "rustls::client=warn" +); + +/// Tempo Zone CLI entry point. +pub struct ZoneCli(Cli); + +impl ZoneCli { + /// Parse CLI arguments from the environment. + pub fn parse() -> Self { + Self(Cli::parse()) + } + + /// Run the Tempo Zone node. + /// + /// Configures the node builder, launches the zone node with all sequencer + /// background tasks, and blocks until exit. + pub fn run(self) -> eyre::Result<()> { + let mut cli = self.0; + + prepend_log_filter(&mut cli.logs.log_stdout_filter, ZONE_LOG_FILTER_DIRECTIVES); + prepend_log_filter(&mut cli.logs.log_file_filter, ZONE_LOG_FILTER_DIRECTIVES); + + let components = |spec: Arc| { + ( + ZoneEvmConfig::new_without_l1(spec), + NoopConsensus::default(), + ) + }; + + cli.run_with_components::(components, async move |mut builder, args| { + info!(target: "reth::cli", "Launching Tempo Zone node"); + + builder.config_mut().network.discovery.disable_discovery = true; + builder.config_mut().rpc.disable_auth_server = true; + builder.config_mut().rpc.rpc_max_logs_per_response = MAX_LOGS_PER_RESPONSE.into(); + builder.config_mut().rpc.rpc_max_blocks_per_filter = MAX_BLOCKS_PER_FILTER.into(); + + let mut node = ZoneNode::new( + args.l1_rpc_url, + args.portal_address, + args.l1_genesis_block_number, + args.l1_fetch_concurrency, + Duration::from_millis(args.l1_retry_connection_interval_ms), + ) + .with_private_rpc(ZonePrivateRpcConfig { + private_rpc_port: args.private_rpc_port, + zone_id: args.zone_id, + max_auth_token_validity: Duration::from_secs( + args.private_rpc_max_auth_token_validity_secs, + ), + }); + + if args.enable_sequencer { + let sequencer_signer: PrivateKeySigner = args + .sequencer_key + .parse() + .expect("invalid sequencer private key"); + node = node.with_sequencer(ZoneSequencerAddOnsConfig { + sequencer_signer, + zone_id: args.zone_id, + zone_poll_interval: Duration::from_secs(args.zone_poll_interval_secs), + batch_interval: Duration::from_secs(args.zone_batch_interval_secs), + batch_anchor_config: BatchAnchorConfig::default(), + withdrawal_poll_interval: Duration::from_secs( + args.withdrawal_poll_interval_secs, + ), + }); + } + + let handle = builder.node(node).launch_with_debug_capabilities().await?; + handle.wait_for_node_exit().await + }) + } +} + +/// Tempo Zone CLI arguments. +#[derive(Debug, Clone, Args)] +pub struct ZoneArgs { + /// L1 WebSocket RPC URL for subscribing to deposit events and chain notifications. + #[arg(long = "l1.rpc-url", env = "L1_RPC_URL")] + pub l1_rpc_url: String, + + /// ZonePortal contract address on L1. + #[arg(long = "l1.portal-address", env = "L1_PORTAL_ADDRESS")] + pub portal_address: Address, + + /// Block building interval in milliseconds. + #[arg( + long = "block.interval-ms", + env = "BLOCK_INTERVAL_MS", + default_value_t = 250 + )] + pub block_interval_ms: u64, + + /// Sequencer private key (hex, with or without 0x prefix). + #[arg(long = "sequencer-key", env = "SEQUENCER_KEY")] + pub sequencer_key: String, + + /// How often (in seconds) the zone monitor polls for new L2 blocks. + #[arg( + long = "zone.poll-interval-secs", + env = "ZONE_POLL_INTERVAL_SECS", + default_value_t = 1 + )] + pub zone_poll_interval_secs: u64, + + /// Maximum time (in seconds) to accumulate zone blocks before submitting a batch to L1. + #[arg( + long = "zone.batch-interval-secs", + env = "ZONE_BATCH_INTERVAL_SECS", + default_value_t = 60 + )] + pub zone_batch_interval_secs: u64, + + /// How often (in seconds) the withdrawal processor polls the L1 queue. + #[arg( + long = "withdrawal-poll-interval-secs", + env = "WITHDRAWAL_POLL_INTERVAL_SECS", + default_value_t = 5 + )] + pub withdrawal_poll_interval_secs: u64, + + /// Genesis Tempo L1 block number override. + #[arg(long = "l1.genesis-block-number", env = "L1_GENESIS_BLOCK_NUMBER")] + pub l1_genesis_block_number: Option, + + /// Maximum number of concurrent L1 receipt fetches. + #[arg( + long = "l1.fetch-concurrency", + env = "L1_FETCH_CONCURRENCY", + default_value_t = 4 + )] + pub l1_fetch_concurrency: usize, + + /// Interval in milliseconds between WebSocket reconnection attempts to L1. + #[arg( + long = "l1.retry-connection-interval", + env = "L1_RETRY_CONNECTION_INTERVAL_MS", + default_value_t = 100 + )] + pub l1_retry_connection_interval_ms: u64, + + /// Zone ID for the private RPC auth token validation. + #[arg(long = "zone.id", env = "ZONE_ID", default_value_t = 0)] + pub zone_id: u32, + + /// Port for the private zone RPC server (0 for OS-assigned). + #[arg( + long = "private-rpc.port", + env = "PRIVATE_RPC_PORT", + default_value_t = 8544 + )] + pub private_rpc_port: u16, + + /// Maximum auth token validity window the private RPC accepts, in seconds. + #[arg( + long = "private-rpc.max-auth-token-validity-secs", + env = "PRIVATE_RPC_MAX_AUTH_TOKEN_VALIDITY_SECS", + default_value_t = DEFAULT_MAX_AUTH_TOKEN_VALIDITY_SECS + )] + pub private_rpc_max_auth_token_validity_secs: u64, + + /// Enable the Zone node in sequencer mode. This advances block production and submits + /// withdrawal batches. + #[arg(long = "sequencer", env = "SEQUENCER")] + pub enable_sequencer: bool, +} + +fn prepend_log_filter(filter: &mut String, directives: &str) { + if filter.is_empty() { + *filter = directives.to_owned(); + } else { + *filter = format!("{directives},{filter}"); + } +} diff --git a/crates/node/src/engine.rs b/crates/node/src/engine.rs new file mode 100644 index 00000000..7c268929 --- /dev/null +++ b/crates/node/src/engine.rs @@ -0,0 +1,291 @@ +//! Zone Engine — L1-event-driven block production for zone nodes. +//! +//! Advances the zone chain whenever new L1 blocks arrive in the deposit +//! queue, enabling full-speed sync during catch-up and instant reaction in +//! steady state. +//! +//! ## Block production flow +//! +//! ```text +//! L1Subscriber ──enqueue──► DepositQueue ──notify──► ZoneEngine +//! │ │ +//! │ 1. peek queue → L1 block +//! │ 2. build ZonePayloadAttributes +//! │ (inner attrs + l1_block) +//! │ 3. FCU w/ payload attributes +//! │ │ +//! │ ▼ +//! │ reth payload service +//! │ │ +//! │ 4. build payload +//! │ (L1 data from attributes) +//! │ │ +//! │ ▼ +//! │ ZoneEngine +//! │ 5. resolve payload +//! │ 6. newPayload +//! │ 7. FCU (update head) +//! │ │ +//! ◄── confirm ◄───────────┘ +//! ``` +//! +//! The deposit queue uses a **peek / confirm** pattern: the engine peeks at +//! the next L1 block, wraps it into [`ZonePayloadAttributes`], and only +//! confirms (removes) the block after `newPayload` succeeds. A failed build +//! leaves the block in the queue for retry. +//! +//! The zone assumes **instant finality** — head, safe, and finalized all point +//! to the same block. + +use alloy_consensus::BlockHeader as _; +use alloy_primitives::{Address, B256}; +use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes as EthPayloadAttributes}; +use eyre::OptionExt; +use reth_chainspec::EthereumHardforks; +use reth_node_builder::ConsensusEngineHandle; +use reth_payload_builder::PayloadBuilderHandle; +use reth_payload_primitives::{BuiltPayload, PayloadKind, PayloadTypes}; +use reth_primitives_traits::SealedHeader; +use std::{sync::Arc, time::Duration}; +use tempo_chainspec::spec::TempoChainSpec; +use tempo_primitives::TempoHeader; +use tracing::{error, warn}; + +use zone_l1::{DepositQueue, L1BlockDeposits, PolicyProvider, PreparedL1Block}; +use zone_payload::{ZonePayloadAttributes, ZonePayloadTypes}; + +/// Engine that drives L2 block production from L1 events. +/// +/// Waits for L1 blocks in the [`DepositQueue`], then for each block: +/// 1. Peeks the L1 block from the queue +/// 2. Builds [`ZonePayloadAttributes`] wrapping inner Tempo attrs + L1 data +/// 3. Sends FCU with payload attributes to start a build +/// 4. Resolves the built payload +/// 5. Submits via `newPayload` +/// 6. Confirms the L1 block in the queue (removes it) +/// +/// On failure the L1 block stays in the queue and is retried. +#[derive(Debug)] +pub struct ZoneEngine { + /// Chain spec for hardfork checks when building attributes. + chain_spec: Arc, + /// Engine API handle for FCU and newPayload. + to_engine: ConsensusEngineHandle, + /// Payload builder handle. + payload_builder: PayloadBuilderHandle, + /// Queue of L1 blocks with their deposits. + deposit_queue: DepositQueue, + /// Latest block header — used as parent for the next payload and as the + /// head/safe/finalized hash in FCU (instant finality). + last_header: SealedHeader, + /// Address that receives block fees. + fee_recipient: Address, + /// Sequencer's secp256k1 secret key for ECIES decryption of encrypted deposits. + sequencer_key: k256::SecretKey, + /// ZonePortal address on L1 — used as context in HKDF key derivation. + portal_address: Address, + /// Cache-first, RPC-fallback TIP-403 policy provider for authorization checks + /// on encrypted deposit recipients during preparation. + policy_provider: PolicyProvider, +} + +impl ZoneEngine { + pub fn new( + chain_spec: Arc, + to_engine: ConsensusEngineHandle, + payload_builder: PayloadBuilderHandle, + deposit_queue: DepositQueue, + last_header: SealedHeader, + fee_recipient: Address, + sequencer_key: k256::SecretKey, + portal_address: Address, + policy_provider: PolicyProvider, + ) -> Self { + Self { + chain_spec, + to_engine, + payload_builder, + deposit_queue, + last_header, + fee_recipient, + sequencer_key, + portal_address, + policy_provider, + } + } + + /// Runs the main Zone engine loop. + /// + /// This method never returns under normal operation. It: + /// 1. Waits for L1 blocks to arrive in the deposit queue + /// 2. Advances the zone chain for each available L1 block (no delay between blocks) + /// 3. Sends periodic FCU heartbeats + pub async fn run(mut self) { + let mut fcu_interval = tokio::time::interval(Duration::from_secs(1)); + fcu_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + // Send initial FCU to establish head + if let Err(e) = self.update_forkchoice_state().await { + error!(target: "zone::engine", "Error sending initial FCU: {:?}", e); + } + + loop { + tokio::select! { + // Wait for new L1 blocks in the deposit queue + _ = self.deposit_queue.notified() => { + self.advance_all_available().await; + } + // Periodic FCU heartbeat — also drains any blocks we missed + _ = fcu_interval.tick() => { + self.advance_all_available().await; + if let Err(e) = self.update_forkchoice_state().await { + error!(target: "zone::engine", "Error updating fork choice: {:?}", e); + } + } + } + } + } + + /// Returns the current forkchoice state. + /// + /// The zone has instant finality so head = safe = finalized. + fn forkchoice_state(&self) -> ForkchoiceState { + ForkchoiceState::same_hash(self.last_header.hash()) + } + + /// Send an FCU without payload attributes (heartbeat). + async fn update_forkchoice_state(&self) -> eyre::Result<()> { + let state = self.forkchoice_state(); + let res = self.to_engine.fork_choice_updated(state, None).await?; + + if !res.is_valid() { + eyre::bail!("Invalid fork choice update {state:?}: {res:?}") + } + + Ok(()) + } + + /// Advance the chain for all available L1 blocks in the queue. + /// + /// During catch-up this processes blocks as fast as the EVM can execute + /// them, with no timer delays between blocks. + /// + /// Reorg safety is handled upstream by the L1 subscriber, which only + /// enqueues blocks once they are confirmed by a successor. + async fn advance_all_available(&mut self) { + while let Some(l1_block) = self.deposit_queue.peek() { + if let Err(e) = self.advance(l1_block).await { + error!(target: "zone::engine", "Error advancing the chain: {:?}", e); + tokio::time::sleep(Duration::from_millis(100)).await; + break; + } + } + } + + /// Decrypt encrypted deposits, check TIP-403 policy authorization, and + /// ABI-encode everything into a [`PreparedL1Block`] ready for the payload + /// builder. Errors (e.g. policy RPC failures) are propagated so the engine + /// retries rather than allowing unauthorized deposits through. + async fn prepare_l1_block(&self, l1_block: L1BlockDeposits) -> eyre::Result { + l1_block + .prepare( + &self.sequencer_key, + self.portal_address, + &self.policy_provider, + ) + .await + } + + /// Advance the chain by one block. + /// + /// Wraps the given L1 block into [`ZonePayloadAttributes`], sends FCU + /// with those attributes, waits for the payload to be built, then submits + /// via `newPayload`. Only confirms (removes) the L1 block from the + /// deposit queue after `newPayload` succeeds. + async fn advance(&mut self, l1_block: L1BlockDeposits) -> eyre::Result<()> { + let l1_num_hash = l1_block.header.num_hash(); + + // Zone block timestamp is locked to the L1 block's timestamp so the + // two chains stay in lockstep. + let timestamp_secs = l1_block.header.timestamp(); + let timestamp_millis_part = l1_block.header.timestamp_millis_part; + + let l1_block = self.prepare_l1_block(l1_block).await?; + + let attributes = ZonePayloadAttributes { + inner: EthPayloadAttributes { + timestamp: timestamp_secs, + prev_randao: B256::ZERO, + suggested_fee_recipient: self.fee_recipient, + withdrawals: self + .chain_spec + .is_shanghai_active_at_timestamp(timestamp_secs) + .then(Default::default), + parent_beacon_block_root: self + .chain_spec + .is_cancun_active_at_timestamp(timestamp_secs) + .then_some(B256::ZERO), + slot_number: None, + target_gas_limit: None, + }, + timestamp_millis_part, + l1_block, + }; + + // Send FCU with payload attributes through the engine API to trigger + // payload building. The forkchoice state points at the current head; + // the attributes carry the L1 block data for the new zone block. + let res = self + .to_engine + .fork_choice_updated(self.forkchoice_state(), Some(attributes)) + .await?; + + if res.is_invalid() { + eyre::bail!("Invalid payload status") + } + + let payload_id = res.payload_id.ok_or_eyre("No payload id")?; + + let Some(Ok(payload)) = self + .payload_builder + .resolve_kind(payload_id, PayloadKind::WaitForPending) + .await + else { + eyre::bail!("No payload") + }; + + let header = payload.block().sealed_header().clone(); + let block_number = header.number(); + let payload = ZonePayloadTypes::block_to_payload(payload.block().clone(), None); + let res = self.to_engine.new_payload(payload).await?; + + if !res.is_valid() { + eyre::bail!("Invalid payload for block {block_number}") + } + + // newPayload succeeded — confirm the L1 block in the queue so it is + // removed. If the queue was reorged between peek and confirm, the + // block was already purged; log a warning but still update + // last_header since the zone chain has advanced. + if self.deposit_queue.confirm(l1_num_hash).is_none() { + warn!(target: "zone::engine", ?l1_num_hash, "L1 block was purged from queue during build"); + } + + // GC stale versioned entries from the policy cache. Only the engine + // drives this — the subscriber must not advance past blocks the engine + // hasn't processed yet, otherwise policy lookups for in-flight blocks + // could return wrong results. + self.policy_provider.cache().advance(l1_num_hash.number); + + self.last_header = header; + + // Canonicalize the new head — FCU-with-attrs above only set the + // *previous* head as canonical; this bare FCU makes the just-built + // block the EL's canonical head. + if let Err(e) = self.update_forkchoice_state().await { + error!(target: "zone::engine", "Error sending post-newPayload FCU: {:?}", e); + } + + Ok(()) + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs new file mode 100644 index 00000000..4bb7d847 --- /dev/null +++ b/crates/node/src/lib.rs @@ -0,0 +1,17 @@ +//! Tempo Zone node assembly. + +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(unnameable_types)] +#![allow(clippy::too_many_arguments)] + +use eyre as _; + +#[cfg(feature = "cli")] +pub mod cli; +pub mod engine; +pub mod node; +pub mod rpc; + +pub use engine::ZoneEngine; +pub use node::{ZoneExecutorBuilder, ZoneNode, ZonePrivateRpcConfig, ZoneSequencerAddOnsConfig}; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs new file mode 100644 index 00000000..62db24d9 --- /dev/null +++ b/crates/node/src/node.rs @@ -0,0 +1,884 @@ +//! Tempo Zone Node configuration. +//! +//! This is a lightweight L2 node built on reth's node builder infrastructure. +//! It reuses Tempo's EVM, primitives, and pool, but with noop consensus/network/payload. + +use crate::{ + ZoneEngine, + rpc::{TempoZoneRpc, ZoneRpcApi, rpc_connection_config, start_private_rpc}, +}; +use alloy_primitives::Address; +use alloy_provider::Provider as _; +use alloy_signer_local::PrivateKeySigner; +use k256::SecretKey; +use reth_eth_wire_types::primitives::BasicNetworkPrimitives; +use reth_node_api::{ + AddOnsContext, FullNodeComponents, FullNodeTypes, NodeAddOns, NodeTypes, + PayloadAttributesBuilder, PayloadTypes, +}; +use reth_node_builder::{ + BuilderContext, DebugNode, Node, NodeAdapter, + components::{ + BasicPayloadServiceBuilder, ComponentsBuilder, ExecutorBuilder, NoopConsensusBuilder, + NoopNetworkBuilder, PoolBuilder, spawn_maintenance_tasks, + }, + rpc::{ + BasicEngineValidatorBuilder, EngineValidatorAddOn, EthApiBuilder, NoopEngineApiBuilder, + PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, + }, +}; +use reth_primitives_traits::{SealedHeader, transaction::error::InvalidTransactionError}; +use reth_provider::ChainSpecProvider; +use reth_rpc_builder::Identity; +use reth_rpc_eth_api::EthApiTypes; +use reth_storage_api::{BlockNumReader, EmptyBodyStorage, HeaderProvider, StateProviderFactory}; +use reth_transaction_pool::{ + Pool, TransactionValidationTaskExecutor, blobstore::InMemoryBlobStore, + error::InvalidPoolTransactionError, +}; +use std::{collections::HashSet, sync::Arc, time::Duration}; +use tempo_alloy::TempoNetwork; +use tempo_chainspec::spec::TempoChainSpec; +use tempo_evm::TempoEvmConfig; +use tempo_node::{ + DEFAULT_AA_VALID_AFTER_MAX_SECS, engine::TempoEngineValidator, rpc::TempoEthApiBuilder, +}; +use tempo_primitives::{ + self as primitives, TempoHeader, TempoPrimitives, TempoTxEnvelope, TempoTxType, +}; +use tempo_transaction_pool::{ + AA2dPool, AA2dPoolConfig, TempoTransactionPool, + amm::AmmLiquidityCache, + ordering::TempoTipOrdering, + transaction::TempoPooledTransaction, + validator::{DEFAULT_MAX_TEMPO_AUTHORIZATIONS, TempoTransactionValidator}, +}; +use tempo_zone_contracts::{ + TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZonePortal, +}; +use tracing::{debug, info}; +use zone_evm::ZoneEvmConfig; +use zone_l1::{ + DepositQueue, L1Subscriber, L1SubscriberConfig, PolicyCache, TempoStateExt, + state::{ + L1StateCache, L1StateProvider, L1StateProviderConfig, PolicyProvider, + spawn_policy_resolution_task, spawn_pool_prefetch_task, + }, +}; +use zone_payload::{ZonePayloadAttributes, ZonePayloadFactory, ZonePayloadTypes}; +use zone_sequencer::{BatchAnchorConfig, ZoneSequencerConfig, spawn_zone_sequencer}; + +/// Network primitives for Zone Nodes +type ZoneNetworkPrimitives = BasicNetworkPrimitives; + +/// Configuration for the sequencer background tasks +#[derive(Debug, Clone)] +pub struct ZoneSequencerAddOnsConfig { + /// Sequencer private key signer for signing L1 transactions. + pub sequencer_signer: PrivateKeySigner, + /// Zone ID for chain ID validation. + pub zone_id: u32, + /// How often the zone monitor polls for new L2 blocks. + pub zone_poll_interval: Duration, + /// Maximum time to accumulate zone blocks before batch submission. + pub batch_interval: Duration, + /// EIP-2935 history and safety-margin limits used by the batch submitter. + pub batch_anchor_config: BatchAnchorConfig, + /// How often the withdrawal processor polls the L1 queue. + pub withdrawal_poll_interval: Duration, +} + +/// Configuration for the Zone private RPC server extension. +#[derive(Debug, Clone, Default)] +pub struct ZonePrivateRpcConfig { + /// Port for RPC traffic. + pub private_rpc_port: u16, + /// Zone ID for chain ID validation and private RPC auth. + pub zone_id: u32, + /// Max duration for private RPC auth. + pub max_auth_token_validity: Duration, +} + +/// Tempo Zone node type configuration. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ZoneNode { + /// Queue of L1 deposit messages to be included in the next zone block. + deposit_queue: DepositQueue, + /// Configuration for the L1 event subscriber (RPC endpoint, poll interval, etc.). + l1_config: L1SubscriberConfig, + /// Configuration for the L1 state provider (contract addresses, query parameters). + l1_state_provider_config: L1StateProviderConfig, + /// Shared L1 state cache (enabled tokens, zone metadata, etc.). + l1_state_cache: L1StateCache, + /// Shared TIP-403 policy cache, populated by the unified [`L1Subscriber`](zone_l1::L1Subscriber) + /// and read by the precompile during block building. + policy_cache: PolicyCache, + /// Address of the L1 deposit portal contract. + portal_address: Address, + /// Optional pre-configured list of enabled token addresses. When set, the + /// startup L1 RPC query for `enabledTokenCount`/`enabledTokens` is skipped. + initial_tokens: Option>, + /// Private RPC config. + private_rpc_config: ZonePrivateRpcConfig, + /// Optional sequencer config. When set, sequencer tasks are spawned. + sequencer_config: Option, +} + +impl ZoneNode { + // Creates a new ZoneNode + pub fn new( + l1_rpc_url: String, + portal_address: Address, + genesis_tempo_block_number: Option, + l1_fetch_concurrency: usize, + retry_connection_interval: Duration, + ) -> Self { + let deposit_queue = DepositQueue::default(); + + let policy_cache = PolicyCache::default(); + let l1_state_cache = L1StateCache::new(HashSet::from([portal_address])); + let l1_config = L1SubscriberConfig { + l1_rpc_url: l1_rpc_url.clone(), + portal_address, + genesis_tempo_block_number, + policy_cache: policy_cache.clone(), + l1_state_cache: l1_state_cache.clone(), + l1_fetch_concurrency, + retry_connection_interval, + }; + + let l1_state_provider_config = L1StateProviderConfig { + l1_rpc_url, + portal_address, + retry_connection_interval, + ..Default::default() + }; + + Self { + deposit_queue, + l1_config, + l1_state_provider_config, + l1_state_cache, + policy_cache, + portal_address, + initial_tokens: None, + private_rpc_config: ZonePrivateRpcConfig::default(), + sequencer_config: None, + } + } + + /// Set the private RPC configuration. + pub fn with_private_rpc(mut self, config: ZonePrivateRpcConfig) -> Self { + self.private_rpc_config = config; + self + } + + /// Set the sequencer configuration. When set, batch submission and + /// withdrawal processing tasks are spawned during node launch. + pub fn with_sequencer(mut self, config: ZoneSequencerAddOnsConfig) -> Self { + self.sequencer_config = Some(config); + self + } + + /// Set the initial list of enabled token addresses. + /// When set, the startup L1 RPC query for enabled tokens is skipped. + pub fn with_initial_tokens(mut self, tokens: Vec
) -> Self { + self.initial_tokens = Some(tokens); + self + } + + /// Returns the current deposit queue + pub fn deposit_queue(&self) -> DepositQueue { + self.deposit_queue.clone() + } + + /// Returns the current l1 state cache + pub fn l1_state_cache(&self) -> L1StateCache { + self.l1_state_cache.clone() + } + + /// Returns the current TIP-403 policy cache + pub fn policy_cache(&self) -> PolicyCache { + self.policy_cache.clone() + } + + /// Returns a [`ComponentsBuilder`] configured for a Zone node. + pub fn components( + executor_builder: ZoneExecutorBuilder, + ) -> ComponentsBuilder< + N, + ZonePoolBuilder, + BasicPayloadServiceBuilder, + NoopNetworkBuilder, + ZoneExecutorBuilder, + NoopConsensusBuilder, + > + where + N: FullNodeTypes, + { + ComponentsBuilder::default() + .node_types::() + .pool(ZonePoolBuilder) + .executor(executor_builder) + .payload(BasicPayloadServiceBuilder::new( + ZonePayloadFactory::default(), + )) + .network(NoopNetworkBuilder::::default()) + .noop_consensus() + } +} + +impl NodeTypes for ZoneNode { + type Primitives = TempoPrimitives; + type ChainSpec = TempoChainSpec; + type Storage = EmptyBodyStorage; + type Payload = ZonePayloadTypes; +} + +/// Addons for Tempo Zone nodes. +pub struct ZoneAddOns +where + N: FullNodeComponents, + N::Pool: reth_transaction_pool::TransactionPool, +{ + inner: RpcAddOns< + N, + TempoEthApiBuilder, + ZoneEngineValidatorBuilder, + NoopEngineApiBuilder, + BasicEngineValidatorBuilder, + Identity, + >, + /// Queue of L1 deposit messages to be included in the next zone block. + deposit_queue: DepositQueue, + /// Configuration for the L1 event subscriber + l1_config: L1SubscriberConfig, + /// TIP-403 policy cache + policy_cache: PolicyCache, + /// ZonePortal address on L1. + portal_address: Address, + /// Pre-configured list of initial tokens. + initial_tokens: Option>, + /// Private RPC configuration. + private_rpc_config: ZonePrivateRpcConfig, + /// Sequencer configuration. + sequencer_config: Option, +} + +impl std::fmt::Debug for ZoneAddOns +where + N: FullNodeComponents, + N::Pool: reth_transaction_pool::TransactionPool, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ZoneAddOns").finish_non_exhaustive() + } +} + +impl ZoneAddOns> +where + N: FullNodeTypes, +{ + /// Creates a new ZoneAddOns instance. + pub fn new( + deposit_queue: DepositQueue, + l1_config: L1SubscriberConfig, + policy_cache: PolicyCache, + portal_address: Address, + initial_tokens: Option>, + private_rpc_config: ZonePrivateRpcConfig, + sequencer_config: Option, + ) -> Self { + Self { + inner: RpcAddOns::new( + TempoEthApiBuilder::default(), + ZoneEngineValidatorBuilder, + NoopEngineApiBuilder::default(), + BasicEngineValidatorBuilder::default(), + Identity::default(), + Default::default(), + ), + deposit_queue, + l1_config, + policy_cache, + portal_address, + initial_tokens, + private_rpc_config, + sequencer_config, + } + } +} + +impl NodeAddOns for ZoneAddOns +where + N: FullNodeComponents, + N::Pool: reth_transaction_pool::TransactionPool< + Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, + >, + TempoEthApiBuilder: EthApiBuilder>, +{ + type Handle = , + ZoneEngineValidatorBuilder, + NoopEngineApiBuilder, + BasicEngineValidatorBuilder, + Identity, + > as NodeAddOns>::Handle; + + async fn launch_add_ons(mut self, ctx: AddOnsContext<'_, N>) -> eyre::Result { + let sp = ctx.node.provider().latest()?; + let tempo_block_number = sp.tempo_block_number()?; + self.policy_cache.set_last_l1_block(tempo_block_number); + info!(target: "reth::cli", tempo_block_number, "Read local tempoBlockNumber for L1 subscriber"); + + let l1_provider = alloy_provider::ProviderBuilder::new_with_network::() + .connect_with_config( + &self.l1_config.l1_rpc_url, + rpc_connection_config(self.l1_config.retry_connection_interval), + ) + .await? + .erased(); + + self.resolve_and_seed_tokens(&l1_provider).await?; + self.spawn_l1_subscriber(&ctx); + self.spawn_policy_tasks(&l1_provider, &ctx); + + if let Some(ref config) = self.sequencer_config { + let sequencer_addr = config.sequencer_signer.address(); + let sequencer_key = SecretKey::from(config.sequencer_signer.credential()); + self.spawn_zone_engine(l1_provider, &ctx, sequencer_addr, sequencer_key)?; + } + + let task_executor = ctx.node.task_executor().clone(); + + let chain_id = ctx + .node + .provider() + .chain_spec() + .inner + .genesis() + .config + .chain_id; + let handle = self.inner.launch_add_ons(ctx).await?; + + Self::launch_private_rpc( + self.private_rpc_config, + &handle, + self.l1_config.l1_rpc_url.clone(), + self.l1_config.retry_connection_interval, + self.l1_config.portal_address, + chain_id, + ) + .await?; + + if let Some(config) = self.sequencer_config.take() { + let sequencer_addr = config.sequencer_signer.address(); + + Self::launch_sequencer_tasks( + config, + &handle, + &task_executor, + self.l1_config.l1_rpc_url, + self.l1_config.portal_address, + self.l1_config.retry_connection_interval, + sequencer_addr, + chain_id, + ) + .await?; + } + + Ok(handle) + } +} + +impl ZoneAddOns +where + N: FullNodeComponents, + N::Pool: reth_transaction_pool::TransactionPool< + Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, + >, + TempoEthApiBuilder: EthApiBuilder>, +{ + /// Resolve enabled tokens and seed the policy cache. + async fn resolve_and_seed_tokens( + &mut self, + l1_provider: &alloy_provider::DynProvider, + ) -> eyre::Result<()> { + let portal = self.portal_address; + let tracked_tokens = if let Some(tokens) = self.initial_tokens.take() { + info!(target: "reth::cli", count = tokens.len(), ?tokens, "Using pre-configured initial tokens"); + tokens + } else { + let tokens = ZonePortal::new(portal, l1_provider) + .enabled_tokens() + .await?; + info!(target: "reth::cli", count = tokens.len(), ?tokens, "Discovered enabled tokens from L1"); + tokens + }; + + self.policy_cache + .seed_token_policies(portal, &tracked_tokens, l1_provider) + .await?; + info!(target: "reth::cli", "Seeded token policies from L1"); + Ok(()) + } + + /// Spawn the L1 subscriber. Listens for new blocks and deposit events. + fn spawn_l1_subscriber(&mut self, ctx: &AddOnsContext<'_, N>) { + L1Subscriber::spawn( + self.l1_config.clone(), + ctx.node.provider().clone(), + self.deposit_queue.clone(), + ctx.node.task_executor().clone(), + ); + info!(target: "reth::cli", "Unified L1 subscriber started"); + } + + /// Spawn TIP-403 policy resolution and pool prefetch tasks. + fn spawn_policy_tasks( + &self, + l1_provider: &alloy_provider::DynProvider, + ctx: &AddOnsContext<'_, N>, + ) { + let policy_task_handle = spawn_policy_resolution_task( + self.policy_cache.clone(), + l1_provider.clone(), + 16, + 256, + ctx.node.task_executor().clone(), + ); + spawn_pool_prefetch_task( + ctx.node.pool().clone(), + policy_task_handle, + ctx.node.task_executor().clone(), + ); + info!(target: "reth::cli", "TIP-403 policy prefetch tasks started"); + } + + /// Spawn the [`ZoneEngine`] for L1-event-driven block production. + fn spawn_zone_engine( + &self, + l1_provider: alloy_provider::DynProvider, + ctx: &AddOnsContext<'_, N>, + fee_recipient: Address, + sequencer_key: SecretKey, + ) -> eyre::Result<()> { + let policy_provider = PolicyProvider::new( + self.policy_cache.clone(), + l1_provider, + tokio::runtime::Handle::current(), + ); + let provider = ctx.node.provider(); + let last_header = provider + .sealed_header(provider.best_block_number()?)? + .ok_or_else(|| eyre::eyre!("no latest block header"))?; + let engine = ZoneEngine::new( + provider.chain_spec(), + ctx.beacon_engine_handle.clone(), + ctx.node.payload_builder_handle().clone(), + self.deposit_queue.clone(), + last_header, + fee_recipient, + sequencer_key, + self.portal_address, + policy_provider, + ); + ctx.node + .task_executor() + .spawn_critical_task("zone-engine", engine.run()); + info!(target: "reth::cli", "ZoneEngine spawned"); + Ok(()) + } + + /// Launch the private RPC server. + async fn launch_private_rpc( + config: ZonePrivateRpcConfig, + handle: &>::Handle, + l1_rpc_url: String, + retry_connection_interval: Duration, + portal_address: Address, + chain_id: u64, + ) -> eyre::Result<()> { + if config.zone_id != 0 { + let expected = zone_primitives::constants::zone_chain_id(config.zone_id); + if chain_id != expected { + eyre::bail!( + "chain ID mismatch: zone.id={} requires chain_id={}, but genesis has {}", + config.zone_id, + expected, + chain_id, + ); + } + } + + let eth_handlers = handle.eth_handlers().clone(); + let zone_rpc_url = handle + .rpc_server_handles + .rpc + .http_url() + .expect("HTTP RPC server must be enabled for private RPC"); + let private_rpc_config = zone_rpc::PrivateRpcConfig { + listen_addr: ([0, 0, 0, 0], config.private_rpc_port).into(), + l1_rpc_url, + zone_rpc_url, + retry_connection_interval, + zone_id: config.zone_id, + chain_id, + max_auth_token_validity: config.max_auth_token_validity, + zone_portal: portal_address, + }; + let api: Arc = + Arc::new(TempoZoneRpc::new(eth_handlers, private_rpc_config.clone()).await?); + let local_addr = start_private_rpc(private_rpc_config, api).await?; + info!(target: "reth::cli", %local_addr, "Private zone RPC server started"); + + Ok(()) + } + + /// Launch sequencer background tasks: batch submission, withdrawal processing, + /// and engine shutdown hook. + async fn launch_sequencer_tasks( + config: ZoneSequencerAddOnsConfig, + handle: &>::Handle, + task_executor: &reth_tasks::TaskExecutor, + l1_rpc_url: String, + portal_address: Address, + retry_connection_interval: Duration, + sequencer_addr: Address, + chain_id: u64, + ) -> eyre::Result<()> { + if config.zone_id != 0 { + let expected = zone_primitives::constants::zone_chain_id(config.zone_id); + if chain_id != expected { + eyre::bail!( + "chain ID mismatch: zone.id={} requires chain_id={}, but genesis has {}", + config.zone_id, + expected, + chain_id, + ); + } + } + + let zone_rpc_url = handle + .rpc_server_handles + .rpc + .http_url() + .expect("HTTP RPC server must be enabled for sequencer mode"); + + info!(target: "reth::cli", %sequencer_addr, "Starting sequencer background tasks"); + let sequencer_config = ZoneSequencerConfig { + portal_address, + l1_rpc_url, + retry_connection_interval, + withdrawal_poll_interval: config.withdrawal_poll_interval, + outbox_address: ZONE_OUTBOX_ADDRESS, + inbox_address: ZONE_INBOX_ADDRESS, + tempo_state_address: TEMPO_STATE_ADDRESS, + zone_rpc_url, + zone_poll_interval: config.zone_poll_interval, + batch_interval: config.batch_interval, + batch_anchor_config: config.batch_anchor_config, + }; + let seq_handle = spawn_zone_sequencer(sequencer_config, config.sequencer_signer).await; + info!(target: "reth::cli", "Sequencer tasks spawned"); + + // Critical task — node shuts down if either exits. + task_executor.spawn_critical_task("zone-monitor", async move { + tokio::select! { + res = seq_handle.withdrawal_handle => { + tracing::error!(target: "reth::cli", ?res, "Withdrawal processor task exited"); + } + res = seq_handle.monitor_handle => { + tracing::error!(target: "reth::cli", ?res, "Zone monitor task exited"); + } + } + }); + + // Flush unpersisted blocks on shutdown. + let engine_shutdown = handle.engine_shutdown.clone(); + task_executor.spawn_critical_with_graceful_shutdown_signal( + "zone-engine-shutdown", + |shutdown| async move { + let _guard = shutdown.await; + info!(target: "reth::cli", "Shutdown signal received — flushing engine state"); + if let Some(done) = engine_shutdown.shutdown() { + let _ = done.await; + } + }, + ); + + Ok(()) + } +} + +impl RethRpcAddOns for ZoneAddOns +where + N: FullNodeComponents, + N::Pool: reth_transaction_pool::TransactionPool< + Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, + >, + TempoEthApiBuilder: + EthApiBuilder>, +{ + type EthApi = as EthApiBuilder>::EthApi; + + fn hooks_mut(&mut self) -> &mut reth_node_builder::rpc::RpcHooks { + self.inner.hooks_mut() + } +} + +impl EngineValidatorAddOn for ZoneAddOns +where + N: FullNodeComponents, + N::Pool: reth_transaction_pool::TransactionPool< + Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, + >, + TempoEthApiBuilder: EthApiBuilder>, +{ + type ValidatorBuilder = BasicEngineValidatorBuilder; + + fn engine_validator_builder(&self) -> Self::ValidatorBuilder { + self.inner.engine_validator_builder() + } +} + +impl Node for ZoneNode +where + N: FullNodeTypes, +{ + type ComponentsBuilder = ComponentsBuilder< + N, + ZonePoolBuilder, + BasicPayloadServiceBuilder, + NoopNetworkBuilder, + ZoneExecutorBuilder, + NoopConsensusBuilder, + >; + type AddOns = ZoneAddOns>; + + fn components_builder(&self) -> Self::ComponentsBuilder { + let executor_builder = ZoneExecutorBuilder::new( + self.l1_state_provider_config.clone(), + self.l1_state_cache.clone(), + self.policy_cache.clone(), + ); + Self::components(executor_builder) + } + + fn add_ons(&self) -> Self::AddOns { + ZoneAddOns::new( + self.deposit_queue.clone(), + self.l1_config.clone(), + self.policy_cache.clone(), + self.portal_address, + self.initial_tokens.clone(), + self.private_rpc_config.clone(), + self.sequencer_config.clone(), + ) + } +} + +impl> DebugNode for ZoneNode { + type RpcBlock = + alloy_rpc_types_eth::Block, TempoHeader>; + + fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> primitives::Block { + rpc_block + .into_consensus_block() + .map_transactions(|tx| tx.into_inner()) + } + + fn local_payload_attributes_builder( + chain_spec: &Self::ChainSpec, + ) -> impl PayloadAttributesBuilder<::PayloadAttributes, TempoHeader> + { + ZonePayloadAttributesBuilder::new(Arc::new(chain_spec.clone())) + } +} + +/// Builds [`ZonePayloadAttributes`] with `l1_block: None` — suitable for +/// debug/test scenarios where no L1 data is available. +#[derive(Debug)] +pub(crate) struct ZonePayloadAttributesBuilder; + +impl ZonePayloadAttributesBuilder { + pub(crate) fn new(_chain_spec: Arc) -> Self { + Self + } +} + +impl PayloadAttributesBuilder for ZonePayloadAttributesBuilder { + fn build(&self, _parent: &SealedHeader) -> ZonePayloadAttributes { + unimplemented!("zone blocks require L1 data — use ZoneEngine instead") + } +} + +/// Builder that constructs the [`ZoneEvmConfig`] used during block execution. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ZoneExecutorBuilder { + l1_state_provider_config: L1StateProviderConfig, + l1_state_cache: L1StateCache, + policy_cache: PolicyCache, +} + +impl ZoneExecutorBuilder { + /// Create a zone executor builder with the shared L1 state/policy caches. + pub fn new( + l1_state_provider_config: L1StateProviderConfig, + l1_state_cache: L1StateCache, + policy_cache: PolicyCache, + ) -> Self { + Self { + l1_state_provider_config, + l1_state_cache, + policy_cache, + } + } +} + +impl ExecutorBuilder for ZoneExecutorBuilder +where + Node: FullNodeTypes, +{ + type EVM = ZoneEvmConfig; + + async fn build_evm(self, ctx: &BuilderContext) -> eyre::Result { + let runtime_handle = tokio::runtime::Handle::current(); + let l1_provider = L1StateProvider::new( + self.l1_state_provider_config.clone(), + self.l1_state_cache, + runtime_handle.clone(), + ) + .await?; + + let mut evm_config = ZoneEvmConfig::new(ctx.chain_spec(), l1_provider); + + // Create PolicyProvider for the TIP-403 proxy precompile. + let policy_l1 = alloy_provider::ProviderBuilder::new_with_network::() + .connect_with_config( + &self.l1_state_provider_config.l1_rpc_url, + rpc_connection_config(self.l1_state_provider_config.retry_connection_interval), + ) + .await? + .erased(); + + let policy_provider = PolicyProvider::new(self.policy_cache, policy_l1, runtime_handle); + evm_config = evm_config.with_policy_provider(policy_provider); + info!(target: "reth::cli", "Zone EVM initialized with TempoStateReader + TIP-403 proxy precompiles"); + + Ok(evm_config) + } +} + +/// Engine validator builder for Zone. +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct ZoneEngineValidatorBuilder; + +impl PayloadValidatorBuilder for ZoneEngineValidatorBuilder +where + Node: FullNodeComponents, +{ + type Validator = TempoEngineValidator; + + async fn build(self, _ctx: &AddOnsContext<'_, Node>) -> eyre::Result { + Ok(TempoEngineValidator::new()) + } +} + +/// Transaction pool builder for Zone - uses Tempo pool with defaults. +#[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] +pub struct ZonePoolBuilder; + +impl PoolBuilder for ZonePoolBuilder +where + Node: FullNodeTypes, +{ + type Pool = TempoTransactionPool; + + async fn build_pool( + self, + ctx: &BuilderContext, + _evm_config: ZoneEvmConfig, + ) -> eyre::Result { + 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 additional_tasks = ctx.config().txpool.additional_validation_tasks; + let task_executor = ctx.task_executor().clone(); + let mut validator = TransactionValidationTaskExecutor::eth_builder( + ctx.provider().clone(), + tempo_evm_config, + ) + .with_max_tx_input_bytes(ctx.config().txpool.max_tx_input_bytes) + .with_local_transactions_config(pool_config.local_transactions_config.clone()) + .set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap) + .with_max_tx_gas_limit(ctx.config().txpool.max_tx_gas_limit) + .set_block_gas_limit(ctx.chain_spec().inner.genesis().gas_limit) + .disable_balance_check() + .with_minimum_priority_fee(ctx.config().txpool.minimum_priority_fee) + .with_custom_tx_type(TempoTxType::AA as u8) + .no_eip4844() + .build::(blob_store.clone()); + + validator.set_additional_stateless_validation(|_origin, tx| { + use alloy_consensus::Transaction; + if tx.is_create() { + return Err(InvalidPoolTransactionError::Consensus( + InvalidTransactionError::TxTypeNotSupported, + )); + } + Ok(()) + }); + + let validator = + TransactionValidationTaskExecutor::spawn(validator, &task_executor, additional_tasks); + + let aa_2d_config = AA2dPoolConfig { + price_bump_config: pool_config.price_bumps, + pending_limit: pool_config.pending_limit, + queued_limit: pool_config.queued_limit, + max_txs_per_sender: pool_config.max_account_slots, + }; + let aa_2d_pool = AA2dPool::new(aa_2d_config); + let amm_liquidity_cache = AmmLiquidityCache::new(ctx.provider())?; + + let validator = validator.map(|v| { + TempoTransactionValidator::new( + v, + DEFAULT_AA_VALID_AFTER_MAX_SECS, + DEFAULT_MAX_TEMPO_AUTHORIZATIONS, + amm_liquidity_cache.clone(), + ) + }); + let protocol_pool = Pool::new( + validator, + TempoTipOrdering::default(), + blob_store, + pool_config.clone(), + ); + + let transaction_pool = TempoTransactionPool::new(protocol_pool, aa_2d_pool); + + spawn_maintenance_tasks(ctx, transaction_pool.clone(), &pool_config)?; + + // Spawn unified Tempo pool maintenance task + // This consolidates: expired AA txs, 2D nonce updates, AMM cache, and keychain revocations + ctx.task_executor().spawn_critical_task( + "txpool maintenance - tempo pool", + tempo_transaction_pool::maintain::maintain_tempo_pool(transaction_pool.clone()), + ); + + info!(target: "reth::cli", "Transaction pool initialized"); + debug!(target: "reth::cli", "Spawned txpool maintenance task"); + + Ok(transaction_pool) + } +} diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs new file mode 100644 index 00000000..feb6c9c0 --- /dev/null +++ b/crates/node/src/rpc.rs @@ -0,0 +1,1144 @@ +//! [`ZoneRpcApi`] implementation backed by reth's EthApi (in-process reth-backed). +//! +//! Re-exports the standalone `zone-rpc` crate so everything is accessible +//! via `zone::rpc::*`. + +pub use zone_rpc::*; + +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Weak}, + time::Duration, +}; + +use alloy_network::{ReceiptResponse, TransactionResponse}; +use alloy_primitives::{Address, B256, Bloom, Bytes, U64, U256}; +use alloy_provider::{DynProvider, Provider, ProviderBuilder}; +use alloy_rpc_types_eth::{ + Block, BlockId, BlockNumberOrTag, BlockTransactions, Filter, FilterChanges, FilterId, + TransactionRequest, + state::{EvmOverrides, StateOverride}, +}; +use alloy_sol_types::{SolCall, SolEvent, SolEventInterface}; +use eyre::WrapErr; +use futures::StreamExt; +use reth_provider::CanonStateSubscriptions; +use reth_rpc::{EthFilter, eth::filter::EthFilterError}; +use reth_rpc_builder::EthHandlers; +use reth_rpc_eth_api::{ + EthApiTypes, EthFilterApiServer, RpcConvert, + helpers::{EthApiSpec, EthBlocks, EthCall, EthFees, EthState, EthTransactions, FullEthApi}, +}; +use reth_rpc_eth_types::logs_utils; +use tempo_alloy::{ + TempoNetwork, + rpc::{TempoHeaderResponse, TempoTransactionRequest}, +}; +use tempo_contracts::precompiles::{ + ACCOUNT_KEYCHAIN_ADDRESS, + account_keychain::IAccountKeychain::{self, KeyInfo, getKeyCall}, +}; +use tempo_primitives::{TempoHeader, TempoTxEnvelope}; +use tokio::{ + sync::Mutex, + time::{MissedTickBehavior, interval}, +}; + +use alloy_rpc_client::ConnectionConfig; +use tempo_zone_contracts::{ + DepositType, TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_TOKEN_ADDRESS, ZoneInbox, ZonePortal, +}; +use zone_rpc::{ + auth::AuthContext, + types::{ + AuthorizationTokenInfoResponse, BoxEyreFut, BoxFut, DepositKind, DepositState, + DepositStatusEntry, DepositStatusResponse, JsonRpcError, ZoneInfoResponse, internal, + raw_null, raw_zero, to_raw, + }, +}; + +type RpcBlock = Block, TempoHeaderResponse>; +const FILTER_OWNER_PRUNE_INTERVAL: Duration = Duration::from_secs(60); + +fn filter_not_found_error() -> JsonRpcError { + JsonRpcError::invalid_params("filter not found") +} + +fn map_eth_filter_error(err: EthFilterError) -> JsonRpcError { + match err { + EthFilterError::FilterNotFound(_) => filter_not_found_error(), + other => internal(other), + } +} + +fn stale_filter_owner_ids( + owner_ids: impl IntoIterator, + active_ids: &HashSet, +) -> Vec { + owner_ids + .into_iter() + .filter(|id| !active_ids.contains(id)) + .collect() +} + +async fn prune_filter_owners( + filter: &EthFilter, + owners: &Mutex>, +) { + let owner_ids = { + let owners = owners.lock().await; + owners.keys().cloned().collect::>() + }; + if owner_ids.is_empty() { + return; + } + + let active_ids = filter + .active_filters() + .ids() + .await + .into_iter() + .collect::>(); + let stale_ids = stale_filter_owner_ids(owner_ids, &active_ids); + if stale_ids.is_empty() { + return; + } + + let mut owners = owners.lock().await; + for id in stale_ids { + owners.remove(&id); + } +} + +/// [`ZoneRpcApi`] implementation backed by reth's [`EthHandlers`]. +/// +/// This is the privacy enforcement layer for the zone's JSON-RPC surface. +/// Only methods explicitly routed through [`ZoneRpcApi`] are reachable — +/// everything else is rejected by the dispatcher's [`classify_method`] +/// whitelist, so this struct effectively acts as an **enforced allowlist** +/// of Ethereum JSON-RPC endpoints. +/// +/// For every allowed endpoint it applies typed privacy checks *before* +/// serializing to JSON: +/// +/// - **Block redaction** — zeroing `logsBloom` and clearing transaction +/// lists for non-sequencer callers. +/// - **Sender-scoped access** — returning `null` for transactions and +/// receipts not owned by the authenticated caller. +/// - **`from`-enforcement** — `eth_call` / `eth_estimateGas` may only +/// simulate from the authenticated account (`-32004` on mismatch, +/// auto-set when omitted); state overrides are rejected for +/// non-sequencer callers (`-32602`). +/// - **Sender verification** — `eth_sendRawTransaction` checks that the +/// recovered transaction sender matches the authenticated account +/// (`-32003` on mismatch). +/// +/// [`classify_method`]: zone_rpc::types::classify_method +pub struct TempoZoneRpc { + eth: EthHandlers, + config: zone_rpc::PrivateRpcConfig, + l1_provider: DynProvider, + zone_provider: DynProvider, + tempo_state: tempo_zone_contracts::TempoState::TempoStateInstance< + DynProvider, + TempoNetwork, + >, + /// Maps filter IDs to the authenticated account that created them. + /// The reth filter registry remains the source of truth for filter liveness. + filter_owners: Arc>>, +} + +impl TempoZoneRpc { + /// Wrap reth's [`EthHandlers`] (api + filter + pubsub). + pub async fn new( + eth: EthHandlers, + config: zone_rpc::PrivateRpcConfig, + ) -> eyre::Result { + let l1_rpc_url = config.l1_rpc_url.clone(); + let zone_rpc_url = config.zone_rpc_url.clone(); + let l1_provider = ProviderBuilder::new_with_network::() + .connect_with_config( + &l1_rpc_url, + rpc_connection_config(config.retry_connection_interval), + ) + .await + .wrap_err("failed to connect private RPC L1 provider")? + .erased(); + let zone_provider = ProviderBuilder::new_with_network::() + .connect_with_config( + &zone_rpc_url, + rpc_connection_config(config.retry_connection_interval), + ) + .await + .wrap_err("failed to connect private RPC zone provider")? + .erased(); + let tempo_state = + tempo_zone_contracts::TempoState::new(TEMPO_STATE_ADDRESS, zone_provider.clone()); + let rpc = Self { + eth, + config, + l1_provider, + zone_provider, + tempo_state, + filter_owners: Arc::new(Mutex::new(HashMap::new())), + }; + rpc.spawn_filter_owner_pruner(); + Ok(rpc) + } + + /// Returns a reference to the inner [`EthFilter`] handler. + pub fn filter(&self) -> &EthFilter { + &self.eth.filter + } + + async fn filter_is_active(&self, id: &FilterId) -> bool { + self.filter().active_filters().contains(id).await + } + + fn spawn_filter_owner_pruner(&self) + where + Api: Send + Sync + 'static, + { + let filter = self.filter().clone(); + let owners: Weak>> = Arc::downgrade(&self.filter_owners); + tokio::spawn(async move { + let mut prune_interval = interval(FILTER_OWNER_PRUNE_INTERVAL); + prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + + loop { + prune_interval.tick().await; + + let Some(owners) = owners.upgrade() else { + break; + }; + + prune_filter_owners(&filter, &owners).await; + } + }); + } + + /// Verify that the filter belongs to the authenticated caller. + /// + /// Returns `Ok(())` if the caller owns the filter or is the sequencer. + /// Returns an error indistinguishable from "filter not found" to avoid + /// leaking filter existence to non-owners. + async fn ensure_filter_owner( + &self, + id: &FilterId, + auth: &AuthContext, + ) -> Result<(), JsonRpcError> { + let owner_matches = { + let owners = self.filter_owners.lock().await; + matches!(owners.get(id), Some(owner) if *owner == auth.caller) + }; + if !owner_matches { + return Err(filter_not_found_error()); + } + if self.filter_is_active(id).await { + Ok(()) + } else { + self.filter_owners.lock().await.remove(id); + Err(filter_not_found_error()) + } + } + + async fn portal_deposits_for_block( + &self, + tempo_block_number: u64, + ) -> Result, JsonRpcError> { + if self.config.zone_portal.is_zero() { + return Err(JsonRpcError::internal("zone portal not configured")); + } + + let filter = Filter::new() + .address(self.config.zone_portal) + .from_block(tempo_block_number) + .to_block(tempo_block_number) + .event_signature(vec![ + ZonePortal::DepositMade::SIGNATURE_HASH, + ZonePortal::EncryptedDepositMade::SIGNATURE_HASH, + ]); + + let logs = self.l1_provider.get_logs(&filter).await.map_err(internal)?; + let mut deposits = Vec::with_capacity(logs.len()); + + for log in logs { + match ZonePortal::ZonePortalEvents::decode_log(&log.inner) + .map_err(internal)? + .data + { + ZonePortal::ZonePortalEvents::DepositMade(event) => { + deposits.push(PortalDepositRecord::Regular { + deposit_hash: event.newCurrentDepositQueueHash, + sender: event.sender, + recipient: event.to, + token: event.token, + amount: event.netAmount, + memo: event.memo, + }); + } + ZonePortal::ZonePortalEvents::EncryptedDepositMade(event) => { + deposits.push(PortalDepositRecord::Encrypted { + deposit_hash: event.newCurrentDepositQueueHash, + sender: event.sender, + token: event.token, + amount: event.netAmount, + }); + } + _ => {} + } + } + + Ok(deposits) + } + + async fn zone_tokens(&self) -> Result, JsonRpcError> { + if self.config.zone_portal.is_zero() { + return Ok(vec![ZONE_TOKEN_ADDRESS]); + } + + ZonePortal::new(self.config.zone_portal, &self.l1_provider) + .enabled_tokens() + .await + .map_err(internal) + } + + async fn zone_sequencer(&self) -> Result { + if self.config.zone_portal.is_zero() { + return Ok(Address::ZERO); + } + + ZonePortal::new(self.config.zone_portal, &self.l1_provider) + .sequencer() + .call() + .await + .map_err(internal) + } + + async fn terminal_event_for_deposit( + &self, + deposit_hash: B256, + ) -> Result, JsonRpcError> { + let filter = Filter::new() + .address(ZONE_INBOX_ADDRESS) + .from_block(0) + .event_signature(vec![ + ZoneInbox::DepositProcessed::SIGNATURE_HASH, + ZoneInbox::DepositFailed::SIGNATURE_HASH, + ZoneInbox::EncryptedDepositProcessed::SIGNATURE_HASH, + ZoneInbox::EncryptedDepositFailed::SIGNATURE_HASH, + ZoneInbox::DepositRejected::SIGNATURE_HASH, + ]) + .topic1(deposit_hash); + + let logs = self + .zone_provider + .get_logs(&filter) + .await + .map_err(internal)?; + let Some(log) = logs.last() else { + return Ok(None); + }; + + let Some(signature) = log.topics().first().copied() else { + return Ok(None); + }; + + if signature == ZoneInbox::DepositProcessed::SIGNATURE_HASH { + ZoneInbox::DepositProcessed::decode_log(&log.inner).map_err(internal)?; + return Ok(Some(TerminalDepositEvent::RegularProcessed)); + } + + if signature == ZoneInbox::DepositFailed::SIGNATURE_HASH { + ZoneInbox::DepositFailed::decode_log(&log.inner).map_err(internal)?; + return Ok(Some(TerminalDepositEvent::RegularFailed)); + } + + if signature == ZoneInbox::EncryptedDepositProcessed::SIGNATURE_HASH { + let event = + ZoneInbox::EncryptedDepositProcessed::decode_log(&log.inner).map_err(internal)?; + return Ok(Some(TerminalDepositEvent::EncryptedProcessed { + recipient: event.to, + memo: event.memo, + })); + } + + if signature == ZoneInbox::EncryptedDepositFailed::SIGNATURE_HASH { + ZoneInbox::EncryptedDepositFailed::decode_log(&log.inner).map_err(internal)?; + return Ok(Some(TerminalDepositEvent::EncryptedFailed)); + } + + if signature == ZoneInbox::DepositRejected::SIGNATURE_HASH { + let event = ZoneInbox::DepositRejected::decode_log(&log.inner).map_err(internal)?; + return match event.depositType { + DepositType::Regular => Ok(Some(TerminalDepositEvent::RegularRejected)), + DepositType::Encrypted => Ok(Some(TerminalDepositEvent::EncryptedRejected)), + _ => Ok(None), + }; + } + + Ok(None) + } +} + +impl zone_rpc::ZoneRpcApi for TempoZoneRpc +where + Api: FullEthApi + EthApiTypes + Send + Sync + 'static, +{ + fn get_keychain_key(&self, account: Address, key_id: Address) -> BoxEyreFut<'_, KeyInfo> { + Box::pin(async move { + let request = TempoTransactionRequest { + inner: TransactionRequest { + to: Some(ACCOUNT_KEYCHAIN_ADDRESS.into()), + input: getKeyCall { + account, + keyId: key_id, + } + .abi_encode() + .into(), + ..Default::default() + }, + ..Default::default() + }; + + let output = EthCall::call(&self.eth.api, request, None, EvmOverrides::default()) + .await + .wrap_err("AccountKeychain.getKey eth_call failed")?; + + IAccountKeychain::getKeyCall::abi_decode_returns(output.as_ref()).map_err(Into::into) + }) + } + + fn block_number(&self) -> BoxFut<'_> { + Box::pin(async move { + let info = EthApiSpec::chain_info(&self.eth.api).map_err(internal)?; + to_raw(&U256::from(info.best_number)) + }) + } + + fn chain_id(&self) -> BoxFut<'_> { + Box::pin(async move { + let chain_id = EthApiSpec::chain_id(&self.eth.api); + to_raw(&Some(chain_id)) + }) + } + + fn net_version(&self) -> BoxFut<'_> { + Box::pin(async move { + let chain_id = EthApiSpec::chain_id(&self.eth.api); + to_raw(&chain_id.to_string()) + }) + } + + fn syncing(&self) -> BoxFut<'_> { + Box::pin(async move { + let status = EthApiSpec::sync_status(&self.eth.api).map_err(internal)?; + to_raw(&status) + }) + } + + fn coinbase(&self) -> BoxFut<'_> { + Box::pin(async move { to_raw(&self.zone_sequencer().await?) }) + } + + fn gas_price(&self) -> BoxFut<'_> { + Box::pin(async move { + let price = EthFees::gas_price(&self.eth.api).await.map_err(internal)?; + to_raw(&price) + }) + } + + fn max_priority_fee_per_gas(&self) -> BoxFut<'_> { + Box::pin(async move { + let fee = EthFees::suggested_priority_fee(&self.eth.api) + .await + .map_err(internal)?; + to_raw(&fee) + }) + } + + fn fee_history( + &self, + block_count: u64, + newest_block: BlockNumberOrTag, + reward_percentiles: Option>, + ) -> BoxFut<'_> { + Box::pin(async move { + let history = + EthFees::fee_history(&self.eth.api, block_count, newest_block, reward_percentiles) + .await + .map_err(internal)?; + to_raw(&history) + }) + } + + fn get_balance( + &self, + address: Address, + block: Option, + auth: AuthContext, + ) -> BoxFut<'_> { + Box::pin(async move { + // Silent dummy: non-caller addresses get "0x0" to avoid leaking account existence. + if address != auth.caller { + return Ok(raw_zero()); + } + let balance = EthState::balance(&self.eth.api, address, block) + .await + .map_err(internal)?; + to_raw(&balance) + }) + } + + fn get_transaction_count( + &self, + address: Address, + block: Option, + auth: AuthContext, + ) -> BoxFut<'_> { + Box::pin(async move { + // Silent dummy: non-caller addresses get "0x0" to avoid leaking account existence. + if address != auth.caller { + return Ok(raw_zero()); + } + let count = EthState::transaction_count(&self.eth.api, address, block) + .await + .map_err(internal)?; + to_raw(&count) + }) + } + + fn block_by_number( + &self, + number: BlockNumberOrTag, + full: bool, + _auth: AuthContext, + ) -> BoxFut<'_> { + Box::pin(async move { + let block = EthBlocks::rpc_block(&self.eth.api, number.into(), full) + .await + .map_err(internal)?; + + let Some(mut block) = block else { + return Ok(raw_null()); + }; + + redact_block(&mut block); + + to_raw(&block) + }) + } + + fn block_by_hash(&self, hash: B256, full: bool, _auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let block = EthBlocks::rpc_block(&self.eth.api, hash.into(), full) + .await + .map_err(internal)?; + + let Some(mut block) = block else { + return Ok(raw_null()); + }; + + redact_block(&mut block); + + to_raw(&block) + }) + } + + fn transaction_by_hash(&self, hash: B256, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let tx = EthTransactions::transaction_by_hash(&self.eth.api, hash) + .await + .map_err(internal)? + .map(|src| src.into_transaction(self.eth.api.converter())) + .transpose() + .map_err(internal)?; + + let Some(tx) = tx else { return Ok(raw_null()) }; + + if tx.from() != auth.caller { + return Ok(raw_null()); + } + + to_raw(&tx) + }) + } + + fn transaction_receipt(&self, hash: B256, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let receipt = EthTransactions::transaction_receipt(&self.eth.api, hash) + .await + .map_err(internal)?; + + let Some(mut receipt) = receipt else { + return Ok(raw_null()); + }; + + if receipt.from() != auth.caller { + return Ok(raw_null()); + } + + receipt = zone_rpc::filter::filter_receipt_logs(receipt); + + to_raw(&receipt) + }) + } + + fn call( + &self, + mut request: TempoTransactionRequest, + block: Option, + state_override: Option, + auth: AuthContext, + ) -> BoxFut<'_> { + Box::pin(async move { + if state_override.is_some() { + return Err(JsonRpcError::invalid_params("state overrides not allowed")); + } + + zone_rpc::policy::enforce_from(&mut request, &auth)?; + zone_rpc::policy::enforce_no_contract_creation(&request)?; + + let result = EthCall::call( + &self.eth.api, + request, + block, + EvmOverrides::state(state_override), + ) + .await + .map_err(internal)?; + to_raw(&result) + }) + } + + fn estimate_gas( + &self, + mut request: TempoTransactionRequest, + block: Option, + state_override: Option, + auth: AuthContext, + ) -> BoxFut<'_> { + Box::pin(async move { + if state_override.is_some() { + return Err(JsonRpcError::invalid_params("state overrides not allowed")); + } + + zone_rpc::policy::enforce_from(&mut request, &auth)?; + + zone_rpc::policy::enforce_no_contract_creation(&request)?; + + let result = EthCall::estimate_gas_at( + &self.eth.api, + request, + block.unwrap_or_default(), + EvmOverrides::state(state_override), + ) + .await + .map_err(internal)?; + to_raw(&result) + }) + } + + fn send_raw_transaction(&self, data: Bytes, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + zone_rpc::policy::verify_raw_tx_sender(&data, &auth)?; + + let hash = EthTransactions::send_raw_transaction(&self.eth.api, data) + .await + .map_err(internal)?; + to_raw(&hash) + }) + } + + fn send_raw_transaction_sync(&self, data: Bytes, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + zone_rpc::policy::verify_raw_tx_sender(&data, &auth)?; + + let mut receipt = EthTransactions::send_raw_transaction_sync(&self.eth.api, data) + .await + .map_err(internal)?; + + receipt = zone_rpc::filter::filter_receipt_logs(receipt); + + to_raw(&receipt) + }) + } + + fn fill_transaction( + &self, + mut request: TempoTransactionRequest, + auth: AuthContext, + ) -> BoxFut<'_> { + Box::pin(async move { + zone_rpc::policy::enforce_from(&mut request, &auth)?; + zone_rpc::policy::enforce_no_contract_creation(&request)?; + + let result = EthTransactions::fill_transaction(&self.eth.api, request) + .await + .map_err(internal)?; + to_raw(&result) + }) + } + + fn get_logs(&self, mut filter: Filter, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let zone_tokens = self.zone_tokens().await?; + zone_rpc::filter::scope_filter_addresses(&mut filter, &zone_tokens)?; + zone_rpc::filter::scope_filter(&mut filter); + let logs = EthFilterApiServer::logs(&self.eth.filter, filter) + .await + .map_err(internal)?; + let filtered = zone_rpc::filter::filter_logs(logs, &auth.caller); + to_raw(&filtered) + }) + } + + fn new_filter(&self, mut filter: Filter, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let zone_tokens = self.zone_tokens().await?; + zone_rpc::filter::scope_filter_addresses(&mut filter, &zone_tokens)?; + zone_rpc::filter::scope_filter(&mut filter); + let id = EthFilterApiServer::new_filter(&self.eth.filter, filter) + .await + .map_err(internal)?; + self.filter_owners + .lock() + .await + .insert(id.clone(), auth.caller); + to_raw(&id) + }) + } + + fn get_filter_logs(&self, id: FilterId, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + self.ensure_filter_owner(&id, &auth).await?; + + let logs = self + .filter() + .filter_logs(id) + .await + .map_err(map_eth_filter_error)?; + + let filtered = zone_rpc::filter::filter_logs(logs, &auth.caller); + to_raw(&filtered) + }) + } + + fn get_filter_changes(&self, id: FilterId, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + self.ensure_filter_owner(&id, &auth).await?; + + let changes = self + .filter() + .filter_changes(id) + .await + .map_err(map_eth_filter_error)?; + + match changes { + FilterChanges::Logs(logs) => { + let filtered = zone_rpc::filter::filter_logs(logs, &auth.caller); + to_raw(&FilterChanges::< + alloy_rpc_types_eth::Transaction, + >::Logs(filtered)) + } + FilterChanges::Hashes(hashes) => to_raw(&FilterChanges::< + alloy_rpc_types_eth::Transaction, + >::Hashes(hashes)), + // Pending transaction filters are disabled — return empty if one somehow exists + FilterChanges::Transactions(_) => to_raw( + &FilterChanges::>::Empty, + ), + FilterChanges::Empty => to_raw( + &FilterChanges::>::Empty, + ), + } + }) + } + + fn new_block_filter(&self, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let id = EthFilterApiServer::new_block_filter(&self.eth.filter) + .await + .map_err(internal)?; + self.filter_owners + .lock() + .await + .insert(id.clone(), auth.caller); + to_raw(&id) + }) + } + + fn uninstall_filter(&self, id: FilterId, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + self.ensure_filter_owner(&id, &auth).await?; + + let result = EthFilterApiServer::uninstall_filter(&self.eth.filter, id.clone()) + .await + .map_err(internal)?; + + if result || !self.filter_is_active(&id).await { + self.filter_owners.lock().await.remove(&id); + } + + to_raw(&result) + }) + } + + fn ws_subscribe_new_heads(&self, _auth: AuthContext) -> BoxWsSubscriptionFut<'_> { + Box::pin(async move { + let api = self.eth.api.clone(); + let provider = self.eth.api.provider().clone(); + let stream = provider + .canonical_state_stream() + .flat_map(move |new_chain| { + let api = api.clone(); + let headers = new_chain + .committed() + .blocks_iter() + .filter_map(move |block| { + match api + .converter() + .convert_header(block.clone_sealed_header(), block.rlp_length()) + { + Ok(header) => Some(header), + Err(err) => { + tracing::error!( + target: "rpc", + %err, + "Failed to convert header" + ); + None + } + } + }) + .collect::>(); + futures::stream::iter(headers) + }) + .map(move |mut header| { + redact_ws_header(&mut header); + to_raw(&header) + }); + let stream: zone_rpc::WsSubscriptionStream = Box::pin(stream); + Ok(stream) + }) + } + + fn ws_subscribe_logs(&self, mut filter: Filter, auth: AuthContext) -> BoxWsSubscriptionFut<'_> { + Box::pin(async move { + let provider = self.eth.api.provider().clone(); + let caller = auth.caller; + + let zone_tokens = self.zone_tokens().await?; + zone_rpc::filter::scope_filter_addresses(&mut filter, &zone_tokens)?; + zone_rpc::filter::scope_filter(&mut filter); + + let stream = provider + .canonical_state_stream() + .flat_map(|canon_state| futures::stream::iter(canon_state.block_receipts())) + .flat_map(move |(block_receipts, removed)| { + let all_logs = logs_utils::matching_block_logs_with_tx_hashes( + &filter, + block_receipts.block, + block_receipts.timestamp, + block_receipts + .tx_receipts + .iter() + .map(|(tx, receipt)| (*tx, receipt)), + removed, + ); + futures::stream::iter(all_logs) + }); + + let stream = stream.filter_map(move |log| { + std::future::ready( + zone_rpc::filter::is_log_visible(&log, &caller).then(|| to_raw(&log)), + ) + }); + let stream: zone_rpc::WsSubscriptionStream = Box::pin(stream); + Ok(stream) + }) + } + + fn zone_get_authorization_token_info(&self, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + to_raw(&AuthorizationTokenInfoResponse { + account: auth.caller, + expires_at: U64::from(auth.expires_at), + }) + }) + } + + fn zone_get_zone_info(&self, _auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let zone_tokens = self.zone_tokens().await?; + let sequencer = self.zone_sequencer().await?; + to_raw(&ZoneInfoResponse { + zone_id: U64::from(self.config.zone_id), + zone_tokens, + sequencer, + chain_id: U64::from(self.config.chain_id), + }) + }) + } + + fn zone_get_deposit_status(&self, tempo_block_number: u64, auth: AuthContext) -> BoxFut<'_> { + Box::pin(async move { + let zone_processed_through = self + .tempo_state + .tempoBlockNumber() + .call() + .await + .map_err(internal)?; + let portal_deposits = self.portal_deposits_for_block(tempo_block_number).await?; + + let mut deposits = Vec::new(); + for deposit in portal_deposits { + match deposit { + PortalDepositRecord::Regular { + deposit_hash, + sender, + recipient, + token, + amount, + memo, + } => { + if sender != auth.caller && recipient != auth.caller { + continue; + } + + let terminal = self.terminal_event_for_deposit(deposit_hash).await?; + let status = regular_deposit_status(terminal)?; + + deposits.push(DepositStatusEntry { + deposit_hash, + kind: DepositKind::Regular, + token, + sender, + recipient: Some(recipient), + amount: U256::from(amount), + memo: Some(memo), + status, + }); + } + PortalDepositRecord::Encrypted { + deposit_hash, + sender, + token, + amount, + } => { + let terminal = self.terminal_event_for_deposit(deposit_hash).await?; + + let include = match (&terminal, sender == auth.caller) { + (_, true) => true, + ( + Some(TerminalDepositEvent::EncryptedProcessed { + recipient, .. + }), + false, + ) => *recipient == auth.caller, + _ => false, + }; + + if !include { + continue; + } + + let (recipient, memo, status) = encrypted_deposit_details(terminal)?; + + deposits.push(DepositStatusEntry { + deposit_hash, + kind: DepositKind::Encrypted, + token, + sender, + recipient, + amount: U256::from(amount), + memo, + status, + }); + } + } + } + + let processed = zone_processed_through >= tempo_block_number + && deposits + .iter() + .all(|deposit| deposit.status != DepositState::Pending); + + to_raw(&DepositStatusResponse { + tempo_block_number: U64::from(tempo_block_number), + zone_processed_through: U64::from(zone_processed_through), + processed, + deposits, + }) + }) + } +} + +#[derive(Debug, Clone)] +enum PortalDepositRecord { + Regular { + deposit_hash: B256, + sender: Address, + recipient: Address, + token: Address, + amount: u128, + memo: B256, + }, + Encrypted { + deposit_hash: B256, + sender: Address, + token: Address, + amount: u128, + }, +} + +#[derive(Debug, Clone)] +enum TerminalDepositEvent { + RegularProcessed, + RegularFailed, + RegularRejected, + EncryptedProcessed { recipient: Address, memo: B256 }, + EncryptedFailed, + EncryptedRejected, +} + +fn regular_deposit_status( + terminal: Option, +) -> Result { + match terminal { + Some(TerminalDepositEvent::RegularProcessed) => Ok(DepositState::Processed), + Some(TerminalDepositEvent::RegularFailed | TerminalDepositEvent::RegularRejected) => { + Ok(DepositState::Failed) + } + Some(TerminalDepositEvent::EncryptedProcessed { .. }) => Err(JsonRpcError::internal( + "encrypted deposit event matched regular deposit hash", + )), + Some(TerminalDepositEvent::EncryptedFailed | TerminalDepositEvent::EncryptedRejected) => { + Err(JsonRpcError::internal( + "encrypted deposit failure matched regular deposit hash", + )) + } + None => Ok(DepositState::Pending), + } +} + +fn encrypted_deposit_details( + terminal: Option, +) -> Result<(Option
, Option, DepositState), JsonRpcError> { + match terminal { + Some(TerminalDepositEvent::EncryptedProcessed { recipient, memo }) => { + Ok((Some(recipient), Some(memo), DepositState::Processed)) + } + Some(TerminalDepositEvent::EncryptedFailed | TerminalDepositEvent::EncryptedRejected) => { + Ok((None, None, DepositState::Failed)) + } + Some( + TerminalDepositEvent::RegularProcessed + | TerminalDepositEvent::RegularFailed + | TerminalDepositEvent::RegularRejected, + ) => Err(JsonRpcError::internal( + "regular deposit event matched encrypted deposit hash", + )), + None => Ok((None, None, DepositState::Pending)), + } +} + +fn redact_tempo_header(header: &mut TempoHeader) { + header.inner.logs_bloom = Bloom::ZERO; +} + +fn redact_ws_header(header: &mut TempoHeaderResponse) { + redact_tempo_header(&mut header.inner.inner); +} + +/// Strip privacy-sensitive fields from a block for non-sequencer callers. +fn redact_block(block: &mut RpcBlock) { + redact_tempo_header(&mut block.header.inner); + block.transactions = BlockTransactions::Hashes(Vec::new()); +} + +pub(crate) fn rpc_connection_config(retry_connection_interval: Duration) -> ConnectionConfig { + ConnectionConfig::new() + .with_max_retries(u32::MAX) + .with_retry_interval(retry_connection_interval) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn regular_deposit_status_maps_terminal_events() { + assert_eq!( + regular_deposit_status(Some(TerminalDepositEvent::RegularProcessed)).unwrap(), + DepositState::Processed + ); + assert_eq!(regular_deposit_status(None).unwrap(), DepositState::Pending); + } + + #[test] + fn regular_deposit_status_rejects_encrypted_terminal_events() { + let err = regular_deposit_status(Some(TerminalDepositEvent::EncryptedFailed)).unwrap_err(); + assert_eq!( + err.message, + "encrypted deposit failure matched regular deposit hash" + ); + } + + #[test] + fn encrypted_deposit_details_maps_terminal_events() { + let recipient = Address::repeat_byte(0x11); + let memo = B256::from([0x22; 32]); + + assert_eq!( + encrypted_deposit_details(Some(TerminalDepositEvent::EncryptedProcessed { + recipient, + memo, + })) + .unwrap(), + (Some(recipient), Some(memo), DepositState::Processed) + ); + assert_eq!( + encrypted_deposit_details(Some(TerminalDepositEvent::EncryptedFailed)).unwrap(), + (None, None, DepositState::Failed) + ); + assert_eq!( + encrypted_deposit_details(None).unwrap(), + (None, None, DepositState::Pending) + ); + } + + #[test] + fn encrypted_deposit_details_rejects_regular_terminal_events() { + let err = + encrypted_deposit_details(Some(TerminalDepositEvent::RegularProcessed)).unwrap_err(); + assert_eq!( + err.message, + "regular deposit event matched encrypted deposit hash" + ); + } + + #[test] + fn stale_filter_owner_ids_removes_only_inactive_entries() { + let active_ids = HashSet::from([ + FilterId::Str("0xactive".to_string()), + FilterId::Str("0xkeep".to_string()), + ]); + let owner_ids = vec![ + FilterId::Str("0xactive".to_string()), + FilterId::Str("0xstale".to_string()), + FilterId::Str("0xkeep".to_string()), + ]; + + let stale_ids = stale_filter_owner_ids(owner_ids, &active_ids); + + assert_eq!(stale_ids, vec![FilterId::Str("0xstale".to_string())]); + } + + #[test] + fn stale_filter_owner_ids_is_noop_for_empty_owner_set() { + let stale_ids = stale_filter_owner_ids(Vec::new(), &HashSet::new()); + + assert!(stale_ids.is_empty()); + } +} diff --git a/crates/tempo-zone/Cargo.toml b/crates/tempo-zone/Cargo.toml index 12444a38..522af731 100644 --- a/crates/tempo-zone/Cargo.toml +++ b/crates/tempo-zone/Cargo.toml @@ -12,64 +12,15 @@ workspace = true [dependencies] # tempo -tempo-chainspec.workspace = true -tempo-evm = { workspace = true, features = ["rpc", "engine"] } -tempo-node.workspace = true -tempo-primitives.workspace = true -tempo-transaction-pool.workspace = true -tempo-alloy.workspace = true -tempo-contracts.workspace = true tempo-zone-contracts = { workspace = true, features = ["std", "serde", "rpc"] } zone-evm.workspace = true zone-l1.workspace = true +zone-node.workspace = true zone-payload.workspace = true -zone-primitives = { workspace = true, features = ["std", "serde"] } -zone-rpc.workspace = true zone-sequencer.workspace = true -# reth -reth-chainspec.workspace = true -reth-eth-wire-types.workspace = true -reth-evm.workspace = true -reth-metrics.workspace = true -reth-node-api.workspace = true -reth-node-builder.workspace = true -reth-payload-builder.workspace = true -reth-payload-primitives.workspace = true -reth-primitives-traits.workspace = true -reth-provider.workspace = true -reth-rpc.workspace = true -reth-rpc-builder.workspace = true -reth-rpc-eth-api.workspace = true -reth-rpc-eth-types.workspace = true -reth-storage-api.workspace = true -reth-tasks.workspace = true -reth-transaction-pool.workspace = true - -# alloy -alloy-consensus.workspace = true -alloy-network.workspace = true -alloy-primitives.workspace = true -alloy-provider.workspace = true -alloy-rpc-client.workspace = true -alloy-rpc-types-engine.workspace = true -alloy-rpc-types-eth.workspace = true -alloy-signer-local.workspace = true -alloy-sol-types.workspace = true - # misc -clap = { workspace = true, optional = true } -k256.workspace = true -reth-consensus = { workspace = true, optional = true } -reth-ethereum = { workspace = true, features = [ - "node", - "cli", -], optional = true } -reth-tracing = { workspace = true, optional = true } eyre.workspace = true -futures.workspace = true -tokio.workspace = true -tracing.workspace = true [dev-dependencies] alloy = { workspace = true, features = [ @@ -81,21 +32,41 @@ alloy = { workspace = true, features = [ "signer-mnemonic-all-languages", "rpc-types", ] } +alloy-consensus.workspace = true alloy-contract.workspace = true alloy-eips.workspace = true alloy-evm.workspace = true +alloy-network.workspace = true alloy-primitives.workspace = true +alloy-provider.workspace = true alloy-rlp.workspace = true +alloy-rpc-client.workspace = true alloy-rpc-types-engine.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-signer-local.workspace = true alloy-sol-types.workspace = true base64.workspace = true const-hex.workspace = true +futures.workspace = true +k256.workspace = true p256.workspace = true rand.workspace = true +reth-chainspec.workspace = true +reth-eth-wire-types.workspace = true reth-ethereum = { workspace = true, features = ["node", "test-utils"] } +reth-node-api.workspace = true reth-node-builder.workspace = true reth-node-core.workspace = true +reth-payload-builder.workspace = true +reth-payload-primitives.workspace = true +reth-primitives-traits.workspace = true +reth-provider.workspace = true reth-rpc-builder.workspace = true +reth-rpc-eth-api.workspace = true +reth-rpc-eth-types.workspace = true +reth-storage-api.workspace = true +reth-tasks.workspace = true +reth-transaction-pool.workspace = true reth-tracing.workspace = true serde = { workspace = true, features = ["derive"] } alloy-signer.workspace = true @@ -103,34 +74,28 @@ reqwest.workspace = true revm.workspace = true serde_json.workspace = true thiserror.workspace = true +tempo-alloy.workspace = true tempo-chainspec.workspace = true tempo-contracts.workspace = true +tempo-evm = { workspace = true, features = ["rpc", "engine"] } +tempo-node.workspace = true tempo-payload-types.workspace = true +tempo-primitives.workspace = true tempo-revm.workspace = true +tempo-transaction-pool.workspace = true sha2.workspace = true tempo-precompiles = { workspace = true, features = ["test-utils"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } tokio-tungstenite.workspace = true +tracing.workspace = true url = "2" +zone-primitives = { workspace = true, features = ["std", "serde"] } [features] default = [] -cli = [ - "dep:clap", - "dep:reth-consensus", - "dep:reth-ethereum", - "dep:reth-tracing", - "tempo-chainspec/cli", -] -jemalloc = ["reth-ethereum?/jemalloc"] +cli = ["zone-node/cli"] +jemalloc = ["zone-node/jemalloc"] test-utils = [ - "reth-chainspec/test-utils", - "reth-ethereum/test-utils", - "reth-node-builder/test-utils", - "reth-payload-builder/test-utils", - "reth-primitives-traits/test-utils", - "reth-provider/test-utils", - "reth-transaction-pool/test-utils", + "zone-node/test-utils", "tempo-precompiles/test-utils", - "tempo-transaction-pool/test-utils", ] diff --git a/crates/tempo-zone/src/cli.rs b/crates/tempo-zone/src/cli.rs index dca6d0a4..f306d35a 100644 --- a/crates/tempo-zone/src/cli.rs +++ b/crates/tempo-zone/src/cli.rs @@ -1,200 +1,3 @@ -//! Tempo Zone CLI. +//! Re-exports for the Tempo Zone CLI. -use std::{sync::Arc, time::Duration}; - -use alloy_primitives::Address; -use alloy_signer_local::PrivateKeySigner; -use clap::{Args, Parser}; -use reth_consensus::noop::NoopConsensus; -use reth_ethereum::cli::Cli; -use reth_tracing::tracing::info; -use tempo_chainspec::spec::{TempoChainSpec, TempoChainSpecParser}; -use zone_evm::ZoneEvmConfig; - -use crate::{ - BatchAnchorConfig, ZoneNode, ZonePrivateRpcConfig, ZoneSequencerAddOnsConfig, - rpc::auth::DEFAULT_MAX_AUTH_TOKEN_VALIDITY_SECS, -}; - -const MAX_LOGS_PER_RESPONSE: u64 = 1_000_000; -const MAX_BLOCKS_PER_FILTER: u64 = 1_000_000; - -const ZONE_LOG_FILTER_DIRECTIVES: &str = concat!( - "tungstenite=warn,", - "alloy_pubsub=warn,", - "alloy_transport_ws=warn,", - "rustls::client=warn" -); - -/// Tempo Zone CLI entry point. -pub struct ZoneCli(Cli); - -impl ZoneCli { - /// Parse CLI arguments from the environment. - pub fn parse() -> Self { - Self(Cli::parse()) - } - - /// Run the Tempo Zone node. - /// - /// Configures the node builder, launches the zone node with all sequencer - /// background tasks, and blocks until exit. - pub fn run(self) -> eyre::Result<()> { - let mut cli = self.0; - - prepend_log_filter(&mut cli.logs.log_stdout_filter, ZONE_LOG_FILTER_DIRECTIVES); - prepend_log_filter(&mut cli.logs.log_file_filter, ZONE_LOG_FILTER_DIRECTIVES); - - let components = |spec: Arc| { - ( - ZoneEvmConfig::new_without_l1(spec), - NoopConsensus::default(), - ) - }; - - cli.run_with_components::(components, async move |mut builder, args| { - info!(target: "reth::cli", "Launching Tempo Zone node"); - - builder.config_mut().network.discovery.disable_discovery = true; - builder.config_mut().rpc.disable_auth_server = true; - builder.config_mut().rpc.rpc_max_logs_per_response = MAX_LOGS_PER_RESPONSE.into(); - builder.config_mut().rpc.rpc_max_blocks_per_filter = MAX_BLOCKS_PER_FILTER.into(); - - let mut node = ZoneNode::new( - args.l1_rpc_url, - args.portal_address, - args.l1_genesis_block_number, - args.l1_fetch_concurrency, - Duration::from_millis(args.l1_retry_connection_interval_ms), - ) - .with_private_rpc(ZonePrivateRpcConfig { - private_rpc_port: args.private_rpc_port, - zone_id: args.zone_id, - max_auth_token_validity: Duration::from_secs( - args.private_rpc_max_auth_token_validity_secs, - ), - }); - - if args.enable_sequencer { - let sequencer_signer: PrivateKeySigner = args - .sequencer_key - .parse() - .expect("invalid sequencer private key"); - node = node.with_sequencer(ZoneSequencerAddOnsConfig { - sequencer_signer, - zone_id: args.zone_id, - zone_poll_interval: Duration::from_secs(args.zone_poll_interval_secs), - batch_interval: Duration::from_secs(args.zone_batch_interval_secs), - batch_anchor_config: BatchAnchorConfig::default(), - withdrawal_poll_interval: Duration::from_secs( - args.withdrawal_poll_interval_secs, - ), - }); - } - - let handle = builder.node(node).launch_with_debug_capabilities().await?; - handle.wait_for_node_exit().await - }) - } -} - -/// Tempo Zone CLI arguments. -#[derive(Debug, Clone, Args)] -pub struct ZoneArgs { - /// L1 WebSocket RPC URL for subscribing to deposit events and chain notifications. - #[arg(long = "l1.rpc-url", env = "L1_RPC_URL")] - pub l1_rpc_url: String, - - /// ZonePortal contract address on L1. - #[arg(long = "l1.portal-address", env = "L1_PORTAL_ADDRESS")] - pub portal_address: Address, - - /// Block building interval in milliseconds. - #[arg( - long = "block.interval-ms", - env = "BLOCK_INTERVAL_MS", - default_value_t = 250 - )] - pub block_interval_ms: u64, - - /// Sequencer private key (hex, with or without 0x prefix). - #[arg(long = "sequencer-key", env = "SEQUENCER_KEY")] - pub sequencer_key: String, - - /// How often (in seconds) the zone monitor polls for new L2 blocks. - #[arg( - long = "zone.poll-interval-secs", - env = "ZONE_POLL_INTERVAL_SECS", - default_value_t = 1 - )] - pub zone_poll_interval_secs: u64, - - /// Maximum time (in seconds) to accumulate zone blocks before submitting a batch to L1. - #[arg( - long = "zone.batch-interval-secs", - env = "ZONE_BATCH_INTERVAL_SECS", - default_value_t = 60 - )] - pub zone_batch_interval_secs: u64, - - /// How often (in seconds) the withdrawal processor polls the L1 queue. - #[arg( - long = "withdrawal-poll-interval-secs", - env = "WITHDRAWAL_POLL_INTERVAL_SECS", - default_value_t = 5 - )] - pub withdrawal_poll_interval_secs: u64, - - /// Genesis Tempo L1 block number override. - #[arg(long = "l1.genesis-block-number", env = "L1_GENESIS_BLOCK_NUMBER")] - pub l1_genesis_block_number: Option, - - /// Maximum number of concurrent L1 receipt fetches. - #[arg( - long = "l1.fetch-concurrency", - env = "L1_FETCH_CONCURRENCY", - default_value_t = 4 - )] - pub l1_fetch_concurrency: usize, - - /// Interval in milliseconds between WebSocket reconnection attempts to L1. - #[arg( - long = "l1.retry-connection-interval", - env = "L1_RETRY_CONNECTION_INTERVAL_MS", - default_value_t = 100 - )] - pub l1_retry_connection_interval_ms: u64, - - /// Zone ID for the private RPC auth token validation. - #[arg(long = "zone.id", env = "ZONE_ID", default_value_t = 0)] - pub zone_id: u32, - - /// Port for the private zone RPC server (0 for OS-assigned). - #[arg( - long = "private-rpc.port", - env = "PRIVATE_RPC_PORT", - default_value_t = 8544 - )] - pub private_rpc_port: u16, - - /// Maximum auth token validity window the private RPC accepts, in seconds. - #[arg( - long = "private-rpc.max-auth-token-validity-secs", - env = "PRIVATE_RPC_MAX_AUTH_TOKEN_VALIDITY_SECS", - default_value_t = DEFAULT_MAX_AUTH_TOKEN_VALIDITY_SECS - )] - pub private_rpc_max_auth_token_validity_secs: u64, - - /// Enable the Zone node in sequencer mode. This advances block production and submits - /// withdrawal batches. - #[arg(long = "sequencer", env = "SEQUENCER")] - pub enable_sequencer: bool, -} - -fn prepend_log_filter(filter: &mut String, directives: &str) { - if filter.is_empty() { - *filter = directives.to_owned(); - } else { - *filter = format!("{directives},{filter}"); - } -} +pub use zone_node::cli::*; diff --git a/crates/tempo-zone/src/engine.rs b/crates/tempo-zone/src/engine.rs index 11511f8f..fb312f0f 100644 --- a/crates/tempo-zone/src/engine.rs +++ b/crates/tempo-zone/src/engine.rs @@ -1,296 +1,3 @@ -//! Zone Engine — L1-event-driven block production for zone nodes. -//! -//! Advances the zone chain whenever new L1 blocks arrive in the deposit -//! queue, enabling full-speed sync during catch-up and instant reaction in -//! steady state. -//! -//! ## Block production flow -//! -//! ```text -//! L1Subscriber ──enqueue──► DepositQueue ──notify──► ZoneEngine -//! │ │ -//! │ 1. peek queue → L1 block -//! │ 2. build ZonePayloadAttributes -//! │ (inner attrs + l1_block) -//! │ 3. FCU w/ payload attributes -//! │ │ -//! │ ▼ -//! │ reth payload service -//! │ │ -//! │ 4. build payload -//! │ (L1 data from attributes) -//! │ │ -//! │ ▼ -//! │ ZoneEngine -//! │ 5. resolve payload -//! │ 6. newPayload -//! │ 7. FCU (update head) -//! │ │ -//! ◄── confirm ◄───────────┘ -//! ``` -//! -//! The deposit queue uses a **peek / confirm** pattern: the engine peeks at -//! the next L1 block, wraps it into [`ZonePayloadAttributes`], and only -//! confirms (removes) the block after `newPayload` succeeds. A failed build -//! leaves the block in the queue for retry. -//! -//! The zone assumes **instant finality** — head, safe, and finalized all point -//! to the same block. +//! Re-exports for zone block production. -use alloy_consensus::BlockHeader as _; -use alloy_primitives::{Address, B256}; -use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes as EthPayloadAttributes}; -use eyre::OptionExt; -use reth_chainspec::EthereumHardforks; -use reth_node_builder::ConsensusEngineHandle; -use reth_payload_builder::PayloadBuilderHandle; -use reth_payload_primitives::{BuiltPayload, PayloadKind, PayloadTypes}; -use reth_primitives_traits::SealedHeader; -use std::{sync::Arc, time::Duration}; -use tempo_chainspec::spec::TempoChainSpec; -use tempo_primitives::TempoHeader; -use tracing::{error, warn}; - -use crate::{ - DepositQueue, L1BlockDeposits, - payload::{ZonePayloadAttributes, ZonePayloadTypes}, -}; - -/// Engine that drives L2 block production from L1 events. -/// -/// Waits for L1 blocks in the [`DepositQueue`], then for each block: -/// 1. Peeks the L1 block from the queue -/// 2. Builds [`ZonePayloadAttributes`] wrapping inner Tempo attrs + L1 data -/// 3. Sends FCU with payload attributes to start a build -/// 4. Resolves the built payload -/// 5. Submits via `newPayload` -/// 6. Confirms the L1 block in the queue (removes it) -/// -/// On failure the L1 block stays in the queue and is retried. -#[derive(Debug)] -pub struct ZoneEngine { - /// Chain spec for hardfork checks when building attributes. - chain_spec: Arc, - /// Engine API handle for FCU and newPayload. - to_engine: ConsensusEngineHandle, - /// Payload builder handle. - payload_builder: PayloadBuilderHandle, - /// Queue of L1 blocks with their deposits. - deposit_queue: DepositQueue, - /// Latest block header — used as parent for the next payload and as the - /// head/safe/finalized hash in FCU (instant finality). - last_header: SealedHeader, - /// Address that receives block fees. - fee_recipient: Address, - /// Sequencer's secp256k1 secret key for ECIES decryption of encrypted deposits. - sequencer_key: k256::SecretKey, - /// ZonePortal address on L1 — used as context in HKDF key derivation. - portal_address: Address, - /// Cache-first, RPC-fallback TIP-403 policy provider for authorization checks - /// on encrypted deposit recipients during preparation. - policy_provider: crate::l1_state::PolicyProvider, -} - -impl ZoneEngine { - pub fn new( - chain_spec: Arc, - to_engine: ConsensusEngineHandle, - payload_builder: PayloadBuilderHandle, - deposit_queue: DepositQueue, - last_header: SealedHeader, - fee_recipient: Address, - sequencer_key: k256::SecretKey, - portal_address: Address, - policy_provider: crate::l1_state::PolicyProvider, - ) -> Self { - Self { - chain_spec, - to_engine, - payload_builder, - deposit_queue, - last_header, - fee_recipient, - sequencer_key, - portal_address, - policy_provider, - } - } - - /// Runs the main Zone engine loop. - /// - /// This method never returns under normal operation. It: - /// 1. Waits for L1 blocks to arrive in the deposit queue - /// 2. Advances the zone chain for each available L1 block (no delay between blocks) - /// 3. Sends periodic FCU heartbeats - pub async fn run(mut self) { - let mut fcu_interval = tokio::time::interval(Duration::from_secs(1)); - fcu_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - // Send initial FCU to establish head - if let Err(e) = self.update_forkchoice_state().await { - error!(target: "zone::engine", "Error sending initial FCU: {:?}", e); - } - - loop { - tokio::select! { - // Wait for new L1 blocks in the deposit queue - _ = self.deposit_queue.notified() => { - self.advance_all_available().await; - } - // Periodic FCU heartbeat — also drains any blocks we missed - _ = fcu_interval.tick() => { - self.advance_all_available().await; - if let Err(e) = self.update_forkchoice_state().await { - error!(target: "zone::engine", "Error updating fork choice: {:?}", e); - } - } - } - } - } - - /// Returns the current forkchoice state. - /// - /// The zone has instant finality so head = safe = finalized. - fn forkchoice_state(&self) -> ForkchoiceState { - ForkchoiceState::same_hash(self.last_header.hash()) - } - - /// Send an FCU without payload attributes (heartbeat). - async fn update_forkchoice_state(&self) -> eyre::Result<()> { - let state = self.forkchoice_state(); - let res = self.to_engine.fork_choice_updated(state, None).await?; - - if !res.is_valid() { - eyre::bail!("Invalid fork choice update {state:?}: {res:?}") - } - - Ok(()) - } - - /// Advance the chain for all available L1 blocks in the queue. - /// - /// During catch-up this processes blocks as fast as the EVM can execute - /// them, with no timer delays between blocks. - /// - /// Reorg safety is handled upstream by the L1 subscriber, which only - /// enqueues blocks once they are confirmed by a successor. - async fn advance_all_available(&mut self) { - while let Some(l1_block) = self.deposit_queue.peek() { - if let Err(e) = self.advance(l1_block).await { - error!(target: "zone::engine", "Error advancing the chain: {:?}", e); - tokio::time::sleep(Duration::from_millis(100)).await; - break; - } - } - } - - /// Decrypt encrypted deposits, check TIP-403 policy authorization, and - /// ABI-encode everything into a [`PreparedL1Block`] ready for the payload - /// builder. Errors (e.g. policy RPC failures) are propagated so the engine - /// retries rather than allowing unauthorized deposits through. - async fn prepare_l1_block( - &self, - l1_block: L1BlockDeposits, - ) -> eyre::Result { - l1_block - .prepare( - &self.sequencer_key, - self.portal_address, - &self.policy_provider, - ) - .await - } - - /// Advance the chain by one block. - /// - /// Wraps the given L1 block into [`ZonePayloadAttributes`], sends FCU - /// with those attributes, waits for the payload to be built, then submits - /// via `newPayload`. Only confirms (removes) the L1 block from the - /// deposit queue after `newPayload` succeeds. - async fn advance(&mut self, l1_block: L1BlockDeposits) -> eyre::Result<()> { - let l1_num_hash = l1_block.header.num_hash(); - - // Zone block timestamp is locked to the L1 block's timestamp so the - // two chains stay in lockstep. - let timestamp_secs = l1_block.header.timestamp(); - let timestamp_millis_part = l1_block.header.timestamp_millis_part; - - let l1_block = self.prepare_l1_block(l1_block).await?; - - let attributes = ZonePayloadAttributes { - inner: EthPayloadAttributes { - timestamp: timestamp_secs, - prev_randao: B256::ZERO, - suggested_fee_recipient: self.fee_recipient, - withdrawals: self - .chain_spec - .is_shanghai_active_at_timestamp(timestamp_secs) - .then(Default::default), - parent_beacon_block_root: self - .chain_spec - .is_cancun_active_at_timestamp(timestamp_secs) - .then_some(B256::ZERO), - slot_number: None, - target_gas_limit: None, - }, - timestamp_millis_part, - l1_block, - }; - - // Send FCU with payload attributes through the engine API to trigger - // payload building. The forkchoice state points at the current head; - // the attributes carry the L1 block data for the new zone block. - let res = self - .to_engine - .fork_choice_updated(self.forkchoice_state(), Some(attributes)) - .await?; - - if res.is_invalid() { - eyre::bail!("Invalid payload status") - } - - let payload_id = res.payload_id.ok_or_eyre("No payload id")?; - - let Some(Ok(payload)) = self - .payload_builder - .resolve_kind(payload_id, PayloadKind::WaitForPending) - .await - else { - eyre::bail!("No payload") - }; - - let header = payload.block().sealed_header().clone(); - let block_number = header.number(); - let payload = ZonePayloadTypes::block_to_payload(payload.block().clone(), None); - let res = self.to_engine.new_payload(payload).await?; - - if !res.is_valid() { - eyre::bail!("Invalid payload for block {block_number}") - } - - // newPayload succeeded — confirm the L1 block in the queue so it is - // removed. If the queue was reorged between peek and confirm, the - // block was already purged; log a warning but still update - // last_header since the zone chain has advanced. - if self.deposit_queue.confirm(l1_num_hash).is_none() { - warn!(target: "zone::engine", ?l1_num_hash, "L1 block was purged from queue during build"); - } - - // GC stale versioned entries from the policy cache. Only the engine - // drives this — the subscriber must not advance past blocks the engine - // hasn't processed yet, otherwise policy lookups for in-flight blocks - // could return wrong results. - self.policy_provider.cache().advance(l1_num_hash.number); - - self.last_header = header; - - // Canonicalize the new head — FCU-with-attrs above only set the - // *previous* head as canonical; this bare FCU makes the just-built - // block the EL's canonical head. - if let Err(e) = self.update_forkchoice_state().await { - error!(target: "zone::engine", "Error sending post-newPayload FCU: {:?}", e); - } - - Ok(()) - } -} +pub use zone_node::engine::*; diff --git a/crates/tempo-zone/src/lib.rs b/crates/tempo-zone/src/lib.rs index f0eef0dd..1d8429b8 100644 --- a/crates/tempo-zone/src/lib.rs +++ b/crates/tempo-zone/src/lib.rs @@ -4,8 +4,6 @@ #![allow(unnameable_types)] #![allow(clippy::too_many_arguments)] use eyre as _; -use reth_evm as _; -use reth_metrics as _; pub mod abi; #[cfg(feature = "cli")] diff --git a/crates/tempo-zone/src/node.rs b/crates/tempo-zone/src/node.rs index c117161f..fadf0d9e 100644 --- a/crates/tempo-zone/src/node.rs +++ b/crates/tempo-zone/src/node.rs @@ -1,882 +1,3 @@ -//! Tempo Zone Node configuration. -//! -//! This is a lightweight L2 node built on reth's node builder infrastructure. -//! It reuses Tempo's EVM, primitives, and pool, but with noop consensus/network/payload. +//! Re-exports for zone node assembly. -use crate::{ - BatchAnchorConfig, DepositQueue, L1SubscriberConfig, PolicyCache, ZoneEngine, - ZoneSequencerConfig, - abi::{TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZonePortal}, - ext::TempoStateExt, - l1::L1Subscriber, - l1_state::{ - L1StateCache, L1StateProvider, L1StateProviderConfig, PolicyProvider, - spawn_policy_resolution_task, spawn_pool_prefetch_task, - }, - payload::{ZonePayloadAttributes, ZonePayloadFactory, ZonePayloadTypes}, - rpc::{TempoZoneRpc, ZoneRpcApi, rpc_connection_config, start_private_rpc}, - spawn_zone_sequencer, -}; -use alloy_primitives::Address; -use alloy_provider::Provider as _; -use alloy_signer_local::PrivateKeySigner; -use k256::SecretKey; -use reth_eth_wire_types::primitives::BasicNetworkPrimitives; -use reth_node_api::{ - AddOnsContext, FullNodeComponents, FullNodeTypes, NodeAddOns, NodeTypes, - PayloadAttributesBuilder, PayloadTypes, -}; -use reth_node_builder::{ - BuilderContext, DebugNode, Node, NodeAdapter, - components::{ - BasicPayloadServiceBuilder, ComponentsBuilder, ExecutorBuilder, NoopConsensusBuilder, - NoopNetworkBuilder, PoolBuilder, spawn_maintenance_tasks, - }, - rpc::{ - BasicEngineValidatorBuilder, EngineValidatorAddOn, EthApiBuilder, NoopEngineApiBuilder, - PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, - }, -}; -use reth_primitives_traits::{SealedHeader, transaction::error::InvalidTransactionError}; -use reth_provider::ChainSpecProvider; -use reth_rpc_builder::Identity; -use reth_rpc_eth_api::EthApiTypes; -use reth_storage_api::{BlockNumReader, EmptyBodyStorage, HeaderProvider, StateProviderFactory}; -use reth_transaction_pool::{ - Pool, TransactionValidationTaskExecutor, blobstore::InMemoryBlobStore, - error::InvalidPoolTransactionError, -}; -use std::{collections::HashSet, sync::Arc, time::Duration}; -use tempo_alloy::TempoNetwork; -use tempo_chainspec::spec::TempoChainSpec; -use tempo_evm::TempoEvmConfig; -use tempo_node::{ - DEFAULT_AA_VALID_AFTER_MAX_SECS, engine::TempoEngineValidator, rpc::TempoEthApiBuilder, -}; -use tempo_primitives::{ - self as primitives, TempoHeader, TempoPrimitives, TempoTxEnvelope, TempoTxType, -}; -use tempo_transaction_pool::{ - AA2dPool, AA2dPoolConfig, TempoTransactionPool, - amm::AmmLiquidityCache, - ordering::TempoTipOrdering, - transaction::TempoPooledTransaction, - validator::{DEFAULT_MAX_TEMPO_AUTHORIZATIONS, TempoTransactionValidator}, -}; -use tracing::{debug, info}; -use zone_evm::ZoneEvmConfig; - -/// Network primitives for Zone Nodes -type ZoneNetworkPrimitives = BasicNetworkPrimitives; - -/// Configuration for the sequencer background tasks -#[derive(Debug, Clone)] -pub struct ZoneSequencerAddOnsConfig { - /// Sequencer private key signer for signing L1 transactions. - pub sequencer_signer: PrivateKeySigner, - /// Zone ID for chain ID validation. - pub zone_id: u32, - /// How often the zone monitor polls for new L2 blocks. - pub zone_poll_interval: Duration, - /// Maximum time to accumulate zone blocks before batch submission. - pub batch_interval: Duration, - /// EIP-2935 history and safety-margin limits used by the batch submitter. - pub batch_anchor_config: BatchAnchorConfig, - /// How often the withdrawal processor polls the L1 queue. - pub withdrawal_poll_interval: Duration, -} - -/// Configuration for the Zone private RPC server extension. -#[derive(Debug, Clone, Default)] -pub struct ZonePrivateRpcConfig { - /// Port for RPC traffic. - pub private_rpc_port: u16, - /// Zone ID for chain ID validation and private RPC auth. - pub zone_id: u32, - /// Max duration for private RPC auth. - pub max_auth_token_validity: Duration, -} - -/// Tempo Zone node type configuration. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct ZoneNode { - /// Queue of L1 deposit messages to be included in the next zone block. - deposit_queue: DepositQueue, - /// Configuration for the L1 event subscriber (RPC endpoint, poll interval, etc.). - l1_config: L1SubscriberConfig, - /// Configuration for the L1 state provider (contract addresses, query parameters). - l1_state_provider_config: L1StateProviderConfig, - /// Shared L1 state cache (enabled tokens, zone metadata, etc.). - l1_state_cache: L1StateCache, - /// Shared TIP-403 policy cache, populated by the unified [`L1Subscriber`](crate::l1::L1Subscriber) - /// and read by the precompile during block building. - policy_cache: PolicyCache, - /// Address of the L1 deposit portal contract. - portal_address: Address, - /// Optional pre-configured list of enabled token addresses. When set, the - /// startup L1 RPC query for `enabledTokenCount`/`enabledTokens` is skipped. - initial_tokens: Option>, - /// Private RPC config. - private_rpc_config: ZonePrivateRpcConfig, - /// Optional sequencer config. When set, sequencer tasks are spawned. - sequencer_config: Option, -} - -impl ZoneNode { - // Creates a new ZoneNode - pub fn new( - l1_rpc_url: String, - portal_address: Address, - genesis_tempo_block_number: Option, - l1_fetch_concurrency: usize, - retry_connection_interval: Duration, - ) -> Self { - let deposit_queue = DepositQueue::default(); - - let policy_cache = PolicyCache::default(); - let l1_state_cache = L1StateCache::new(HashSet::from([portal_address])); - let l1_config = L1SubscriberConfig { - l1_rpc_url: l1_rpc_url.clone(), - portal_address, - genesis_tempo_block_number, - policy_cache: policy_cache.clone(), - l1_state_cache: l1_state_cache.clone(), - l1_fetch_concurrency, - retry_connection_interval, - }; - - let l1_state_provider_config = L1StateProviderConfig { - l1_rpc_url, - portal_address, - retry_connection_interval, - ..Default::default() - }; - - Self { - deposit_queue, - l1_config, - l1_state_provider_config, - l1_state_cache, - policy_cache, - portal_address, - initial_tokens: None, - private_rpc_config: ZonePrivateRpcConfig::default(), - sequencer_config: None, - } - } - - /// Set the private RPC configuration. - pub fn with_private_rpc(mut self, config: ZonePrivateRpcConfig) -> Self { - self.private_rpc_config = config; - self - } - - /// Set the sequencer configuration. When set, batch submission and - /// withdrawal processing tasks are spawned during node launch. - pub fn with_sequencer(mut self, config: ZoneSequencerAddOnsConfig) -> Self { - self.sequencer_config = Some(config); - self - } - - /// Set the initial list of enabled token addresses. - /// When set, the startup L1 RPC query for enabled tokens is skipped. - pub fn with_initial_tokens(mut self, tokens: Vec
) -> Self { - self.initial_tokens = Some(tokens); - self - } - - /// Returns the current deposit queue - pub fn deposit_queue(&self) -> DepositQueue { - self.deposit_queue.clone() - } - - /// Returns the current l1 state cache - pub fn l1_state_cache(&self) -> L1StateCache { - self.l1_state_cache.clone() - } - - /// Returns the current TIP-403 policy cache - pub fn policy_cache(&self) -> PolicyCache { - self.policy_cache.clone() - } - - /// Returns a [`ComponentsBuilder`] configured for a Zone node. - pub fn components( - executor_builder: ZoneExecutorBuilder, - ) -> ComponentsBuilder< - N, - ZonePoolBuilder, - BasicPayloadServiceBuilder, - NoopNetworkBuilder, - ZoneExecutorBuilder, - NoopConsensusBuilder, - > - where - N: FullNodeTypes, - { - ComponentsBuilder::default() - .node_types::() - .pool(ZonePoolBuilder) - .executor(executor_builder) - .payload(BasicPayloadServiceBuilder::new( - ZonePayloadFactory::default(), - )) - .network(NoopNetworkBuilder::::default()) - .noop_consensus() - } -} - -impl NodeTypes for ZoneNode { - type Primitives = TempoPrimitives; - type ChainSpec = TempoChainSpec; - type Storage = EmptyBodyStorage; - type Payload = ZonePayloadTypes; -} - -/// Addons for Tempo Zone nodes. -pub struct ZoneAddOns -where - N: FullNodeComponents, - N::Pool: reth_transaction_pool::TransactionPool, -{ - inner: RpcAddOns< - N, - TempoEthApiBuilder, - ZoneEngineValidatorBuilder, - NoopEngineApiBuilder, - BasicEngineValidatorBuilder, - Identity, - >, - /// Queue of L1 deposit messages to be included in the next zone block. - deposit_queue: DepositQueue, - /// Configuration for the L1 event subscriber - l1_config: L1SubscriberConfig, - /// TIP-403 policy cache - policy_cache: PolicyCache, - /// ZonePortal address on L1. - portal_address: Address, - /// Pre-configured list of initial tokens. - initial_tokens: Option>, - /// Private RPC configuration. - private_rpc_config: ZonePrivateRpcConfig, - /// Sequencer configuration. - sequencer_config: Option, -} - -impl std::fmt::Debug for ZoneAddOns -where - N: FullNodeComponents, - N::Pool: reth_transaction_pool::TransactionPool, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ZoneAddOns").finish_non_exhaustive() - } -} - -impl ZoneAddOns> -where - N: FullNodeTypes, -{ - /// Creates a new ZoneAddOns instance. - pub fn new( - deposit_queue: DepositQueue, - l1_config: L1SubscriberConfig, - policy_cache: PolicyCache, - portal_address: Address, - initial_tokens: Option>, - private_rpc_config: ZonePrivateRpcConfig, - sequencer_config: Option, - ) -> Self { - Self { - inner: RpcAddOns::new( - TempoEthApiBuilder::default(), - ZoneEngineValidatorBuilder, - NoopEngineApiBuilder::default(), - BasicEngineValidatorBuilder::default(), - Identity::default(), - Default::default(), - ), - deposit_queue, - l1_config, - policy_cache, - portal_address, - initial_tokens, - private_rpc_config, - sequencer_config, - } - } -} - -impl NodeAddOns for ZoneAddOns -where - N: FullNodeComponents, - N::Pool: reth_transaction_pool::TransactionPool< - Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, - >, - TempoEthApiBuilder: EthApiBuilder>, -{ - type Handle = , - ZoneEngineValidatorBuilder, - NoopEngineApiBuilder, - BasicEngineValidatorBuilder, - Identity, - > as NodeAddOns>::Handle; - - async fn launch_add_ons(mut self, ctx: AddOnsContext<'_, N>) -> eyre::Result { - let sp = ctx.node.provider().latest()?; - let tempo_block_number = sp.tempo_block_number()?; - self.policy_cache.set_last_l1_block(tempo_block_number); - info!(target: "reth::cli", tempo_block_number, "Read local tempoBlockNumber for L1 subscriber"); - - let l1_provider = alloy_provider::ProviderBuilder::new_with_network::() - .connect_with_config( - &self.l1_config.l1_rpc_url, - rpc_connection_config(self.l1_config.retry_connection_interval), - ) - .await? - .erased(); - - self.resolve_and_seed_tokens(&l1_provider).await?; - self.spawn_l1_subscriber(&ctx); - self.spawn_policy_tasks(&l1_provider, &ctx); - - if let Some(ref config) = self.sequencer_config { - let sequencer_addr = config.sequencer_signer.address(); - let sequencer_key = SecretKey::from(config.sequencer_signer.credential()); - self.spawn_zone_engine(l1_provider, &ctx, sequencer_addr, sequencer_key)?; - } - - let task_executor = ctx.node.task_executor().clone(); - - let chain_id = ctx - .node - .provider() - .chain_spec() - .inner - .genesis() - .config - .chain_id; - let handle = self.inner.launch_add_ons(ctx).await?; - - Self::launch_private_rpc( - self.private_rpc_config, - &handle, - self.l1_config.l1_rpc_url.clone(), - self.l1_config.retry_connection_interval, - self.l1_config.portal_address, - chain_id, - ) - .await?; - - if let Some(config) = self.sequencer_config.take() { - let sequencer_addr = config.sequencer_signer.address(); - - Self::launch_sequencer_tasks( - config, - &handle, - &task_executor, - self.l1_config.l1_rpc_url, - self.l1_config.portal_address, - self.l1_config.retry_connection_interval, - sequencer_addr, - chain_id, - ) - .await?; - } - - Ok(handle) - } -} - -impl ZoneAddOns -where - N: FullNodeComponents, - N::Pool: reth_transaction_pool::TransactionPool< - Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, - >, - TempoEthApiBuilder: EthApiBuilder>, -{ - /// Resolve enabled tokens and seed the policy cache. - async fn resolve_and_seed_tokens( - &mut self, - l1_provider: &alloy_provider::DynProvider, - ) -> eyre::Result<()> { - let portal = self.portal_address; - let tracked_tokens = if let Some(tokens) = self.initial_tokens.take() { - info!(target: "reth::cli", count = tokens.len(), ?tokens, "Using pre-configured initial tokens"); - tokens - } else { - let tokens = ZonePortal::new(portal, l1_provider) - .enabled_tokens() - .await?; - info!(target: "reth::cli", count = tokens.len(), ?tokens, "Discovered enabled tokens from L1"); - tokens - }; - - self.policy_cache - .seed_token_policies(portal, &tracked_tokens, l1_provider) - .await?; - info!(target: "reth::cli", "Seeded token policies from L1"); - Ok(()) - } - - /// Spawn the L1 subscriber. Listens for new blocks and deposit events. - fn spawn_l1_subscriber(&mut self, ctx: &AddOnsContext<'_, N>) { - L1Subscriber::spawn( - self.l1_config.clone(), - ctx.node.provider().clone(), - self.deposit_queue.clone(), - ctx.node.task_executor().clone(), - ); - info!(target: "reth::cli", "Unified L1 subscriber started"); - } - - /// Spawn TIP-403 policy resolution and pool prefetch tasks. - fn spawn_policy_tasks( - &self, - l1_provider: &alloy_provider::DynProvider, - ctx: &AddOnsContext<'_, N>, - ) { - let policy_task_handle = spawn_policy_resolution_task( - self.policy_cache.clone(), - l1_provider.clone(), - 16, - 256, - ctx.node.task_executor().clone(), - ); - spawn_pool_prefetch_task( - ctx.node.pool().clone(), - policy_task_handle, - ctx.node.task_executor().clone(), - ); - info!(target: "reth::cli", "TIP-403 policy prefetch tasks started"); - } - - /// Spawn the [`ZoneEngine`] for L1-event-driven block production. - fn spawn_zone_engine( - &self, - l1_provider: alloy_provider::DynProvider, - ctx: &AddOnsContext<'_, N>, - fee_recipient: Address, - sequencer_key: SecretKey, - ) -> eyre::Result<()> { - let policy_provider = PolicyProvider::new( - self.policy_cache.clone(), - l1_provider, - tokio::runtime::Handle::current(), - ); - let provider = ctx.node.provider(); - let last_header = provider - .sealed_header(provider.best_block_number()?)? - .ok_or_else(|| eyre::eyre!("no latest block header"))?; - let engine = ZoneEngine::new( - provider.chain_spec(), - ctx.beacon_engine_handle.clone(), - ctx.node.payload_builder_handle().clone(), - self.deposit_queue.clone(), - last_header, - fee_recipient, - sequencer_key, - self.portal_address, - policy_provider, - ); - ctx.node - .task_executor() - .spawn_critical_task("zone-engine", engine.run()); - info!(target: "reth::cli", "ZoneEngine spawned"); - Ok(()) - } - - /// Launch the private RPC server. - async fn launch_private_rpc( - config: ZonePrivateRpcConfig, - handle: &>::Handle, - l1_rpc_url: String, - retry_connection_interval: Duration, - portal_address: Address, - chain_id: u64, - ) -> eyre::Result<()> { - if config.zone_id != 0 { - let expected = zone_primitives::constants::zone_chain_id(config.zone_id); - if chain_id != expected { - eyre::bail!( - "chain ID mismatch: zone.id={} requires chain_id={}, but genesis has {}", - config.zone_id, - expected, - chain_id, - ); - } - } - - let eth_handlers = handle.eth_handlers().clone(); - let zone_rpc_url = handle - .rpc_server_handles - .rpc - .http_url() - .expect("HTTP RPC server must be enabled for private RPC"); - let private_rpc_config = zone_rpc::PrivateRpcConfig { - listen_addr: ([0, 0, 0, 0], config.private_rpc_port).into(), - l1_rpc_url, - zone_rpc_url, - retry_connection_interval, - zone_id: config.zone_id, - chain_id, - max_auth_token_validity: config.max_auth_token_validity, - zone_portal: portal_address, - }; - let api: Arc = - Arc::new(TempoZoneRpc::new(eth_handlers, private_rpc_config.clone()).await?); - let local_addr = start_private_rpc(private_rpc_config, api).await?; - info!(target: "reth::cli", %local_addr, "Private zone RPC server started"); - - Ok(()) - } - - /// Launch sequencer background tasks: batch submission, withdrawal processing, - /// and engine shutdown hook. - async fn launch_sequencer_tasks( - config: ZoneSequencerAddOnsConfig, - handle: &>::Handle, - task_executor: &reth_tasks::TaskExecutor, - l1_rpc_url: String, - portal_address: Address, - retry_connection_interval: Duration, - sequencer_addr: Address, - chain_id: u64, - ) -> eyre::Result<()> { - if config.zone_id != 0 { - let expected = zone_primitives::constants::zone_chain_id(config.zone_id); - if chain_id != expected { - eyre::bail!( - "chain ID mismatch: zone.id={} requires chain_id={}, but genesis has {}", - config.zone_id, - expected, - chain_id, - ); - } - } - - let zone_rpc_url = handle - .rpc_server_handles - .rpc - .http_url() - .expect("HTTP RPC server must be enabled for sequencer mode"); - - info!(target: "reth::cli", %sequencer_addr, "Starting sequencer background tasks"); - let sequencer_config = ZoneSequencerConfig { - portal_address, - l1_rpc_url, - retry_connection_interval, - withdrawal_poll_interval: config.withdrawal_poll_interval, - outbox_address: ZONE_OUTBOX_ADDRESS, - inbox_address: ZONE_INBOX_ADDRESS, - tempo_state_address: TEMPO_STATE_ADDRESS, - zone_rpc_url, - zone_poll_interval: config.zone_poll_interval, - batch_interval: config.batch_interval, - batch_anchor_config: config.batch_anchor_config, - }; - let seq_handle = spawn_zone_sequencer(sequencer_config, config.sequencer_signer).await; - info!(target: "reth::cli", "Sequencer tasks spawned"); - - // Critical task — node shuts down if either exits. - task_executor.spawn_critical_task("zone-monitor", async move { - tokio::select! { - res = seq_handle.withdrawal_handle => { - tracing::error!(target: "reth::cli", ?res, "Withdrawal processor task exited"); - } - res = seq_handle.monitor_handle => { - tracing::error!(target: "reth::cli", ?res, "Zone monitor task exited"); - } - } - }); - - // Flush unpersisted blocks on shutdown. - let engine_shutdown = handle.engine_shutdown.clone(); - task_executor.spawn_critical_with_graceful_shutdown_signal( - "zone-engine-shutdown", - |shutdown| async move { - let _guard = shutdown.await; - info!(target: "reth::cli", "Shutdown signal received — flushing engine state"); - if let Some(done) = engine_shutdown.shutdown() { - let _ = done.await; - } - }, - ); - - Ok(()) - } -} - -impl RethRpcAddOns for ZoneAddOns -where - N: FullNodeComponents, - N::Pool: reth_transaction_pool::TransactionPool< - Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, - >, - TempoEthApiBuilder: - EthApiBuilder>, -{ - type EthApi = as EthApiBuilder>::EthApi; - - fn hooks_mut(&mut self) -> &mut reth_node_builder::rpc::RpcHooks { - self.inner.hooks_mut() - } -} - -impl EngineValidatorAddOn for ZoneAddOns -where - N: FullNodeComponents, - N::Pool: reth_transaction_pool::TransactionPool< - Transaction = tempo_transaction_pool::transaction::TempoPooledTransaction, - >, - TempoEthApiBuilder: EthApiBuilder>, -{ - type ValidatorBuilder = BasicEngineValidatorBuilder; - - fn engine_validator_builder(&self) -> Self::ValidatorBuilder { - self.inner.engine_validator_builder() - } -} - -impl Node for ZoneNode -where - N: FullNodeTypes, -{ - type ComponentsBuilder = ComponentsBuilder< - N, - ZonePoolBuilder, - BasicPayloadServiceBuilder, - NoopNetworkBuilder, - ZoneExecutorBuilder, - NoopConsensusBuilder, - >; - type AddOns = ZoneAddOns>; - - fn components_builder(&self) -> Self::ComponentsBuilder { - let executor_builder = ZoneExecutorBuilder::new( - self.l1_state_provider_config.clone(), - self.l1_state_cache.clone(), - self.policy_cache.clone(), - ); - Self::components(executor_builder) - } - - fn add_ons(&self) -> Self::AddOns { - ZoneAddOns::new( - self.deposit_queue.clone(), - self.l1_config.clone(), - self.policy_cache.clone(), - self.portal_address, - self.initial_tokens.clone(), - self.private_rpc_config.clone(), - self.sequencer_config.clone(), - ) - } -} - -impl> DebugNode for ZoneNode { - type RpcBlock = - alloy_rpc_types_eth::Block, TempoHeader>; - - fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> primitives::Block { - rpc_block - .into_consensus_block() - .map_transactions(|tx| tx.into_inner()) - } - - fn local_payload_attributes_builder( - chain_spec: &Self::ChainSpec, - ) -> impl PayloadAttributesBuilder<::PayloadAttributes, TempoHeader> - { - ZonePayloadAttributesBuilder::new(Arc::new(chain_spec.clone())) - } -} - -/// Builds [`ZonePayloadAttributes`] with `l1_block: None` — suitable for -/// debug/test scenarios where no L1 data is available. -#[derive(Debug)] -pub(crate) struct ZonePayloadAttributesBuilder; - -impl ZonePayloadAttributesBuilder { - pub(crate) fn new(_chain_spec: Arc) -> Self { - Self - } -} - -impl PayloadAttributesBuilder for ZonePayloadAttributesBuilder { - fn build(&self, _parent: &SealedHeader) -> ZonePayloadAttributes { - unimplemented!("zone blocks require L1 data — use ZoneEngine instead") - } -} - -/// Builder that constructs the [`ZoneEvmConfig`] used during block execution. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct ZoneExecutorBuilder { - l1_state_provider_config: L1StateProviderConfig, - l1_state_cache: L1StateCache, - policy_cache: PolicyCache, -} - -impl ZoneExecutorBuilder { - /// Create a zone executor builder with the shared L1 state/policy caches. - pub fn new( - l1_state_provider_config: L1StateProviderConfig, - l1_state_cache: L1StateCache, - policy_cache: PolicyCache, - ) -> Self { - Self { - l1_state_provider_config, - l1_state_cache, - policy_cache, - } - } -} - -impl ExecutorBuilder for ZoneExecutorBuilder -where - Node: FullNodeTypes, -{ - type EVM = ZoneEvmConfig; - - async fn build_evm(self, ctx: &BuilderContext) -> eyre::Result { - let runtime_handle = tokio::runtime::Handle::current(); - let l1_provider = L1StateProvider::new( - self.l1_state_provider_config.clone(), - self.l1_state_cache, - runtime_handle.clone(), - ) - .await?; - - let mut evm_config = ZoneEvmConfig::new(ctx.chain_spec(), l1_provider); - - // Create PolicyProvider for the TIP-403 proxy precompile. - let policy_l1 = alloy_provider::ProviderBuilder::new_with_network::() - .connect_with_config( - &self.l1_state_provider_config.l1_rpc_url, - rpc_connection_config(self.l1_state_provider_config.retry_connection_interval), - ) - .await? - .erased(); - - let policy_provider = PolicyProvider::new(self.policy_cache, policy_l1, runtime_handle); - evm_config = evm_config.with_policy_provider(policy_provider); - info!(target: "reth::cli", "Zone EVM initialized with TempoStateReader + TIP-403 proxy precompiles"); - - Ok(evm_config) - } -} - -/// Engine validator builder for Zone. -#[derive(Debug, Default, Clone)] -#[non_exhaustive] -pub struct ZoneEngineValidatorBuilder; - -impl PayloadValidatorBuilder for ZoneEngineValidatorBuilder -where - Node: FullNodeComponents, -{ - type Validator = TempoEngineValidator; - - async fn build(self, _ctx: &AddOnsContext<'_, Node>) -> eyre::Result { - Ok(TempoEngineValidator::new()) - } -} - -/// Transaction pool builder for Zone - uses Tempo pool with defaults. -#[derive(Debug, Default, Clone, Copy)] -#[non_exhaustive] -pub struct ZonePoolBuilder; - -impl PoolBuilder for ZonePoolBuilder -where - Node: FullNodeTypes, -{ - type Pool = TempoTransactionPool; - - async fn build_pool( - self, - ctx: &BuilderContext, - _evm_config: ZoneEvmConfig, - ) -> eyre::Result { - 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 additional_tasks = ctx.config().txpool.additional_validation_tasks; - let task_executor = ctx.task_executor().clone(); - let mut validator = TransactionValidationTaskExecutor::eth_builder( - ctx.provider().clone(), - tempo_evm_config, - ) - .with_max_tx_input_bytes(ctx.config().txpool.max_tx_input_bytes) - .with_local_transactions_config(pool_config.local_transactions_config.clone()) - .set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap) - .with_max_tx_gas_limit(ctx.config().txpool.max_tx_gas_limit) - .set_block_gas_limit(ctx.chain_spec().inner.genesis().gas_limit) - .disable_balance_check() - .with_minimum_priority_fee(ctx.config().txpool.minimum_priority_fee) - .with_custom_tx_type(TempoTxType::AA as u8) - .no_eip4844() - .build::(blob_store.clone()); - - validator.set_additional_stateless_validation(|_origin, tx| { - use alloy_consensus::Transaction; - if tx.is_create() { - return Err(InvalidPoolTransactionError::Consensus( - InvalidTransactionError::TxTypeNotSupported, - )); - } - Ok(()) - }); - - let validator = - TransactionValidationTaskExecutor::spawn(validator, &task_executor, additional_tasks); - - let aa_2d_config = AA2dPoolConfig { - price_bump_config: pool_config.price_bumps, - pending_limit: pool_config.pending_limit, - queued_limit: pool_config.queued_limit, - max_txs_per_sender: pool_config.max_account_slots, - }; - let aa_2d_pool = AA2dPool::new(aa_2d_config); - let amm_liquidity_cache = AmmLiquidityCache::new(ctx.provider())?; - - let validator = validator.map(|v| { - TempoTransactionValidator::new( - v, - DEFAULT_AA_VALID_AFTER_MAX_SECS, - DEFAULT_MAX_TEMPO_AUTHORIZATIONS, - amm_liquidity_cache.clone(), - ) - }); - let protocol_pool = Pool::new( - validator, - TempoTipOrdering::default(), - blob_store, - pool_config.clone(), - ); - - let transaction_pool = TempoTransactionPool::new(protocol_pool, aa_2d_pool); - - spawn_maintenance_tasks(ctx, transaction_pool.clone(), &pool_config)?; - - // Spawn unified Tempo pool maintenance task - // This consolidates: expired AA txs, 2D nonce updates, AMM cache, and keychain revocations - ctx.task_executor().spawn_critical_task( - "txpool maintenance - tempo pool", - tempo_transaction_pool::maintain::maintain_tempo_pool(transaction_pool.clone()), - ); - - info!(target: "reth::cli", "Transaction pool initialized"); - debug!(target: "reth::cli", "Spawned txpool maintenance task"); - - Ok(transaction_pool) - } -} +pub use zone_node::node::*; diff --git a/crates/tempo-zone/src/rpc.rs b/crates/tempo-zone/src/rpc.rs index 57136824..a42a5920 100644 --- a/crates/tempo-zone/src/rpc.rs +++ b/crates/tempo-zone/src/rpc.rs @@ -1,1141 +1,3 @@ -//! [`ZoneRpcApi`] implementation backed by reth's EthApi (in-process reth-backed). -//! -//! Re-exports the standalone `zone-rpc` crate so everything is accessible -//! via `zone::rpc::*`. +//! Re-exports for zone private RPC. -pub use zone_rpc::*; - -use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, Weak}, - time::Duration, -}; - -use alloy_network::{ReceiptResponse, TransactionResponse}; -use alloy_primitives::{Address, B256, Bloom, Bytes, U64, U256}; -use alloy_provider::{DynProvider, Provider, ProviderBuilder}; -use alloy_rpc_types_eth::{ - Block, BlockId, BlockNumberOrTag, BlockTransactions, Filter, FilterChanges, FilterId, - TransactionRequest, - state::{EvmOverrides, StateOverride}, -}; -use alloy_sol_types::{SolCall, SolEvent, SolEventInterface}; -use eyre::WrapErr; -use futures::StreamExt; -use reth_provider::CanonStateSubscriptions; -use reth_rpc::{EthFilter, eth::filter::EthFilterError}; -use reth_rpc_builder::EthHandlers; -use reth_rpc_eth_api::{ - EthApiTypes, EthFilterApiServer, RpcConvert, - helpers::{EthApiSpec, EthBlocks, EthCall, EthFees, EthState, EthTransactions, FullEthApi}, -}; -use reth_rpc_eth_types::logs_utils; -use tempo_alloy::{ - TempoNetwork, - rpc::{TempoHeaderResponse, TempoTransactionRequest}, -}; -use tempo_contracts::precompiles::{ - ACCOUNT_KEYCHAIN_ADDRESS, - account_keychain::IAccountKeychain::{self, KeyInfo, getKeyCall}, -}; -use tempo_primitives::{TempoHeader, TempoTxEnvelope}; -use tokio::{ - sync::Mutex, - time::{MissedTickBehavior, interval}, -}; - -use crate::abi::{ - DepositType, TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_TOKEN_ADDRESS, ZoneInbox, ZonePortal, -}; -use alloy_rpc_client::ConnectionConfig; -use zone_rpc::{ - auth::AuthContext, - types::{ - AuthorizationTokenInfoResponse, BoxEyreFut, BoxFut, DepositKind, DepositState, - DepositStatusEntry, DepositStatusResponse, JsonRpcError, ZoneInfoResponse, internal, - raw_null, raw_zero, to_raw, - }, -}; - -type RpcBlock = Block, TempoHeaderResponse>; -const FILTER_OWNER_PRUNE_INTERVAL: Duration = Duration::from_secs(60); - -fn filter_not_found_error() -> JsonRpcError { - JsonRpcError::invalid_params("filter not found") -} - -fn map_eth_filter_error(err: EthFilterError) -> JsonRpcError { - match err { - EthFilterError::FilterNotFound(_) => filter_not_found_error(), - other => internal(other), - } -} - -fn stale_filter_owner_ids( - owner_ids: impl IntoIterator, - active_ids: &HashSet, -) -> Vec { - owner_ids - .into_iter() - .filter(|id| !active_ids.contains(id)) - .collect() -} - -async fn prune_filter_owners( - filter: &EthFilter, - owners: &Mutex>, -) { - let owner_ids = { - let owners = owners.lock().await; - owners.keys().cloned().collect::>() - }; - if owner_ids.is_empty() { - return; - } - - let active_ids = filter - .active_filters() - .ids() - .await - .into_iter() - .collect::>(); - let stale_ids = stale_filter_owner_ids(owner_ids, &active_ids); - if stale_ids.is_empty() { - return; - } - - let mut owners = owners.lock().await; - for id in stale_ids { - owners.remove(&id); - } -} - -/// [`ZoneRpcApi`] implementation backed by reth's [`EthHandlers`]. -/// -/// This is the privacy enforcement layer for the zone's JSON-RPC surface. -/// Only methods explicitly routed through [`ZoneRpcApi`] are reachable — -/// everything else is rejected by the dispatcher's [`classify_method`] -/// whitelist, so this struct effectively acts as an **enforced allowlist** -/// of Ethereum JSON-RPC endpoints. -/// -/// For every allowed endpoint it applies typed privacy checks *before* -/// serializing to JSON: -/// -/// - **Block redaction** — zeroing `logsBloom` and clearing transaction -/// lists for non-sequencer callers. -/// - **Sender-scoped access** — returning `null` for transactions and -/// receipts not owned by the authenticated caller. -/// - **`from`-enforcement** — `eth_call` / `eth_estimateGas` may only -/// simulate from the authenticated account (`-32004` on mismatch, -/// auto-set when omitted); state overrides are rejected for -/// non-sequencer callers (`-32602`). -/// - **Sender verification** — `eth_sendRawTransaction` checks that the -/// recovered transaction sender matches the authenticated account -/// (`-32003` on mismatch). -/// -/// [`classify_method`]: zone_rpc::types::classify_method -pub struct TempoZoneRpc { - eth: EthHandlers, - config: zone_rpc::PrivateRpcConfig, - l1_provider: DynProvider, - zone_provider: DynProvider, - tempo_state: - crate::abi::TempoState::TempoStateInstance, TempoNetwork>, - /// Maps filter IDs to the authenticated account that created them. - /// The reth filter registry remains the source of truth for filter liveness. - filter_owners: Arc>>, -} - -impl TempoZoneRpc { - /// Wrap reth's [`EthHandlers`] (api + filter + pubsub). - pub async fn new( - eth: EthHandlers, - config: zone_rpc::PrivateRpcConfig, - ) -> eyre::Result { - let l1_rpc_url = config.l1_rpc_url.clone(); - let zone_rpc_url = config.zone_rpc_url.clone(); - let l1_provider = ProviderBuilder::new_with_network::() - .connect_with_config( - &l1_rpc_url, - rpc_connection_config(config.retry_connection_interval), - ) - .await - .wrap_err("failed to connect private RPC L1 provider")? - .erased(); - let zone_provider = ProviderBuilder::new_with_network::() - .connect_with_config( - &zone_rpc_url, - rpc_connection_config(config.retry_connection_interval), - ) - .await - .wrap_err("failed to connect private RPC zone provider")? - .erased(); - let tempo_state = crate::abi::TempoState::new(TEMPO_STATE_ADDRESS, zone_provider.clone()); - let rpc = Self { - eth, - config, - l1_provider, - zone_provider, - tempo_state, - filter_owners: Arc::new(Mutex::new(HashMap::new())), - }; - rpc.spawn_filter_owner_pruner(); - Ok(rpc) - } - - /// Returns a reference to the inner [`EthFilter`] handler. - pub fn filter(&self) -> &EthFilter { - &self.eth.filter - } - - async fn filter_is_active(&self, id: &FilterId) -> bool { - self.filter().active_filters().contains(id).await - } - - fn spawn_filter_owner_pruner(&self) - where - Api: Send + Sync + 'static, - { - let filter = self.filter().clone(); - let owners: Weak>> = Arc::downgrade(&self.filter_owners); - tokio::spawn(async move { - let mut prune_interval = interval(FILTER_OWNER_PRUNE_INTERVAL); - prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); - - loop { - prune_interval.tick().await; - - let Some(owners) = owners.upgrade() else { - break; - }; - - prune_filter_owners(&filter, &owners).await; - } - }); - } - - /// Verify that the filter belongs to the authenticated caller. - /// - /// Returns `Ok(())` if the caller owns the filter or is the sequencer. - /// Returns an error indistinguishable from "filter not found" to avoid - /// leaking filter existence to non-owners. - async fn ensure_filter_owner( - &self, - id: &FilterId, - auth: &AuthContext, - ) -> Result<(), JsonRpcError> { - let owner_matches = { - let owners = self.filter_owners.lock().await; - matches!(owners.get(id), Some(owner) if *owner == auth.caller) - }; - if !owner_matches { - return Err(filter_not_found_error()); - } - if self.filter_is_active(id).await { - Ok(()) - } else { - self.filter_owners.lock().await.remove(id); - Err(filter_not_found_error()) - } - } - - async fn portal_deposits_for_block( - &self, - tempo_block_number: u64, - ) -> Result, JsonRpcError> { - if self.config.zone_portal.is_zero() { - return Err(JsonRpcError::internal("zone portal not configured")); - } - - let filter = Filter::new() - .address(self.config.zone_portal) - .from_block(tempo_block_number) - .to_block(tempo_block_number) - .event_signature(vec![ - ZonePortal::DepositMade::SIGNATURE_HASH, - ZonePortal::EncryptedDepositMade::SIGNATURE_HASH, - ]); - - let logs = self.l1_provider.get_logs(&filter).await.map_err(internal)?; - let mut deposits = Vec::with_capacity(logs.len()); - - for log in logs { - match ZonePortal::ZonePortalEvents::decode_log(&log.inner) - .map_err(internal)? - .data - { - ZonePortal::ZonePortalEvents::DepositMade(event) => { - deposits.push(PortalDepositRecord::Regular { - deposit_hash: event.newCurrentDepositQueueHash, - sender: event.sender, - recipient: event.to, - token: event.token, - amount: event.netAmount, - memo: event.memo, - }); - } - ZonePortal::ZonePortalEvents::EncryptedDepositMade(event) => { - deposits.push(PortalDepositRecord::Encrypted { - deposit_hash: event.newCurrentDepositQueueHash, - sender: event.sender, - token: event.token, - amount: event.netAmount, - }); - } - _ => {} - } - } - - Ok(deposits) - } - - async fn zone_tokens(&self) -> Result, JsonRpcError> { - if self.config.zone_portal.is_zero() { - return Ok(vec![ZONE_TOKEN_ADDRESS]); - } - - ZonePortal::new(self.config.zone_portal, &self.l1_provider) - .enabled_tokens() - .await - .map_err(internal) - } - - async fn zone_sequencer(&self) -> Result { - if self.config.zone_portal.is_zero() { - return Ok(Address::ZERO); - } - - ZonePortal::new(self.config.zone_portal, &self.l1_provider) - .sequencer() - .call() - .await - .map_err(internal) - } - - async fn terminal_event_for_deposit( - &self, - deposit_hash: B256, - ) -> Result, JsonRpcError> { - let filter = Filter::new() - .address(ZONE_INBOX_ADDRESS) - .from_block(0) - .event_signature(vec![ - ZoneInbox::DepositProcessed::SIGNATURE_HASH, - ZoneInbox::DepositFailed::SIGNATURE_HASH, - ZoneInbox::EncryptedDepositProcessed::SIGNATURE_HASH, - ZoneInbox::EncryptedDepositFailed::SIGNATURE_HASH, - ZoneInbox::DepositRejected::SIGNATURE_HASH, - ]) - .topic1(deposit_hash); - - let logs = self - .zone_provider - .get_logs(&filter) - .await - .map_err(internal)?; - let Some(log) = logs.last() else { - return Ok(None); - }; - - let Some(signature) = log.topics().first().copied() else { - return Ok(None); - }; - - if signature == ZoneInbox::DepositProcessed::SIGNATURE_HASH { - ZoneInbox::DepositProcessed::decode_log(&log.inner).map_err(internal)?; - return Ok(Some(TerminalDepositEvent::RegularProcessed)); - } - - if signature == ZoneInbox::DepositFailed::SIGNATURE_HASH { - ZoneInbox::DepositFailed::decode_log(&log.inner).map_err(internal)?; - return Ok(Some(TerminalDepositEvent::RegularFailed)); - } - - if signature == ZoneInbox::EncryptedDepositProcessed::SIGNATURE_HASH { - let event = - ZoneInbox::EncryptedDepositProcessed::decode_log(&log.inner).map_err(internal)?; - return Ok(Some(TerminalDepositEvent::EncryptedProcessed { - recipient: event.to, - memo: event.memo, - })); - } - - if signature == ZoneInbox::EncryptedDepositFailed::SIGNATURE_HASH { - ZoneInbox::EncryptedDepositFailed::decode_log(&log.inner).map_err(internal)?; - return Ok(Some(TerminalDepositEvent::EncryptedFailed)); - } - - if signature == ZoneInbox::DepositRejected::SIGNATURE_HASH { - let event = ZoneInbox::DepositRejected::decode_log(&log.inner).map_err(internal)?; - return match event.depositType { - DepositType::Regular => Ok(Some(TerminalDepositEvent::RegularRejected)), - DepositType::Encrypted => Ok(Some(TerminalDepositEvent::EncryptedRejected)), - _ => Ok(None), - }; - } - - Ok(None) - } -} - -impl zone_rpc::ZoneRpcApi for TempoZoneRpc -where - Api: FullEthApi + EthApiTypes + Send + Sync + 'static, -{ - fn get_keychain_key(&self, account: Address, key_id: Address) -> BoxEyreFut<'_, KeyInfo> { - Box::pin(async move { - let request = TempoTransactionRequest { - inner: TransactionRequest { - to: Some(ACCOUNT_KEYCHAIN_ADDRESS.into()), - input: getKeyCall { - account, - keyId: key_id, - } - .abi_encode() - .into(), - ..Default::default() - }, - ..Default::default() - }; - - let output = EthCall::call(&self.eth.api, request, None, EvmOverrides::default()) - .await - .wrap_err("AccountKeychain.getKey eth_call failed")?; - - IAccountKeychain::getKeyCall::abi_decode_returns(output.as_ref()).map_err(Into::into) - }) - } - - fn block_number(&self) -> BoxFut<'_> { - Box::pin(async move { - let info = EthApiSpec::chain_info(&self.eth.api).map_err(internal)?; - to_raw(&U256::from(info.best_number)) - }) - } - - fn chain_id(&self) -> BoxFut<'_> { - Box::pin(async move { - let chain_id = EthApiSpec::chain_id(&self.eth.api); - to_raw(&Some(chain_id)) - }) - } - - fn net_version(&self) -> BoxFut<'_> { - Box::pin(async move { - let chain_id = EthApiSpec::chain_id(&self.eth.api); - to_raw(&chain_id.to_string()) - }) - } - - fn syncing(&self) -> BoxFut<'_> { - Box::pin(async move { - let status = EthApiSpec::sync_status(&self.eth.api).map_err(internal)?; - to_raw(&status) - }) - } - - fn coinbase(&self) -> BoxFut<'_> { - Box::pin(async move { to_raw(&self.zone_sequencer().await?) }) - } - - fn gas_price(&self) -> BoxFut<'_> { - Box::pin(async move { - let price = EthFees::gas_price(&self.eth.api).await.map_err(internal)?; - to_raw(&price) - }) - } - - fn max_priority_fee_per_gas(&self) -> BoxFut<'_> { - Box::pin(async move { - let fee = EthFees::suggested_priority_fee(&self.eth.api) - .await - .map_err(internal)?; - to_raw(&fee) - }) - } - - fn fee_history( - &self, - block_count: u64, - newest_block: BlockNumberOrTag, - reward_percentiles: Option>, - ) -> BoxFut<'_> { - Box::pin(async move { - let history = - EthFees::fee_history(&self.eth.api, block_count, newest_block, reward_percentiles) - .await - .map_err(internal)?; - to_raw(&history) - }) - } - - fn get_balance( - &self, - address: Address, - block: Option, - auth: AuthContext, - ) -> BoxFut<'_> { - Box::pin(async move { - // Silent dummy: non-caller addresses get "0x0" to avoid leaking account existence. - if address != auth.caller { - return Ok(raw_zero()); - } - let balance = EthState::balance(&self.eth.api, address, block) - .await - .map_err(internal)?; - to_raw(&balance) - }) - } - - fn get_transaction_count( - &self, - address: Address, - block: Option, - auth: AuthContext, - ) -> BoxFut<'_> { - Box::pin(async move { - // Silent dummy: non-caller addresses get "0x0" to avoid leaking account existence. - if address != auth.caller { - return Ok(raw_zero()); - } - let count = EthState::transaction_count(&self.eth.api, address, block) - .await - .map_err(internal)?; - to_raw(&count) - }) - } - - fn block_by_number( - &self, - number: BlockNumberOrTag, - full: bool, - _auth: AuthContext, - ) -> BoxFut<'_> { - Box::pin(async move { - let block = EthBlocks::rpc_block(&self.eth.api, number.into(), full) - .await - .map_err(internal)?; - - let Some(mut block) = block else { - return Ok(raw_null()); - }; - - redact_block(&mut block); - - to_raw(&block) - }) - } - - fn block_by_hash(&self, hash: B256, full: bool, _auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let block = EthBlocks::rpc_block(&self.eth.api, hash.into(), full) - .await - .map_err(internal)?; - - let Some(mut block) = block else { - return Ok(raw_null()); - }; - - redact_block(&mut block); - - to_raw(&block) - }) - } - - fn transaction_by_hash(&self, hash: B256, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let tx = EthTransactions::transaction_by_hash(&self.eth.api, hash) - .await - .map_err(internal)? - .map(|src| src.into_transaction(self.eth.api.converter())) - .transpose() - .map_err(internal)?; - - let Some(tx) = tx else { return Ok(raw_null()) }; - - if tx.from() != auth.caller { - return Ok(raw_null()); - } - - to_raw(&tx) - }) - } - - fn transaction_receipt(&self, hash: B256, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let receipt = EthTransactions::transaction_receipt(&self.eth.api, hash) - .await - .map_err(internal)?; - - let Some(mut receipt) = receipt else { - return Ok(raw_null()); - }; - - if receipt.from() != auth.caller { - return Ok(raw_null()); - } - - receipt = zone_rpc::filter::filter_receipt_logs(receipt); - - to_raw(&receipt) - }) - } - - fn call( - &self, - mut request: TempoTransactionRequest, - block: Option, - state_override: Option, - auth: AuthContext, - ) -> BoxFut<'_> { - Box::pin(async move { - if state_override.is_some() { - return Err(JsonRpcError::invalid_params("state overrides not allowed")); - } - - zone_rpc::policy::enforce_from(&mut request, &auth)?; - zone_rpc::policy::enforce_no_contract_creation(&request)?; - - let result = EthCall::call( - &self.eth.api, - request, - block, - EvmOverrides::state(state_override), - ) - .await - .map_err(internal)?; - to_raw(&result) - }) - } - - fn estimate_gas( - &self, - mut request: TempoTransactionRequest, - block: Option, - state_override: Option, - auth: AuthContext, - ) -> BoxFut<'_> { - Box::pin(async move { - if state_override.is_some() { - return Err(JsonRpcError::invalid_params("state overrides not allowed")); - } - - zone_rpc::policy::enforce_from(&mut request, &auth)?; - - zone_rpc::policy::enforce_no_contract_creation(&request)?; - - let result = EthCall::estimate_gas_at( - &self.eth.api, - request, - block.unwrap_or_default(), - EvmOverrides::state(state_override), - ) - .await - .map_err(internal)?; - to_raw(&result) - }) - } - - fn send_raw_transaction(&self, data: Bytes, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - zone_rpc::policy::verify_raw_tx_sender(&data, &auth)?; - - let hash = EthTransactions::send_raw_transaction(&self.eth.api, data) - .await - .map_err(internal)?; - to_raw(&hash) - }) - } - - fn send_raw_transaction_sync(&self, data: Bytes, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - zone_rpc::policy::verify_raw_tx_sender(&data, &auth)?; - - let mut receipt = EthTransactions::send_raw_transaction_sync(&self.eth.api, data) - .await - .map_err(internal)?; - - receipt = zone_rpc::filter::filter_receipt_logs(receipt); - - to_raw(&receipt) - }) - } - - fn fill_transaction( - &self, - mut request: TempoTransactionRequest, - auth: AuthContext, - ) -> BoxFut<'_> { - Box::pin(async move { - zone_rpc::policy::enforce_from(&mut request, &auth)?; - zone_rpc::policy::enforce_no_contract_creation(&request)?; - - let result = EthTransactions::fill_transaction(&self.eth.api, request) - .await - .map_err(internal)?; - to_raw(&result) - }) - } - - fn get_logs(&self, mut filter: Filter, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let zone_tokens = self.zone_tokens().await?; - zone_rpc::filter::scope_filter_addresses(&mut filter, &zone_tokens)?; - zone_rpc::filter::scope_filter(&mut filter); - let logs = EthFilterApiServer::logs(&self.eth.filter, filter) - .await - .map_err(internal)?; - let filtered = zone_rpc::filter::filter_logs(logs, &auth.caller); - to_raw(&filtered) - }) - } - - fn new_filter(&self, mut filter: Filter, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let zone_tokens = self.zone_tokens().await?; - zone_rpc::filter::scope_filter_addresses(&mut filter, &zone_tokens)?; - zone_rpc::filter::scope_filter(&mut filter); - let id = EthFilterApiServer::new_filter(&self.eth.filter, filter) - .await - .map_err(internal)?; - self.filter_owners - .lock() - .await - .insert(id.clone(), auth.caller); - to_raw(&id) - }) - } - - fn get_filter_logs(&self, id: FilterId, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - self.ensure_filter_owner(&id, &auth).await?; - - let logs = self - .filter() - .filter_logs(id) - .await - .map_err(map_eth_filter_error)?; - - let filtered = zone_rpc::filter::filter_logs(logs, &auth.caller); - to_raw(&filtered) - }) - } - - fn get_filter_changes(&self, id: FilterId, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - self.ensure_filter_owner(&id, &auth).await?; - - let changes = self - .filter() - .filter_changes(id) - .await - .map_err(map_eth_filter_error)?; - - match changes { - FilterChanges::Logs(logs) => { - let filtered = zone_rpc::filter::filter_logs(logs, &auth.caller); - to_raw(&FilterChanges::< - alloy_rpc_types_eth::Transaction, - >::Logs(filtered)) - } - FilterChanges::Hashes(hashes) => to_raw(&FilterChanges::< - alloy_rpc_types_eth::Transaction, - >::Hashes(hashes)), - // Pending transaction filters are disabled — return empty if one somehow exists - FilterChanges::Transactions(_) => to_raw( - &FilterChanges::>::Empty, - ), - FilterChanges::Empty => to_raw( - &FilterChanges::>::Empty, - ), - } - }) - } - - fn new_block_filter(&self, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let id = EthFilterApiServer::new_block_filter(&self.eth.filter) - .await - .map_err(internal)?; - self.filter_owners - .lock() - .await - .insert(id.clone(), auth.caller); - to_raw(&id) - }) - } - - fn uninstall_filter(&self, id: FilterId, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - self.ensure_filter_owner(&id, &auth).await?; - - let result = EthFilterApiServer::uninstall_filter(&self.eth.filter, id.clone()) - .await - .map_err(internal)?; - - if result || !self.filter_is_active(&id).await { - self.filter_owners.lock().await.remove(&id); - } - - to_raw(&result) - }) - } - - fn ws_subscribe_new_heads(&self, _auth: AuthContext) -> BoxWsSubscriptionFut<'_> { - Box::pin(async move { - let api = self.eth.api.clone(); - let provider = self.eth.api.provider().clone(); - let stream = provider - .canonical_state_stream() - .flat_map(move |new_chain| { - let api = api.clone(); - let headers = new_chain - .committed() - .blocks_iter() - .filter_map(move |block| { - match api - .converter() - .convert_header(block.clone_sealed_header(), block.rlp_length()) - { - Ok(header) => Some(header), - Err(err) => { - tracing::error!( - target: "rpc", - %err, - "Failed to convert header" - ); - None - } - } - }) - .collect::>(); - futures::stream::iter(headers) - }) - .map(move |mut header| { - redact_ws_header(&mut header); - to_raw(&header) - }); - let stream: zone_rpc::WsSubscriptionStream = Box::pin(stream); - Ok(stream) - }) - } - - fn ws_subscribe_logs(&self, mut filter: Filter, auth: AuthContext) -> BoxWsSubscriptionFut<'_> { - Box::pin(async move { - let provider = self.eth.api.provider().clone(); - let caller = auth.caller; - - let zone_tokens = self.zone_tokens().await?; - zone_rpc::filter::scope_filter_addresses(&mut filter, &zone_tokens)?; - zone_rpc::filter::scope_filter(&mut filter); - - let stream = provider - .canonical_state_stream() - .flat_map(|canon_state| futures::stream::iter(canon_state.block_receipts())) - .flat_map(move |(block_receipts, removed)| { - let all_logs = logs_utils::matching_block_logs_with_tx_hashes( - &filter, - block_receipts.block, - block_receipts.timestamp, - block_receipts - .tx_receipts - .iter() - .map(|(tx, receipt)| (*tx, receipt)), - removed, - ); - futures::stream::iter(all_logs) - }); - - let stream = stream.filter_map(move |log| { - std::future::ready( - zone_rpc::filter::is_log_visible(&log, &caller).then(|| to_raw(&log)), - ) - }); - let stream: zone_rpc::WsSubscriptionStream = Box::pin(stream); - Ok(stream) - }) - } - - fn zone_get_authorization_token_info(&self, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - to_raw(&AuthorizationTokenInfoResponse { - account: auth.caller, - expires_at: U64::from(auth.expires_at), - }) - }) - } - - fn zone_get_zone_info(&self, _auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let zone_tokens = self.zone_tokens().await?; - let sequencer = self.zone_sequencer().await?; - to_raw(&ZoneInfoResponse { - zone_id: U64::from(self.config.zone_id), - zone_tokens, - sequencer, - chain_id: U64::from(self.config.chain_id), - }) - }) - } - - fn zone_get_deposit_status(&self, tempo_block_number: u64, auth: AuthContext) -> BoxFut<'_> { - Box::pin(async move { - let zone_processed_through = self - .tempo_state - .tempoBlockNumber() - .call() - .await - .map_err(internal)?; - let portal_deposits = self.portal_deposits_for_block(tempo_block_number).await?; - - let mut deposits = Vec::new(); - for deposit in portal_deposits { - match deposit { - PortalDepositRecord::Regular { - deposit_hash, - sender, - recipient, - token, - amount, - memo, - } => { - if sender != auth.caller && recipient != auth.caller { - continue; - } - - let terminal = self.terminal_event_for_deposit(deposit_hash).await?; - let status = regular_deposit_status(terminal)?; - - deposits.push(DepositStatusEntry { - deposit_hash, - kind: DepositKind::Regular, - token, - sender, - recipient: Some(recipient), - amount: U256::from(amount), - memo: Some(memo), - status, - }); - } - PortalDepositRecord::Encrypted { - deposit_hash, - sender, - token, - amount, - } => { - let terminal = self.terminal_event_for_deposit(deposit_hash).await?; - - let include = match (&terminal, sender == auth.caller) { - (_, true) => true, - ( - Some(TerminalDepositEvent::EncryptedProcessed { - recipient, .. - }), - false, - ) => *recipient == auth.caller, - _ => false, - }; - - if !include { - continue; - } - - let (recipient, memo, status) = encrypted_deposit_details(terminal)?; - - deposits.push(DepositStatusEntry { - deposit_hash, - kind: DepositKind::Encrypted, - token, - sender, - recipient, - amount: U256::from(amount), - memo, - status, - }); - } - } - } - - let processed = zone_processed_through >= tempo_block_number - && deposits - .iter() - .all(|deposit| deposit.status != DepositState::Pending); - - to_raw(&DepositStatusResponse { - tempo_block_number: U64::from(tempo_block_number), - zone_processed_through: U64::from(zone_processed_through), - processed, - deposits, - }) - }) - } -} - -#[derive(Debug, Clone)] -enum PortalDepositRecord { - Regular { - deposit_hash: B256, - sender: Address, - recipient: Address, - token: Address, - amount: u128, - memo: B256, - }, - Encrypted { - deposit_hash: B256, - sender: Address, - token: Address, - amount: u128, - }, -} - -#[derive(Debug, Clone)] -enum TerminalDepositEvent { - RegularProcessed, - RegularFailed, - RegularRejected, - EncryptedProcessed { recipient: Address, memo: B256 }, - EncryptedFailed, - EncryptedRejected, -} - -fn regular_deposit_status( - terminal: Option, -) -> Result { - match terminal { - Some(TerminalDepositEvent::RegularProcessed) => Ok(DepositState::Processed), - Some(TerminalDepositEvent::RegularFailed | TerminalDepositEvent::RegularRejected) => { - Ok(DepositState::Failed) - } - Some(TerminalDepositEvent::EncryptedProcessed { .. }) => Err(JsonRpcError::internal( - "encrypted deposit event matched regular deposit hash", - )), - Some(TerminalDepositEvent::EncryptedFailed | TerminalDepositEvent::EncryptedRejected) => { - Err(JsonRpcError::internal( - "encrypted deposit failure matched regular deposit hash", - )) - } - None => Ok(DepositState::Pending), - } -} - -fn encrypted_deposit_details( - terminal: Option, -) -> Result<(Option
, Option, DepositState), JsonRpcError> { - match terminal { - Some(TerminalDepositEvent::EncryptedProcessed { recipient, memo }) => { - Ok((Some(recipient), Some(memo), DepositState::Processed)) - } - Some(TerminalDepositEvent::EncryptedFailed | TerminalDepositEvent::EncryptedRejected) => { - Ok((None, None, DepositState::Failed)) - } - Some( - TerminalDepositEvent::RegularProcessed - | TerminalDepositEvent::RegularFailed - | TerminalDepositEvent::RegularRejected, - ) => Err(JsonRpcError::internal( - "regular deposit event matched encrypted deposit hash", - )), - None => Ok((None, None, DepositState::Pending)), - } -} - -fn redact_tempo_header(header: &mut TempoHeader) { - header.inner.logs_bloom = Bloom::ZERO; -} - -fn redact_ws_header(header: &mut TempoHeaderResponse) { - redact_tempo_header(&mut header.inner.inner); -} - -/// Strip privacy-sensitive fields from a block for non-sequencer callers. -fn redact_block(block: &mut RpcBlock) { - redact_tempo_header(&mut block.header.inner); - block.transactions = BlockTransactions::Hashes(Vec::new()); -} - -pub(crate) fn rpc_connection_config(retry_connection_interval: Duration) -> ConnectionConfig { - ConnectionConfig::new() - .with_max_retries(u32::MAX) - .with_retry_interval(retry_connection_interval) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn regular_deposit_status_maps_terminal_events() { - assert_eq!( - regular_deposit_status(Some(TerminalDepositEvent::RegularProcessed)).unwrap(), - DepositState::Processed - ); - assert_eq!(regular_deposit_status(None).unwrap(), DepositState::Pending); - } - - #[test] - fn regular_deposit_status_rejects_encrypted_terminal_events() { - let err = regular_deposit_status(Some(TerminalDepositEvent::EncryptedFailed)).unwrap_err(); - assert_eq!( - err.message, - "encrypted deposit failure matched regular deposit hash" - ); - } - - #[test] - fn encrypted_deposit_details_maps_terminal_events() { - let recipient = Address::repeat_byte(0x11); - let memo = B256::from([0x22; 32]); - - assert_eq!( - encrypted_deposit_details(Some(TerminalDepositEvent::EncryptedProcessed { - recipient, - memo, - })) - .unwrap(), - (Some(recipient), Some(memo), DepositState::Processed) - ); - assert_eq!( - encrypted_deposit_details(Some(TerminalDepositEvent::EncryptedFailed)).unwrap(), - (None, None, DepositState::Failed) - ); - assert_eq!( - encrypted_deposit_details(None).unwrap(), - (None, None, DepositState::Pending) - ); - } - - #[test] - fn encrypted_deposit_details_rejects_regular_terminal_events() { - let err = - encrypted_deposit_details(Some(TerminalDepositEvent::RegularProcessed)).unwrap_err(); - assert_eq!( - err.message, - "regular deposit event matched encrypted deposit hash" - ); - } - - #[test] - fn stale_filter_owner_ids_removes_only_inactive_entries() { - let active_ids = HashSet::from([ - FilterId::Str("0xactive".to_string()), - FilterId::Str("0xkeep".to_string()), - ]); - let owner_ids = vec![ - FilterId::Str("0xactive".to_string()), - FilterId::Str("0xstale".to_string()), - FilterId::Str("0xkeep".to_string()), - ]; - - let stale_ids = stale_filter_owner_ids(owner_ids, &active_ids); - - assert_eq!(stale_ids, vec![FilterId::Str("0xstale".to_string())]); - } - - #[test] - fn stale_filter_owner_ids_is_noop_for_empty_owner_set() { - let stale_ids = stale_filter_owner_ids(Vec::new(), &HashSet::new()); - - assert!(stale_ids.is_empty()); - } -} +pub use zone_node::rpc::*; From d69d78fcc1e54aded99741e4551a1dcac5cd604d Mon Sep 17 00:00:00 2001 From: 0xKitsune <0xKitsune@protonmail.com> Date: Wed, 24 Jun 2026 21:30:52 -0400 Subject: [PATCH 2/2] refactor: remove zone-node crate --- .github/workflows/specs.yml | 4 +- Cargo.lock | 100 ++++----------- Cargo.toml | 1 - bin/tempo-zone/Cargo.toml | 4 +- bin/tempo-zone/src/main.rs | 2 +- crates/node/Cargo.toml | 37 ++++++ crates/{tempo-zone => node}/README.md | 0 crates/node/src/lib.rs | 3 +- crates/node/src/rpc.rs | 2 +- .../tests/advance_tempo.rs | 2 +- .../tests/assets/test-genesis.json | 0 .../tests/assets/zone-test-genesis.json | 0 .../{tempo-zone => node}/tests/it/README.md | 0 .../tests/it/demo_asset_swap.rs | 0 .../tests/it/demo_cross_zone.rs | 2 +- .../tests/it/demo_shield_and_send.rs | 2 +- .../{tempo-zone => node}/tests/it/deposit.rs | 2 +- crates/{tempo-zone => node}/tests/it/e2e.rs | 9 +- .../tests/it/enable_token.rs | 2 +- .../{tempo-zone => node}/tests/it/l1_e2e.rs | 14 +-- crates/{tempo-zone => node}/tests/it/main.rs | 2 - .../tests/it/precompiles.rs | 0 .../tests/it/private_rpc.rs | 2 +- .../tests/it/private_rpc_e2e.rs | 0 .../tests/it/restart_e2e.rs | 4 +- .../tests/it/stepping_e2e.rs | 3 +- .../tests/it/tip403_policy.rs | 2 +- .../tests/it/tip403_transfers.rs | 2 +- crates/{tempo-zone => node}/tests/it/utils.rs | 118 +++++++++--------- .../tests/l1_state_reader.rs | 4 +- crates/tempo-zone/Cargo.toml | 101 --------------- crates/tempo-zone/src/abi.rs | 3 - crates/tempo-zone/src/batch.rs | 3 - crates/tempo-zone/src/cli.rs | 3 - crates/tempo-zone/src/engine.rs | 3 - crates/tempo-zone/src/ext.rs | 3 - crates/tempo-zone/src/l1.rs | 6 - crates/tempo-zone/src/l1_state.rs | 3 - crates/tempo-zone/src/lib.rs | 37 ------ crates/tempo-zone/src/node.rs | 3 - crates/tempo-zone/src/nonce_keys.rs | 3 - crates/tempo-zone/src/payload.rs | 6 - crates/tempo-zone/src/precompiles/mod.rs | 3 - crates/tempo-zone/src/rpc.rs | 3 - crates/tempo-zone/src/sequencer.rs | 3 - crates/tempo-zone/src/withdrawals.rs | 3 - crates/tempo-zone/src/zonemonitor.rs | 3 - xtask/Cargo.toml | 3 +- xtask/src/demo_blacklist.rs | 14 +-- xtask/src/demo_swap_and_deposit.rs | 10 +- xtask/src/encrypted_deposit.rs | 8 +- xtask/src/generate_zone_genesis.rs | 2 +- xtask/src/set_encryption_key.rs | 2 +- xtask/src/spam_deposits.rs | 6 +- xtask/src/zone_info.rs | 2 +- xtask/src/zone_utils.rs | 6 +- 56 files changed, 177 insertions(+), 388 deletions(-) rename crates/{tempo-zone => node}/README.md (100%) rename crates/{tempo-zone => node}/tests/advance_tempo.rs (99%) rename crates/{tempo-zone => node}/tests/assets/test-genesis.json (100%) rename crates/{tempo-zone => node}/tests/assets/zone-test-genesis.json (100%) rename crates/{tempo-zone => node}/tests/it/README.md (100%) rename crates/{tempo-zone => node}/tests/it/demo_asset_swap.rs (100%) rename crates/{tempo-zone => node}/tests/it/demo_cross_zone.rs (98%) rename crates/{tempo-zone => node}/tests/it/demo_shield_and_send.rs (98%) rename crates/{tempo-zone => node}/tests/it/deposit.rs (99%) rename crates/{tempo-zone => node}/tests/it/e2e.rs (99%) rename crates/{tempo-zone => node}/tests/it/enable_token.rs (99%) rename crates/{tempo-zone => node}/tests/it/l1_e2e.rs (99%) rename crates/{tempo-zone => node}/tests/it/main.rs (94%) rename crates/{tempo-zone => node}/tests/it/precompiles.rs (100%) rename crates/{tempo-zone => node}/tests/it/private_rpc.rs (99%) rename crates/{tempo-zone => node}/tests/it/private_rpc_e2e.rs (100%) rename crates/{tempo-zone => node}/tests/it/restart_e2e.rs (99%) rename crates/{tempo-zone => node}/tests/it/stepping_e2e.rs (99%) rename crates/{tempo-zone => node}/tests/it/tip403_policy.rs (99%) rename crates/{tempo-zone => node}/tests/it/tip403_transfers.rs (99%) rename crates/{tempo-zone => node}/tests/it/utils.rs (96%) rename crates/{tempo-zone => node}/tests/l1_state_reader.rs (96%) delete mode 100644 crates/tempo-zone/Cargo.toml delete mode 100644 crates/tempo-zone/src/abi.rs delete mode 100644 crates/tempo-zone/src/batch.rs delete mode 100644 crates/tempo-zone/src/cli.rs delete mode 100644 crates/tempo-zone/src/engine.rs delete mode 100644 crates/tempo-zone/src/ext.rs delete mode 100644 crates/tempo-zone/src/l1.rs delete mode 100644 crates/tempo-zone/src/l1_state.rs delete mode 100644 crates/tempo-zone/src/lib.rs delete mode 100644 crates/tempo-zone/src/node.rs delete mode 100644 crates/tempo-zone/src/nonce_keys.rs delete mode 100644 crates/tempo-zone/src/payload.rs delete mode 100644 crates/tempo-zone/src/precompiles/mod.rs delete mode 100644 crates/tempo-zone/src/rpc.rs delete mode 100644 crates/tempo-zone/src/sequencer.rs delete mode 100644 crates/tempo-zone/src/withdrawals.rs delete mode 100644 crates/tempo-zone/src/zonemonitor.rs diff --git a/.github/workflows/specs.yml b/.github/workflows/specs.yml index 43dfcfab..ac476f4b 100644 --- a/.github/workflows/specs.yml +++ b/.github/workflows/specs.yml @@ -7,7 +7,7 @@ on: - "specs/ref-impls/src/**" - "specs/ref-impls/test/**" - "specs/ref-impls/foundry.toml" - - "crates/tempo-zone/**" + - "crates/node/**" - ".github/workflows/specs.yml" merge_group: pull_request: @@ -16,7 +16,7 @@ on: - "specs/ref-impls/src/**" - "specs/ref-impls/test/**" - "specs/ref-impls/foundry.toml" - - "crates/tempo-zone/**" + - "crates/node/**" - ".github/workflows/specs.yml" permissions: diff --git a/Cargo.lock b/Cargo.lock index 903eced4..b7b35ae9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11457,8 +11457,9 @@ dependencies = [ "tempo-precompiles", "tempo-primitives", "tempo-revm", + "tempo-zone-contracts", "tokio", - "zone", + "zone-precompiles", "zone-primitives", ] @@ -11468,7 +11469,7 @@ version = "0.1.0" dependencies = [ "reth-cli-util", "rustls", - "zone", + "zone-node", ] [[package]] @@ -13291,78 +13292,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zone" -version = "0.1.0" -dependencies = [ - "alloy", - "alloy-consensus", - "alloy-contract", - "alloy-eips", - "alloy-evm", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rlp", - "alloy-rpc-client", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-signer", - "alloy-signer-local", - "alloy-sol-types", - "base64 0.22.1", - "const-hex", - "eyre", - "futures", - "k256", - "p256", - "rand 0.8.6", - "reqwest 0.13.4", - "reth-chainspec", - "reth-eth-wire-types", - "reth-ethereum", - "reth-node-api", - "reth-node-builder", - "reth-node-core", - "reth-payload-builder", - "reth-payload-primitives", - "reth-primitives-traits", - "reth-provider", - "reth-rpc-builder", - "reth-rpc-eth-api", - "reth-rpc-eth-types", - "reth-storage-api", - "reth-tasks", - "reth-tracing", - "reth-transaction-pool", - "revm", - "serde", - "serde_json", - "sha2 0.10.9", - "tempo-alloy", - "tempo-chainspec", - "tempo-contracts", - "tempo-evm", - "tempo-node", - "tempo-payload-types", - "tempo-precompiles", - "tempo-primitives", - "tempo-revm", - "tempo-transaction-pool", - "tempo-zone-contracts", - "thiserror 2.0.18", - "tokio", - "tokio-tungstenite 0.28.0", - "tracing", - "url", - "zone-evm", - "zone-l1", - "zone-node", - "zone-payload", - "zone-primitives", - "zone-sequencer", -] - [[package]] name = "zone-evm" version = "0.1.0" @@ -13440,25 +13369,37 @@ dependencies = [ name = "zone-node" version = "0.1.0" dependencies = [ + "alloy", "alloy-consensus", + "alloy-contract", + "alloy-eips", + "alloy-evm", "alloy-network", "alloy-primitives", "alloy-provider", + "alloy-rlp", "alloy-rpc-client", "alloy-rpc-types-engine", "alloy-rpc-types-eth", + "alloy-signer", "alloy-signer-local", "alloy-sol-types", + "base64 0.22.1", "clap", + "const-hex", "eyre", "futures", "k256", + "p256", + "rand 0.8.6", + "reqwest 0.13.4", "reth-chainspec", "reth-consensus", "reth-eth-wire-types", "reth-ethereum", "reth-node-api", "reth-node-builder", + "reth-node-core", "reth-payload-builder", "reth-payload-primitives", "reth-primitives-traits", @@ -13471,19 +13412,30 @@ dependencies = [ "reth-tasks", "reth-tracing", "reth-transaction-pool", + "revm", + "serde", + "serde_json", + "sha2 0.10.9", "tempo-alloy", "tempo-chainspec", "tempo-contracts", "tempo-evm", "tempo-node", + "tempo-payload-types", + "tempo-precompiles", "tempo-primitives", + "tempo-revm", "tempo-transaction-pool", "tempo-zone-contracts", + "thiserror 2.0.18", "tokio", + "tokio-tungstenite 0.28.0", "tracing", + "url", "zone-evm", "zone-l1", "zone-payload", + "zone-precompiles", "zone-primitives", "zone-rpc", "zone-sequencer", diff --git a/Cargo.toml b/Cargo.toml index b46822a2..63fb268e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ members = [ "crates/payload", "crates/precompiles", "crates/primitives", - "crates/tempo-zone", "crates/rpc", "crates/sequencer", "xtask", diff --git a/bin/tempo-zone/Cargo.toml b/bin/tempo-zone/Cargo.toml index 7eb57a6b..640630f8 100644 --- a/bin/tempo-zone/Cargo.toml +++ b/bin/tempo-zone/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] # tempo -zone = { path = "../../crates/tempo-zone", features = ["cli"] } +zone-node = { workspace = true, features = ["cli"] } # reth reth-cli-util.workspace = true @@ -29,4 +29,4 @@ path = "src/main.rs" [features] default = ["jemalloc"] -jemalloc = ["reth-cli-util/jemalloc", "zone/jemalloc"] +jemalloc = ["reth-cli-util/jemalloc", "zone-node/jemalloc"] diff --git a/bin/tempo-zone/src/main.rs b/bin/tempo-zone/src/main.rs index bd92cc01..164e820c 100644 --- a/bin/tempo-zone/src/main.rs +++ b/bin/tempo-zone/src/main.rs @@ -1,7 +1,7 @@ //! Tempo Zone L2 Node. #![cfg_attr(not(test), warn(unused_crate_dependencies))] -use zone::cli::ZoneCli; +use zone_node::cli::ZoneCli; #[global_allocator] static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator(); diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index c25a7c00..ea4a3e8f 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -69,6 +69,42 @@ k256.workspace = true tokio.workspace = true tracing.workspace = true +[dev-dependencies] +alloy = { workspace = true, features = [ + "full", + "genesis", + "providers", + "signers", + "signer-local", + "signer-mnemonic-all-languages", + "rpc-types", +] } +alloy-contract.workspace = true +alloy-eips.workspace = true +alloy-evm.workspace = true +alloy-rlp.workspace = true +alloy-signer.workspace = true +base64.workspace = true +const-hex.workspace = true +p256.workspace = true +rand.workspace = true +reth-ethereum = { workspace = true, features = ["node", "test-utils"] } +reth-node-core.workspace = true +reth-tracing.workspace = true +reqwest.workspace = true +revm.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sha2.workspace = true +tempo-payload-types.workspace = true +tempo-precompiles = { workspace = true, features = ["test-utils"] } +tempo-revm.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +tokio-tungstenite.workspace = true +url = "2" +zone-precompiles = { workspace = true, features = ["std"] } + [features] default = [] cli = [ @@ -87,5 +123,6 @@ test-utils = [ "reth-primitives-traits/test-utils", "reth-provider/test-utils", "reth-transaction-pool/test-utils", + "tempo-precompiles/test-utils", "tempo-transaction-pool/test-utils", ] diff --git a/crates/tempo-zone/README.md b/crates/node/README.md similarity index 100% rename from crates/tempo-zone/README.md rename to crates/node/README.md diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 4bb7d847..78207bb4 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -1,5 +1,4 @@ -//! Tempo Zone node assembly. - +#![doc = include_str!("../README.md")] #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] #![allow(unnameable_types)] diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs index feb6c9c0..d2d99869 100644 --- a/crates/node/src/rpc.rs +++ b/crates/node/src/rpc.rs @@ -1,7 +1,7 @@ //! [`ZoneRpcApi`] implementation backed by reth's EthApi (in-process reth-backed). //! //! Re-exports the standalone `zone-rpc` crate so everything is accessible -//! via `zone::rpc::*`. +//! via `zone_node::rpc::*`. pub use zone_rpc::*; diff --git a/crates/tempo-zone/tests/advance_tempo.rs b/crates/node/tests/advance_tempo.rs similarity index 99% rename from crates/tempo-zone/tests/advance_tempo.rs rename to crates/node/tests/advance_tempo.rs index 78554398..45677661 100644 --- a/crates/tempo-zone/tests/advance_tempo.rs +++ b/crates/node/tests/advance_tempo.rs @@ -1,6 +1,6 @@ //! Reproduce the `advanceTempo` system tx reverting at 232 gas. //! -//! Run with: `cargo test -p zone --test advance_tempo -- --nocapture` +//! Run with: `cargo test -p zone-node --test advance_tempo -- --nocapture` use alloy_evm::{Evm, EvmEnv, EvmFactory}; use alloy_primitives::{Address, B256, Bytes, U256, address, keccak256}; diff --git a/crates/tempo-zone/tests/assets/test-genesis.json b/crates/node/tests/assets/test-genesis.json similarity index 100% rename from crates/tempo-zone/tests/assets/test-genesis.json rename to crates/node/tests/assets/test-genesis.json diff --git a/crates/tempo-zone/tests/assets/zone-test-genesis.json b/crates/node/tests/assets/zone-test-genesis.json similarity index 100% rename from crates/tempo-zone/tests/assets/zone-test-genesis.json rename to crates/node/tests/assets/zone-test-genesis.json diff --git a/crates/tempo-zone/tests/it/README.md b/crates/node/tests/it/README.md similarity index 100% rename from crates/tempo-zone/tests/it/README.md rename to crates/node/tests/it/README.md diff --git a/crates/tempo-zone/tests/it/demo_asset_swap.rs b/crates/node/tests/it/demo_asset_swap.rs similarity index 100% rename from crates/tempo-zone/tests/it/demo_asset_swap.rs rename to crates/node/tests/it/demo_asset_swap.rs diff --git a/crates/tempo-zone/tests/it/demo_cross_zone.rs b/crates/node/tests/it/demo_cross_zone.rs similarity index 98% rename from crates/tempo-zone/tests/it/demo_cross_zone.rs rename to crates/node/tests/it/demo_cross_zone.rs index 6a600444..7304e8d7 100644 --- a/crates/tempo-zone/tests/it/demo_cross_zone.rs +++ b/crates/node/tests/it/demo_cross_zone.rs @@ -3,7 +3,7 @@ use crate::utils::{L1TestNode, WithdrawalArgs, ZoneAccount, ZoneTestNode, spawn_sequencer}; use alloy::primitives::U256; use tempo_precompiles::PATH_USD_ADDRESS; -use zone::abi::ZONE_TOKEN_ADDRESS; +use tempo_zone_contracts::ZONE_TOKEN_ADDRESS; /// Longer timeout for real L1 tests. const L1_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); diff --git a/crates/tempo-zone/tests/it/demo_shield_and_send.rs b/crates/node/tests/it/demo_shield_and_send.rs similarity index 98% rename from crates/tempo-zone/tests/it/demo_shield_and_send.rs rename to crates/node/tests/it/demo_shield_and_send.rs index aa878a1d..9d078941 100644 --- a/crates/tempo-zone/tests/it/demo_shield_and_send.rs +++ b/crates/node/tests/it/demo_shield_and_send.rs @@ -1,6 +1,6 @@ use crate::utils::{L1TestNode, ZoneAccount, ZoneTestNode}; use alloy::{primitives::U256, providers::ProviderBuilder}; -use zone::abi::ZONE_TOKEN_ADDRESS; +use tempo_zone_contracts::ZONE_TOKEN_ADDRESS; /// Longer timeout for real L1 tests. const L1_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); diff --git a/crates/tempo-zone/tests/it/deposit.rs b/crates/node/tests/it/deposit.rs similarity index 99% rename from crates/tempo-zone/tests/it/deposit.rs rename to crates/node/tests/it/deposit.rs index 15d364b4..36682c94 100644 --- a/crates/tempo-zone/tests/it/deposit.rs +++ b/crates/node/tests/it/deposit.rs @@ -7,7 +7,7 @@ use alloy::{ use tempo_chainspec::spec::TEMPO_T0_BASE_FEE; use tempo_contracts::precompiles::{IRolesAuth, ITIP20, ITIP20Factory}; use tempo_precompiles::{PATH_USD_ADDRESS, TIP20_FACTORY_ADDRESS, tip20::ISSUER_ROLE}; -use zone::abi::ZonePortal; +use tempo_zone_contracts::ZonePortal; use crate::utils::{self, DEFAULT_POLL, ZoneTestNode, poll_until}; diff --git a/crates/tempo-zone/tests/it/e2e.rs b/crates/node/tests/it/e2e.rs similarity index 99% rename from crates/tempo-zone/tests/it/e2e.rs rename to crates/node/tests/it/e2e.rs index 4d547baf..418a3949 100644 --- a/crates/tempo-zone/tests/it/e2e.rs +++ b/crates/node/tests/it/e2e.rs @@ -12,13 +12,10 @@ use alloy_signer_local::{MnemonicBuilder, coins_bip39::English}; use tempo_chainspec::spec::TEMPO_T0_BASE_FEE; use tempo_contracts::precompiles::ITIP20; use tempo_precompiles::PATH_USD_ADDRESS; -use zone::{ - ChainTempoStateExt, - abi::{ - TEMPO_STATE_ADDRESS, TempoState, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZoneInbox, - ZoneOutbox, - }, +use tempo_zone_contracts::{ + TEMPO_STATE_ADDRESS, TempoState, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS, ZoneInbox, ZoneOutbox, }; +use zone_l1::ChainTempoStateExt; use crate::utils::{ DEFAULT_POLL, DEFAULT_TIMEOUT, L1Fixture, TEST_MNEMONIC, WITHDRAWAL_TX_GAS, ZoneTestNode, diff --git a/crates/tempo-zone/tests/it/enable_token.rs b/crates/node/tests/it/enable_token.rs similarity index 99% rename from crates/tempo-zone/tests/it/enable_token.rs rename to crates/node/tests/it/enable_token.rs index c626085d..5e1cb26b 100644 --- a/crates/tempo-zone/tests/it/enable_token.rs +++ b/crates/node/tests/it/enable_token.rs @@ -5,7 +5,7 @@ //! tokens are correctly minted. use alloy::primitives::{U256, address}; -use zone::{EnabledToken, L1Deposit, L1PortalEvents}; +use zone_l1::{EnabledToken, L1Deposit, L1PortalEvents}; use crate::utils::{DEFAULT_TIMEOUT, L1Fixture, start_local_zone_with_fixture}; diff --git a/crates/tempo-zone/tests/it/l1_e2e.rs b/crates/node/tests/it/l1_e2e.rs similarity index 99% rename from crates/tempo-zone/tests/it/l1_e2e.rs rename to crates/node/tests/it/l1_e2e.rs index c50df143..22318dd5 100644 --- a/crates/tempo-zone/tests/it/l1_e2e.rs +++ b/crates/node/tests/it/l1_e2e.rs @@ -13,7 +13,7 @@ use alloy::{ }; use std::time::Duration; use tempo_precompiles::PATH_USD_ADDRESS; -use zone::abi::{TEMPO_STATE_ADDRESS, TempoState, ZONE_TOKEN_ADDRESS}; +use tempo_zone_contracts::{TEMPO_STATE_ADDRESS, TempoState, ZONE_TOKEN_ADDRESS}; /// Longer timeout for real L1 tests — the L1 dev node produces blocks every /// 500ms and the L1Subscriber needs to connect, backfill, and subscribe. @@ -1066,9 +1066,9 @@ async fn test_encrypted_deposit_blacklisted_recipient() -> eyre::Result<()> { // for the sender's L1 balance to be restored. { use tempo_contracts::precompiles::ITIP20; - use zone::precompiles::ecies; + use zone_precompiles::ecies; - let portal = zone::abi::ZonePortal::new(portal_address, depositor.l1_provider()); + let portal = tempo_zone_contracts::ZonePortal::new(portal_address, depositor.l1_provider()); ITIP20::new(PATH_USD_ADDRESS, depositor.l1_provider()) .approve(portal_address, U256::MAX) @@ -1098,7 +1098,7 @@ async fn test_encrypted_deposit_blacklisted_recipient() -> eyre::Result<()> { PATH_USD_ADDRESS, deposit_amount, key_index, - zone::abi::EncryptedDepositPayload { + tempo_zone_contracts::EncryptedDepositPayload { ephemeralPubkeyX: enc.eph_pub_x, ephemeralPubkeyYParity: enc.eph_pub_y_parity, ciphertext: enc.ciphertext.into(), @@ -1198,7 +1198,7 @@ async fn test_blacklisted_sender_transfer_rejected() -> eyre::Result<()> { cache.set_policy_type(compound_policy_id, PolicyType::COMPOUND); cache.set_compound( compound_policy_id, - zone::l1_state::tip403::CompoundData { + zone_l1::state::tip403::CompoundData { sender_policy_id, recipient_policy_id: 1, mint_recipient_policy_id: 1, @@ -1214,7 +1214,7 @@ async fn test_blacklisted_sender_transfer_rejected() -> eyre::Result<()> { let deposit_amount: u128 = 1_000_000; // 1 pathUSD { use tempo_contracts::precompiles::ITIP20; - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; let dev_provider = l1.dev_provider(); ITIP20::new(PATH_USD_ADDRESS, &dev_provider) @@ -1333,7 +1333,7 @@ async fn test_deposit_to_blacklisted_recipient_reverts_on_l1() -> eyre::Result<( .await?; // Attempt a deposit to the blacklisted recipient — should revert on L1 - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; let portal = ZonePortal::new(portal_address, &depositor_provider); let result = portal .deposit( diff --git a/crates/tempo-zone/tests/it/main.rs b/crates/node/tests/it/main.rs similarity index 94% rename from crates/tempo-zone/tests/it/main.rs rename to crates/node/tests/it/main.rs index 266a4196..86837388 100644 --- a/crates/tempo-zone/tests/it/main.rs +++ b/crates/node/tests/it/main.rs @@ -14,6 +14,4 @@ mod tip403_policy; mod tip403_transfers; mod utils; -use zone as _; - fn main() {} diff --git a/crates/tempo-zone/tests/it/precompiles.rs b/crates/node/tests/it/precompiles.rs similarity index 100% rename from crates/tempo-zone/tests/it/precompiles.rs rename to crates/node/tests/it/precompiles.rs diff --git a/crates/tempo-zone/tests/it/private_rpc.rs b/crates/node/tests/it/private_rpc.rs similarity index 99% rename from crates/tempo-zone/tests/it/private_rpc.rs rename to crates/node/tests/it/private_rpc.rs index 9560a2a9..3ae3d6c3 100644 --- a/crates/tempo-zone/tests/it/private_rpc.rs +++ b/crates/node/tests/it/private_rpc.rs @@ -8,7 +8,7 @@ use alloy_primitives::Address; use p256::ecdsa::SigningKey as P256SigningKey; use rand::thread_rng; use tempo_primitives::transaction::tt_signature::TempoSignature; -use zone::rpc::{ +use zone_node::rpc::{ auth::{AuthorizationToken, build_token_fields}, types::{MethodTier, classify_method}, }; diff --git a/crates/tempo-zone/tests/it/private_rpc_e2e.rs b/crates/node/tests/it/private_rpc_e2e.rs similarity index 100% rename from crates/tempo-zone/tests/it/private_rpc_e2e.rs rename to crates/node/tests/it/private_rpc_e2e.rs diff --git a/crates/tempo-zone/tests/it/restart_e2e.rs b/crates/node/tests/it/restart_e2e.rs similarity index 99% rename from crates/tempo-zone/tests/it/restart_e2e.rs rename to crates/node/tests/it/restart_e2e.rs index e1de29c8..dc7ac864 100644 --- a/crates/tempo-zone/tests/it/restart_e2e.rs +++ b/crates/node/tests/it/restart_e2e.rs @@ -9,7 +9,7 @@ use crate::utils::{L1TestNode, ZoneAccount, ZoneTestNode, spawn_sequencer}; use alloy::primitives::{Address, U256}; -use zone::abi::{ZONE_OUTBOX_ADDRESS, ZONE_TOKEN_ADDRESS, ZoneOutbox, ZonePortal}; +use tempo_zone_contracts::{ZONE_OUTBOX_ADDRESS, ZONE_TOKEN_ADDRESS, ZoneOutbox, ZonePortal}; /// Longer timeout for real L1 tests. const L1_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); @@ -186,7 +186,7 @@ async fn test_sequencer_restart_with_pending_withdrawal_queue() -> eyre::Result< l1.fund_user(account.address(), deposit_amount).await?; account.deposit(deposit_amount, L1_TIMEOUT, &zone).await?; - let zone::ZoneSequencerHandle { + let zone_sequencer::ZoneSequencerHandle { withdrawal_handle, monitor_handle, } = spawn_sequencer(&l1, &zone, portal_address, l1.dev_signer()).await; diff --git a/crates/tempo-zone/tests/it/stepping_e2e.rs b/crates/node/tests/it/stepping_e2e.rs similarity index 99% rename from crates/tempo-zone/tests/it/stepping_e2e.rs rename to crates/node/tests/it/stepping_e2e.rs index 057a489b..98b5b124 100644 --- a/crates/tempo-zone/tests/it/stepping_e2e.rs +++ b/crates/node/tests/it/stepping_e2e.rs @@ -11,7 +11,8 @@ use crate::utils::{ use alloy::providers::Provider; use alloy_sol_types::SolCall; use std::time::Duration; -use zone::{BatchAnchorConfig, abi::ZonePortal}; +use tempo_zone_contracts::ZonePortal; +use zone_sequencer::BatchAnchorConfig; const EIP2935_HISTORY_WINDOW: u64 = 8192; const EIP2935_SAFETY_MARGIN: u64 = 360; diff --git a/crates/tempo-zone/tests/it/tip403_policy.rs b/crates/node/tests/it/tip403_policy.rs similarity index 99% rename from crates/tempo-zone/tests/it/tip403_policy.rs rename to crates/node/tests/it/tip403_policy.rs index 13e5ed4b..a6f81a8f 100644 --- a/crates/tempo-zone/tests/it/tip403_policy.rs +++ b/crates/node/tests/it/tip403_policy.rs @@ -10,7 +10,7 @@ use alloy_signer_local::{MnemonicBuilder, coins_bip39::English}; use tempo_chainspec::spec::TEMPO_T0_BASE_FEE; use tempo_contracts::precompiles::{ITIP20, ITIP403Registry}; use tempo_precompiles::{PATH_USD_ADDRESS, TIP403_REGISTRY_ADDRESS}; -use zone::l1_state::tip403::{CompoundData, PolicyEvent}; +use zone_l1::state::tip403::{CompoundData, PolicyEvent}; use crate::utils::{DEFAULT_TIMEOUT, TEST_MNEMONIC, start_local_zone_with_fixture}; diff --git a/crates/tempo-zone/tests/it/tip403_transfers.rs b/crates/node/tests/it/tip403_transfers.rs similarity index 99% rename from crates/tempo-zone/tests/it/tip403_transfers.rs rename to crates/node/tests/it/tip403_transfers.rs index 30fb034c..f6c3a134 100644 --- a/crates/tempo-zone/tests/it/tip403_transfers.rs +++ b/crates/node/tests/it/tip403_transfers.rs @@ -16,7 +16,7 @@ use tempo_chainspec::spec::TEMPO_T0_BASE_FEE; use tempo_contracts::precompiles::ITIP20; use tempo_node::rpc::NATIVE_BALANCE_PLACEHOLDER; use tempo_precompiles::PATH_USD_ADDRESS; -use zone::abi::{ZONE_OUTBOX_ADDRESS, ZoneOutbox}; +use tempo_zone_contracts::{ZONE_OUTBOX_ADDRESS, ZoneOutbox}; use crate::utils::{ DEFAULT_TIMEOUT, TEST_MNEMONIC, WITHDRAWAL_TX_GAS, start_local_zone_with_fixture, diff --git a/crates/tempo-zone/tests/it/utils.rs b/crates/node/tests/it/utils.rs similarity index 96% rename from crates/tempo-zone/tests/it/utils.rs rename to crates/node/tests/it/utils.rs index 6b97de5d..89b14556 100644 --- a/crates/tempo-zone/tests/it/utils.rs +++ b/crates/node/tests/it/utils.rs @@ -36,10 +36,10 @@ use tempo_contracts::precompiles::{ }; use tempo_precompiles::{PATH_USD_ADDRESS, tip403_registry::ALLOW_ALL_POLICY_ID}; use tempo_primitives::{TempoHeader, transaction::tt_signature::TempoSignature}; -use zone::{ +use zone_l1::{ Deposit, DepositQueue, EnabledToken, EncryptedDeposit, L1Deposit, L1PortalEvents, L1StateCache, - ZoneNode, }; +use zone_node::ZoneNode; #[path = "../../../rpc/test-utils/auth_tokens.rs"] mod auth_tokens; @@ -147,7 +147,7 @@ const DUMMY_L1_URL: &str = "http://127.0.0.1:1"; /// token's `transferPolicyId` via RPC. pathUSD defaults to builtin policy `1` /// (allow all), and local tests rely on that behavior for outbox `transferFrom` /// flows. -fn seed_local_policy_cache(policy_cache: &zone::PolicyCache) { +fn seed_local_policy_cache(policy_cache: &zone_l1::PolicyCache) { const LOCAL_POLICY_CACHE_SEED_BLOCK: u64 = 1; policy_cache.set_last_l1_block(LOCAL_POLICY_CACHE_SEED_BLOCK); @@ -211,14 +211,15 @@ where /// - [`start_local_with_chain_id()`](Self::start_local_with_chain_id) — standalone with custom chain ID (multi-zone tests) /// - [`start_from_l1()`](Self::start_from_l1) — connected to a real [`L1TestNode`], genesis patched from L1 header /// - [`start()`](Self::start) — connected to an external L1 via WebSocket URL -type RpcApiFuture = Pin>>>>; -type RpcApiFactory = dyn Fn(zone::rpc::PrivateRpcConfig) -> RpcApiFuture + Send + Sync; +type RpcApiFuture = + Pin>>>>; +type RpcApiFactory = dyn Fn(zone_node::rpc::PrivateRpcConfig) -> RpcApiFuture + Send + Sync; pub(crate) struct ZoneTestNode { http_url: url::Url, deposit_queue: DepositQueue, l1_state_cache: L1StateCache, - policy_cache: zone::PolicyCache, + policy_cache: zone_l1::PolicyCache, rpc_api_factory: Arc, node_handle: Box, _tasks: Runtime, @@ -248,15 +249,15 @@ impl ZoneTestNode { } /// Returns a handle to the policy cache for TIP-403 authorization. - pub(crate) fn policy_cache(&self) -> &zone::PolicyCache { + pub(crate) fn policy_cache(&self) -> &zone_l1::PolicyCache { &self.policy_cache } /// Builds the real private RPC API backed by the node's EthHandlers. pub(crate) async fn rpc_api( &self, - config: zone::rpc::PrivateRpcConfig, - ) -> eyre::Result> { + config: zone_node::rpc::PrivateRpcConfig, + ) -> eyre::Result> { (self.rpc_api_factory)(config).await } @@ -312,7 +313,7 @@ impl ZoneTestNode { target: u64, timeout: Duration, ) -> eyre::Result { - use zone::abi::{TEMPO_STATE_ADDRESS, TempoState}; + use tempo_zone_contracts::{TEMPO_STATE_ADDRESS, TempoState}; let tempo_state = TempoState::new(TEMPO_STATE_ADDRESS, self.provider()); poll_until( @@ -353,7 +354,7 @@ impl ZoneTestNode { after_block: u64, timeout: Duration, ) -> eyre::Result { - use zone::abi::{TEMPO_STATE_ADDRESS, TempoState}; + use tempo_zone_contracts::{TEMPO_STATE_ADDRESS, TempoState}; let provider = self.provider(); let tempo_state = TempoState::new(TEMPO_STATE_ADDRESS, &provider); @@ -433,7 +434,7 @@ impl ZoneTestNode { ) -> eyre::Result { let l1_provider = ProviderBuilder::new_with_network::().connect_http(l1_http_url.clone()); - let portal = zone::abi::ZonePortal::new(portal_address, &l1_provider); + let portal = tempo_zone_contracts::ZonePortal::new(portal_address, &l1_provider); let genesis_block_number = portal.genesisTempoBlockNumber().call().await?; let (genesis, genesis_block_number) = build_l1_anchored_genesis_at_block(l1_http_url, portal_address, genesis_block_number) @@ -556,7 +557,7 @@ impl ZoneTestNode { .connect(&l1_provider_url) .await? .erased(); - let policy_provider = zone::PolicyProvider::new( + let policy_provider = zone_l1::PolicyProvider::new( policy_cache.clone(), l1_provider, tokio::runtime::Handle::current(), @@ -565,7 +566,7 @@ impl ZoneTestNode { let last_header = provider .sealed_header(provider.best_block_number()?)? .ok_or_else(|| eyre::eyre!("no latest block header"))?; - let engine = zone::ZoneEngine::new( + let engine = zone_node::ZoneEngine::new( provider.chain_spec(), node_handle.node.add_ons_handle.beacon_engine_handle.clone(), node_handle.node.payload_builder_handle.clone(), @@ -592,15 +593,15 @@ impl ZoneTestNode { // Build the real private RPC API while the handle is still concrete, // before type-erasing it into Box. let eth_handlers = node_handle.node.eth_handlers().clone(); - let rpc_api_factory = Arc::new(move |config: zone::rpc::PrivateRpcConfig| { + let rpc_api_factory = Arc::new(move |config: zone_node::rpc::PrivateRpcConfig| { let eth_handlers = eth_handlers.clone(); Box::pin(async move { Ok( - Arc::new(zone::rpc::TempoZoneRpc::new(eth_handlers, config).await?) - as Arc, + Arc::new(zone_node::rpc::TempoZoneRpc::new(eth_handlers, config).await?) + as Arc, ) }) - as Pin>>>> + as Pin>>>> }); Ok(Self { @@ -744,7 +745,7 @@ impl L1TestNode { /// Assert that a `BatchSubmitted` event exists on the portal. pub(crate) async fn assert_batch_submitted(&self, portal_address: Address) -> eyre::Result<()> { - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; let portal = ZonePortal::new(portal_address, self.provider()); let events = portal.BatchSubmitted_filter().from_block(0).query().await?; eyre::ensure!( @@ -761,7 +762,7 @@ impl L1TestNode { to: Address, amount: u128, ) -> eyre::Result<()> { - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; let portal = ZonePortal::new(portal_address, self.provider()); let events = portal .WithdrawalProcessed_filter() @@ -785,7 +786,7 @@ impl L1TestNode { amount: u128, callback_success: bool, ) -> eyre::Result<()> { - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; let portal = ZonePortal::new(portal_address, self.provider()); let events = portal .WithdrawalProcessed_filter() @@ -1001,7 +1002,7 @@ impl L1TestNode { sequencer: Address, ) -> eyre::Result
{ use tempo_precompiles::PATH_USD_ADDRESS; - use zone::abi::ZoneFactory; + use tempo_zone_contracts::ZoneFactory; let l1_provider = self.dev_provider(); let factory = ZoneFactory::new(factory_address, &l1_provider); @@ -1154,7 +1155,7 @@ impl L1TestNode { portal_address: Address, token: Address, ) -> eyre::Result<()> { - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; let provider = self.dev_provider(); let portal = ZonePortal::new(portal_address, &provider); let receipt = portal @@ -1200,7 +1201,7 @@ impl L1TestNode { ) -> eyre::Result<()> { use alloy_signer::SignerSync; use k256::{AffinePoint, ProjectivePoint, Scalar, elliptic_curve::sec1::ToEncodedPoint}; - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; // Derive public key coordinates let scalar: Scalar = *encryption_key.to_nonzero_scalar(); @@ -1242,8 +1243,9 @@ impl L1TestNode { portal_address: Address, recipient: Address, memo: B256, - ) -> eyre::Result<(U256, zone::abi::EncryptedDepositPayload)> { - use zone::{abi::ZonePortal, precompiles::ecies}; + ) -> eyre::Result<(U256, tempo_zone_contracts::EncryptedDepositPayload)> { + use tempo_zone_contracts::ZonePortal; + use zone_precompiles::ecies; let portal = ZonePortal::new(portal_address, self.provider()); let key_result = portal.sequencerEncryptionKey().call().await?; @@ -1266,7 +1268,7 @@ impl L1TestNode { Ok(( key_index, - zone::abi::EncryptedDepositPayload { + tempo_zone_contracts::EncryptedDepositPayload { ephemeralPubkeyX: enc.eph_pub_x, ephemeralPubkeyYParity: enc.eph_pub_y_parity, ciphertext: enc.ciphertext.into(), @@ -1769,7 +1771,7 @@ impl WithdrawalArgs { memo: B256, min_amount_out: u128, ) -> Self { - use zone::abi::SwapAndDepositRouterPlaintextCallback; + use tempo_zone_contracts::SwapAndDepositRouterPlaintextCallback; let callback_data = SwapAndDepositRouterPlaintextCallback { token_out, @@ -1798,10 +1800,10 @@ impl WithdrawalArgs { token_out: Address, target_portal: Address, key_index: U256, - encrypted: zone::abi::EncryptedDepositPayload, + encrypted: tempo_zone_contracts::EncryptedDepositPayload, min_amount_out: u128, ) -> Self { - let callback_data = zone::abi::SwapAndDepositRouterEncryptedCallback { + let callback_data = tempo_zone_contracts::SwapAndDepositRouterEncryptedCallback { token_out, target_portal, key_index, @@ -1981,7 +1983,7 @@ impl ZoneAccount { ) -> eyre::Result<(u64, U256)> { use tempo_contracts::precompiles::ITIP20; use tempo_precompiles::PATH_USD_ADDRESS; - use zone::abi::{ZONE_TOKEN_ADDRESS, ZonePortal}; + use tempo_zone_contracts::{ZONE_TOKEN_ADDRESS, ZonePortal}; if !self.l1_portal_approved { ITIP20::new(PATH_USD_ADDRESS, &self.l1_provider) @@ -2044,7 +2046,7 @@ impl ZoneAccount { zone: &ZoneTestNode, ) -> eyre::Result { use tempo_contracts::precompiles::ITIP20; - use zone::abi::ZonePortal; + use tempo_zone_contracts::ZonePortal; // Approve portal for this specific token ITIP20::new(token, &self.l1_provider) @@ -2109,10 +2111,8 @@ impl ZoneAccount { ) -> eyre::Result<(u64, U256)> { use tempo_contracts::precompiles::ITIP20; use tempo_precompiles::PATH_USD_ADDRESS; - use zone::{ - abi::{EncryptedDepositPayload, ZONE_TOKEN_ADDRESS, ZonePortal}, - precompiles::ecies, - }; + use tempo_zone_contracts::{EncryptedDepositPayload, ZONE_TOKEN_ADDRESS, ZonePortal}; + use zone_precompiles::ecies; let portal_address = self.portal_address; @@ -2201,7 +2201,7 @@ impl ZoneAccount { /// Skips approval if already approved in this session. /// Uses the default zone token (pathUSD / `ZONE_TOKEN_ADDRESS`). pub(crate) async fn withdraw_with(&mut self, args: WithdrawalArgs) -> eyre::Result<()> { - use zone::abi::ZONE_TOKEN_ADDRESS; + use tempo_zone_contracts::ZONE_TOKEN_ADDRESS; self.withdraw_token_with(ZONE_TOKEN_ADDRESS, args).await } @@ -2222,7 +2222,7 @@ impl ZoneAccount { args: WithdrawalArgs, ) -> eyre::Result<()> { use tempo_contracts::precompiles::ITIP20; - use zone::abi::{ZONE_OUTBOX_ADDRESS, ZoneOutbox}; + use tempo_zone_contracts::{ZONE_OUTBOX_ADDRESS, ZoneOutbox}; // Approve outbox for this token ITIP20::new(token, &self.l2_provider) @@ -2265,13 +2265,13 @@ pub(crate) async fn spawn_sequencer( zone: &ZoneTestNode, portal_address: Address, sequencer_signer: alloy_signer_local::PrivateKeySigner, -) -> zone::ZoneSequencerHandle { +) -> zone_sequencer::ZoneSequencerHandle { spawn_sequencer_with_anchor_config( l1, zone, portal_address, sequencer_signer, - zone::BatchAnchorConfig::default(), + zone_sequencer::BatchAnchorConfig::default(), ) .await } @@ -2282,11 +2282,11 @@ pub(crate) async fn spawn_sequencer_with_anchor_config( zone: &ZoneTestNode, portal_address: Address, sequencer_signer: alloy_signer_local::PrivateKeySigner, - batch_anchor_config: zone::BatchAnchorConfig, -) -> zone::ZoneSequencerHandle { - use zone::abi::{TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS}; + batch_anchor_config: zone_sequencer::BatchAnchorConfig, +) -> zone_sequencer::ZoneSequencerHandle { + use tempo_zone_contracts::{TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS}; - let config = zone::ZoneSequencerConfig { + let config = zone_sequencer::ZoneSequencerConfig { portal_address, l1_rpc_url: l1.http_url().to_string(), retry_connection_interval: Duration::from_millis(100), @@ -2300,7 +2300,7 @@ pub(crate) async fn spawn_sequencer_with_anchor_config( batch_anchor_config, }; - zone::spawn_zone_sequencer(config, sequencer_signer).await + zone_sequencer::spawn_zone_sequencer(config, sequencer_signer).await } /// Start a local zone node with an L1Fixture already seeded for `seed_blocks` blocks. @@ -2347,7 +2347,7 @@ fn build_auth_token( chain_id: u64, ) -> String { use alloy_signer::SignerSync; - use zone::rpc::auth::build_token_fields; + use zone_node::rpc::auth::build_token_fields; let now = now_secs(); let expires_at = now + 600; @@ -2369,7 +2369,7 @@ fn build_auth_token_with_signature( zone_id: u32, chain_id: u64, ) -> String { - use zone::rpc::auth::build_token_fields; + use zone_node::rpc::auth::build_token_fields; let now = now_secs(); let expires_at = now + 600; @@ -2381,7 +2381,7 @@ fn build_auth_token_with_signature( fn build_p256_auth_token(signing_key: &P256SigningKey, zone_id: u32, chain_id: u64) -> String { let now = now_secs(); let expires_at = now + 600; - let (_, digest) = zone::rpc::auth::build_token_fields(zone_id, chain_id, now, expires_at); + let (_, digest) = zone_node::rpc::auth::build_token_fields(zone_id, chain_id, now, expires_at); build_auth_token_with_signature( sign_p256_signature(digest, signing_key).expect("p256 signing failed"), zone_id, @@ -2397,7 +2397,7 @@ fn build_webauthn_auth_token( ) -> String { let now = now_secs(); let expires_at = now + 600; - let (_, digest) = zone::rpc::auth::build_token_fields(zone_id, chain_id, now, expires_at); + let (_, digest) = zone_node::rpc::auth::build_token_fields(zone_id, chain_id, now, expires_at); build_auth_token_with_signature( sign_webauthn_signature(signing_key, challenge_digest.unwrap_or(digest)) .expect("webauthn signing failed"), @@ -2415,7 +2415,7 @@ fn build_keychain_auth_token( ) -> (String, Address) { let now = now_secs(); let expires_at = now + 600; - let (_, digest) = zone::rpc::auth::build_token_fields(zone_id, chain_id, now, expires_at); + let (_, digest) = zone_node::rpc::auth::build_token_fields(zone_id, chain_id, now, expires_at); let (signature, key_id) = sign_keychain_signature(digest, signing_key, root_account, version) .expect("keychain signing failed"); @@ -2522,7 +2522,7 @@ pub(crate) struct PrivateRpcTestCtx { /// The sequencer signer (gets full access on the private RPC). pub sequencer_signer: alloy_signer_local::PrivateKeySigner, /// Private RPC server configuration. - pub config: zone::rpc::PrivateRpcConfig, + pub config: zone_node::rpc::PrivateRpcConfig, /// L1 fixture for injecting deposits. pub fixture: L1Fixture, } @@ -2804,10 +2804,10 @@ async fn zone_chain_id(zone: &ZoneTestNode) -> eyre::Result { async fn start_private_rpc_url( zone: &ZoneTestNode, - config: zone::rpc::PrivateRpcConfig, + config: zone_node::rpc::PrivateRpcConfig, ) -> eyre::Result { let local_addr = - zone::rpc::start_private_rpc(config.clone(), zone.rpc_api(config).await?).await?; + zone_node::rpc::start_private_rpc(config.clone(), zone.rpc_api(config).await?).await?; Ok(format!("http://{local_addr}").parse()?) } @@ -2815,7 +2815,7 @@ fn build_private_rpc_ctx( zone: ZoneTestNode, private_rpc_url: url::Url, sequencer_signer: alloy_signer_local::PrivateKeySigner, - config: zone::rpc::PrivateRpcConfig, + config: zone_node::rpc::PrivateRpcConfig, fixture: L1Fixture, ) -> PrivateRpcTestCtx { PrivateRpcTestCtx { @@ -2850,14 +2850,14 @@ pub(crate) async fn start_zone_with_private_rpc() -> eyre::Result eyre::Result EncryptedDeposit { use k256::{ProjectivePoint, Scalar, elliptic_curve::sec1::ToEncodedPoint}; use sha2::{Digest, Sha256}; - use zone::precompiles::ecies::{ + use zone_precompiles::ecies::{ build_plaintext, compressed_x_and_parity, encrypt_plaintext, hkdf_sha256, }; diff --git a/crates/tempo-zone/tests/l1_state_reader.rs b/crates/node/tests/l1_state_reader.rs similarity index 96% rename from crates/tempo-zone/tests/l1_state_reader.rs rename to crates/node/tests/l1_state_reader.rs index 8bd156eb..32467a50 100644 --- a/crates/tempo-zone/tests/l1_state_reader.rs +++ b/crates/node/tests/l1_state_reader.rs @@ -1,6 +1,6 @@ //! Integration tests for the L1StateProvider against live Tempo L1 RPC. //! -//! Run with: `cargo test -p zone --test l1_state_reader -- --ignored --nocapture` +//! Run with: `cargo test -p zone-node --test l1_state_reader -- --ignored --nocapture` //! //! Requires `L1_RPC_URL` env var or defaults to moderato RPC. @@ -9,7 +9,7 @@ use std::collections::HashSet; use alloy_primitives::{Address, B256, address}; use alloy_provider::{Provider, ProviderBuilder}; use tempo_alloy::TempoNetwork; -use zone::l1_state::{L1StateCache, L1StateProvider, L1StateProviderConfig}; +use zone_l1::state::{L1StateCache, L1StateProvider, L1StateProviderConfig}; /// ZonePortal address on Tempo L1 moderato. const ZONE_PORTAL: Address = address!("0x1bc99e6a8c4689f1884527152ba542f012316149"); diff --git a/crates/tempo-zone/Cargo.toml b/crates/tempo-zone/Cargo.toml deleted file mode 100644 index 522af731..00000000 --- a/crates/tempo-zone/Cargo.toml +++ /dev/null @@ -1,101 +0,0 @@ -[package] -name = "zone" -description = "Tempo Zone node - a lightweight L2 node built on reth" -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -publish.workspace = true - -[lints] -workspace = true - -[dependencies] -# tempo -tempo-zone-contracts = { workspace = true, features = ["std", "serde", "rpc"] } -zone-evm.workspace = true -zone-l1.workspace = true -zone-node.workspace = true -zone-payload.workspace = true -zone-sequencer.workspace = true - -# misc -eyre.workspace = true - -[dev-dependencies] -alloy = { workspace = true, features = [ - "full", - "genesis", - "providers", - "signers", - "signer-local", - "signer-mnemonic-all-languages", - "rpc-types", -] } -alloy-consensus.workspace = true -alloy-contract.workspace = true -alloy-eips.workspace = true -alloy-evm.workspace = true -alloy-network.workspace = true -alloy-primitives.workspace = true -alloy-provider.workspace = true -alloy-rlp.workspace = true -alloy-rpc-client.workspace = true -alloy-rpc-types-engine.workspace = true -alloy-rpc-types-eth.workspace = true -alloy-signer-local.workspace = true -alloy-sol-types.workspace = true -base64.workspace = true -const-hex.workspace = true -futures.workspace = true -k256.workspace = true -p256.workspace = true -rand.workspace = true -reth-chainspec.workspace = true -reth-eth-wire-types.workspace = true -reth-ethereum = { workspace = true, features = ["node", "test-utils"] } -reth-node-api.workspace = true -reth-node-builder.workspace = true -reth-node-core.workspace = true -reth-payload-builder.workspace = true -reth-payload-primitives.workspace = true -reth-primitives-traits.workspace = true -reth-provider.workspace = true -reth-rpc-builder.workspace = true -reth-rpc-eth-api.workspace = true -reth-rpc-eth-types.workspace = true -reth-storage-api.workspace = true -reth-tasks.workspace = true -reth-transaction-pool.workspace = true -reth-tracing.workspace = true -serde = { workspace = true, features = ["derive"] } -alloy-signer.workspace = true -reqwest.workspace = true -revm.workspace = true -serde_json.workspace = true -thiserror.workspace = true -tempo-alloy.workspace = true -tempo-chainspec.workspace = true -tempo-contracts.workspace = true -tempo-evm = { workspace = true, features = ["rpc", "engine"] } -tempo-node.workspace = true -tempo-payload-types.workspace = true -tempo-primitives.workspace = true -tempo-revm.workspace = true -tempo-transaction-pool.workspace = true -sha2.workspace = true -tempo-precompiles = { workspace = true, features = ["test-utils"] } -tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } -tokio-tungstenite.workspace = true -tracing.workspace = true -url = "2" -zone-primitives = { workspace = true, features = ["std", "serde"] } - -[features] -default = [] -cli = ["zone-node/cli"] -jemalloc = ["zone-node/jemalloc"] -test-utils = [ - "zone-node/test-utils", - "tempo-precompiles/test-utils", -] diff --git a/crates/tempo-zone/src/abi.rs b/crates/tempo-zone/src/abi.rs deleted file mode 100644 index e1c7e19d..00000000 --- a/crates/tempo-zone/src/abi.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! ABI bindings — re-exported from [`tempo_zone_contracts`]. - -pub use tempo_zone_contracts::*; diff --git a/crates/tempo-zone/src/batch.rs b/crates/tempo-zone/src/batch.rs deleted file mode 100644 index 213d7db6..00000000 --- a/crates/tempo-zone/src/batch.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone sequencer settlement. - -pub use zone_sequencer::settlement::*; diff --git a/crates/tempo-zone/src/cli.rs b/crates/tempo-zone/src/cli.rs deleted file mode 100644 index f306d35a..00000000 --- a/crates/tempo-zone/src/cli.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for the Tempo Zone CLI. - -pub use zone_node::cli::*; diff --git a/crates/tempo-zone/src/engine.rs b/crates/tempo-zone/src/engine.rs deleted file mode 100644 index fb312f0f..00000000 --- a/crates/tempo-zone/src/engine.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone block production. - -pub use zone_node::engine::*; diff --git a/crates/tempo-zone/src/ext.rs b/crates/tempo-zone/src/ext.rs deleted file mode 100644 index 7473d5f6..00000000 --- a/crates/tempo-zone/src/ext.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for TempoState storage extension traits. - -pub use zone_l1::{ChainTempoStateExt, TempoStateExt}; diff --git a/crates/tempo-zone/src/l1.rs b/crates/tempo-zone/src/l1.rs deleted file mode 100644 index 587cb20b..00000000 --- a/crates/tempo-zone/src/l1.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Re-exports for L1 subscription and deposit queue types. - -pub use zone_l1::{ - Deposit, DepositQueue, EnabledToken, EncryptedDeposit, L1BlockDeposits, L1Deposit, - L1PortalEvents, L1SequencerEvent, L1Subscriber, L1SubscriberConfig, PreparedL1Block, -}; diff --git a/crates/tempo-zone/src/l1_state.rs b/crates/tempo-zone/src/l1_state.rs deleted file mode 100644 index 58d4f4b9..00000000 --- a/crates/tempo-zone/src/l1_state.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for L1 state cache and policy providers. - -pub use zone_l1::state::*; diff --git a/crates/tempo-zone/src/lib.rs b/crates/tempo-zone/src/lib.rs deleted file mode 100644 index 1d8429b8..00000000 --- a/crates/tempo-zone/src/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -#![doc = include_str!("../README.md")] -#![cfg_attr(not(test), warn(unused_crate_dependencies))] -#![cfg_attr(docsrs, feature(doc_cfg))] -#![allow(unnameable_types)] -#![allow(clippy::too_many_arguments)] -use eyre as _; - -pub mod abi; -#[cfg(feature = "cli")] -pub mod cli; -pub mod ext; -pub use ext::{ChainTempoStateExt, TempoStateExt}; -pub mod batch; -pub mod engine; -pub mod l1; -pub mod l1_state; -mod node; -pub mod nonce_keys; -pub mod payload; -pub mod precompiles; -pub mod rpc; -pub mod sequencer; -pub mod withdrawals; -pub mod zonemonitor; - -pub use batch::{BatchAnchorConfig, BatchData, BatchSubmitter}; -pub use engine::ZoneEngine; -pub use l1::{ - Deposit, DepositQueue, EnabledToken, EncryptedDeposit, L1BlockDeposits, L1Deposit, - L1PortalEvents, L1SequencerEvent, L1Subscriber, L1SubscriberConfig, -}; -pub use l1_state::{L1StateCache, PolicyCache, PolicyProvider}; -pub use node::{ZoneExecutorBuilder, ZoneNode, ZonePrivateRpcConfig, ZoneSequencerAddOnsConfig}; -pub use payload::{ZonePayloadAttributes, ZonePayloadTypes}; -pub use sequencer::{ZoneSequencerConfig, ZoneSequencerHandle, spawn_zone_sequencer}; -pub use withdrawals::{SharedWithdrawalStore, WithdrawalProcessorConfig, WithdrawalStore}; -pub use zonemonitor::{ZoneMonitorConfig, spawn_zone_monitor}; diff --git a/crates/tempo-zone/src/node.rs b/crates/tempo-zone/src/node.rs deleted file mode 100644 index fadf0d9e..00000000 --- a/crates/tempo-zone/src/node.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone node assembly. - -pub use zone_node::node::*; diff --git a/crates/tempo-zone/src/nonce_keys.rs b/crates/tempo-zone/src/nonce_keys.rs deleted file mode 100644 index b2c2ba13..00000000 --- a/crates/tempo-zone/src/nonce_keys.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone sequencer nonce keys. - -pub use zone_sequencer::nonce_keys::*; diff --git a/crates/tempo-zone/src/payload.rs b/crates/tempo-zone/src/payload.rs deleted file mode 100644 index ba6da02a..00000000 --- a/crates/tempo-zone/src/payload.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Re-exports for zone payload types and builder. - -pub use zone_payload::{ - ZonePayloadAttributes, ZonePayloadBuilder, ZonePayloadFactory, ZonePayloadTypes, - build_advance_tempo_tx, -}; diff --git a/crates/tempo-zone/src/precompiles/mod.rs b/crates/tempo-zone/src/precompiles/mod.rs deleted file mode 100644 index 6153813e..00000000 --- a/crates/tempo-zone/src/precompiles/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for Zone EVM precompiles. - -pub use zone_evm::precompiles::*; diff --git a/crates/tempo-zone/src/rpc.rs b/crates/tempo-zone/src/rpc.rs deleted file mode 100644 index a42a5920..00000000 --- a/crates/tempo-zone/src/rpc.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone private RPC. - -pub use zone_node::rpc::*; diff --git a/crates/tempo-zone/src/sequencer.rs b/crates/tempo-zone/src/sequencer.rs deleted file mode 100644 index ceddc9fa..00000000 --- a/crates/tempo-zone/src/sequencer.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone sequencer task orchestration. - -pub use zone_sequencer::{ZoneSequencerConfig, ZoneSequencerHandle, spawn_zone_sequencer}; diff --git a/crates/tempo-zone/src/withdrawals.rs b/crates/tempo-zone/src/withdrawals.rs deleted file mode 100644 index 27c7473b..00000000 --- a/crates/tempo-zone/src/withdrawals.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone withdrawal processing. - -pub use zone_sequencer::withdrawals::*; diff --git a/crates/tempo-zone/src/zonemonitor.rs b/crates/tempo-zone/src/zonemonitor.rs deleted file mode 100644 index 454bee79..00000000 --- a/crates/tempo-zone/src/zonemonitor.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Re-exports for zone L2 monitoring and settlement. - -pub use zone_sequencer::monitor::*; diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 4354824a..a39bbb5b 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -13,12 +13,13 @@ workspace = true [dependencies] tempo-alloy.workspace = true tempo-chainspec.workspace = true -zone = { path = "../crates/tempo-zone" } tempo-contracts.workspace = true tempo-evm.workspace = true tempo-precompiles.workspace = true tempo-primitives.workspace = true +tempo-zone-contracts = { workspace = true, features = ["std", "serde", "rpc"] } zone-primitives.workspace = true +zone-precompiles = { workspace = true, features = ["std"] } tempo-revm.workspace = true alloy = { workspace = true, features = [ diff --git a/xtask/src/demo_blacklist.rs b/xtask/src/demo_blacklist.rs index 40b3ec6a..cee6e87b 100644 --- a/xtask/src/demo_blacklist.rs +++ b/xtask/src/demo_blacklist.rs @@ -74,10 +74,10 @@ use tempo_contracts::precompiles::{ use tempo_precompiles::{ PATH_USD_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS, tip20::ISSUER_ROLE, }; -use zone::{ - abi::{EncryptedDepositPayload, ZONE_OUTBOX_ADDRESS, ZoneInbox, ZoneOutbox, ZonePortal}, - precompiles::ecies::encrypt_deposit, +use tempo_zone_contracts::{ + EncryptedDepositPayload, ZONE_OUTBOX_ADDRESS, ZoneInbox, ZoneOutbox, ZonePortal, }; +use zone_precompiles::ecies::encrypt_deposit; const L1_EXPLORER: &str = "https://explore.moderato.tempo.xyz/tx"; @@ -736,7 +736,7 @@ async fn wait_for_token_enabled>( token: Address, ) -> eyre::Result<()> { let filter = Filter::new() - .address(zone::abi::ZONE_INBOX_ADDRESS) + .address(tempo_zone_contracts::ZONE_INBOX_ADDRESS) .event_signature(ZoneInbox::TokenEnabled::SIGNATURE_HASH) .from_block(1); @@ -764,7 +764,7 @@ async fn wait_for_deposit_processed>( to: Address, ) -> eyre::Result { let filter = Filter::new() - .address(zone::abi::ZONE_INBOX_ADDRESS) + .address(tempo_zone_contracts::ZONE_INBOX_ADDRESS) .event_signature(ZoneInbox::DepositProcessed::SIGNATURE_HASH) .from_block(from_block); @@ -797,11 +797,11 @@ async fn wait_for_encrypted_result>( to: Address, ) -> eyre::Result { let processed_filter = Filter::new() - .address(zone::abi::ZONE_INBOX_ADDRESS) + .address(tempo_zone_contracts::ZONE_INBOX_ADDRESS) .event_signature(ZoneInbox::EncryptedDepositProcessed::SIGNATURE_HASH) .from_block(from_block); let failed_filter = Filter::new() - .address(zone::abi::ZONE_INBOX_ADDRESS) + .address(tempo_zone_contracts::ZONE_INBOX_ADDRESS) .event_signature(ZoneInbox::EncryptedDepositFailed::SIGNATURE_HASH) .from_block(from_block); diff --git a/xtask/src/demo_swap_and_deposit.rs b/xtask/src/demo_swap_and_deposit.rs index 172b40c8..b8451a55 100644 --- a/xtask/src/demo_swap_and_deposit.rs +++ b/xtask/src/demo_swap_and_deposit.rs @@ -14,13 +14,11 @@ use tempo_contracts::precompiles::{ IRolesAuth, ITIP20 as TIP20Token, ITIP20Factory as TIP20Factory, }; use tempo_precompiles::{PATH_USD_ADDRESS, TIP20_FACTORY_ADDRESS, tip20::ISSUER_ROLE}; -use zone::{ - abi::{ - EncryptedDepositPayload, SwapAndDepositRouterEncryptedCallback, ZONE_OUTBOX_ADDRESS, - ZoneOutbox, ZonePortal, - }, - precompiles::ecies::encrypt_deposit, +use tempo_zone_contracts::{ + EncryptedDepositPayload, SwapAndDepositRouterEncryptedCallback, ZONE_OUTBOX_ADDRESS, + ZoneOutbox, ZonePortal, }; +use zone_precompiles::ecies::encrypt_deposit; use crate::zone_utils::{ ROUTER_CALLBACK_GAS_LIMIT, STABLECOIN_DEX_ADDRESS, ZoneMetadata, check, fund_l1_wallet, diff --git a/xtask/src/encrypted_deposit.rs b/xtask/src/encrypted_deposit.rs index 3d321050..0c0d242f 100644 --- a/xtask/src/encrypted_deposit.rs +++ b/xtask/src/encrypted_deposit.rs @@ -13,10 +13,8 @@ use alloy::{ }; use eyre::{WrapErr as _, eyre}; use tempo_alloy::TempoNetwork; -use zone::{ - abi::{EncryptedDepositPayload, ZoneInbox, ZonePortal}, - precompiles::ecies::encrypt_deposit, -}; +use tempo_zone_contracts::{EncryptedDepositPayload, ZoneInbox, ZonePortal}; +use zone_precompiles::ecies::encrypt_deposit; #[derive(Debug, clap::Parser)] pub(crate) struct EncryptedDeposit { @@ -148,7 +146,7 @@ impl EncryptedDeposit { sender: Address, to: Address, ) -> eyre::Result<()> { - use zone::abi::ZONE_INBOX_ADDRESS; + use tempo_zone_contracts::ZONE_INBOX_ADDRESS; println!("Waiting for encrypted deposit to be processed on L2..."); let l2 = ProviderBuilder::new().connect(zone_rpc).await?; diff --git a/xtask/src/generate_zone_genesis.rs b/xtask/src/generate_zone_genesis.rs index 7208970b..e6e1bd26 100644 --- a/xtask/src/generate_zone_genesis.rs +++ b/xtask/src/generate_zone_genesis.rs @@ -39,7 +39,7 @@ use tempo_precompiles::{ tip403_registry::TIP403Registry, }; use tempo_revm::{TempoBlockEnv, TempoTxEnv}; -use zone::precompiles::ZoneTokenFactory; +use zone_precompiles::ZoneTokenFactory; const TEMPO_STATE_ADDRESS: Address = address!("0x1c00000000000000000000000000000000000000"); const ZONE_INBOX_ADDRESS: Address = address!("0x1c00000000000000000000000000000000000001"); diff --git a/xtask/src/set_encryption_key.rs b/xtask/src/set_encryption_key.rs index 6346d0b9..2d9284b6 100644 --- a/xtask/src/set_encryption_key.rs +++ b/xtask/src/set_encryption_key.rs @@ -14,7 +14,7 @@ use alloy::{ use eyre::{WrapErr as _, eyre}; use k256::{AffinePoint, ProjectivePoint, Scalar, elliptic_curve::sec1::ToEncodedPoint}; use tempo_alloy::TempoNetwork; -use zone::abi::ZonePortal; +use tempo_zone_contracts::ZonePortal; #[derive(Debug, clap::Parser)] pub(crate) struct SetEncryptionKey { diff --git a/xtask/src/spam_deposits.rs b/xtask/src/spam_deposits.rs index 832c9c82..31747430 100644 --- a/xtask/src/spam_deposits.rs +++ b/xtask/src/spam_deposits.rs @@ -20,10 +20,8 @@ use tempo_primitives::{ TempoSignature, transaction::{Call, PrimitiveSignature}, }; -use zone::{ - abi::{EncryptedDepositPayload, ZonePortal}, - precompiles::ecies::encrypt_deposit, -}; +use tempo_zone_contracts::{EncryptedDepositPayload, ZonePortal}; +use zone_precompiles::ecies::encrypt_deposit; #[derive(Debug, clap::Parser)] pub(crate) struct SpamDeposits { diff --git a/xtask/src/zone_info.rs b/xtask/src/zone_info.rs index 44d31e88..8c41237d 100644 --- a/xtask/src/zone_info.rs +++ b/xtask/src/zone_info.rs @@ -1,7 +1,7 @@ use alloy::{primitives::Address, providers::ProviderBuilder}; use eyre::eyre; use tempo_alloy::TempoNetwork; -use zone::abi::{ZoneFactory, ZonePortal}; +use tempo_zone_contracts::{ZoneFactory, ZonePortal}; use crate::zone_utils::MODERATO_ZONE_FACTORY; diff --git a/xtask/src/zone_utils.rs b/xtask/src/zone_utils.rs index 059ce878..b82da764 100644 --- a/xtask/src/zone_utils.rs +++ b/xtask/src/zone_utils.rs @@ -13,7 +13,7 @@ use std::{ }; use tempo_alloy::TempoNetwork; use tempo_contracts::precompiles::ITIP20 as TIP20Token; -use zone::abi::{ZoneInbox, ZonePortal}; +use tempo_zone_contracts::{ZoneInbox, ZonePortal}; pub(crate) const L1_EXPLORER: &str = "https://explore.moderato.tempo.xyz/tx"; /// Shared Moderato ZoneFactory. @@ -167,7 +167,7 @@ pub(crate) async fn wait_for_token_enabled>( token: Address, ) -> eyre::Result { let filter = Filter::new() - .address(zone::abi::ZONE_INBOX_ADDRESS) + .address(tempo_zone_contracts::ZONE_INBOX_ADDRESS) .event_signature(ZoneInbox::TokenEnabled::SIGNATURE_HASH) .from_block(from_block); @@ -194,7 +194,7 @@ pub(crate) async fn wait_for_deposit_processed>( token: Address, ) -> eyre::Result { let filter = Filter::new() - .address(zone::abi::ZONE_INBOX_ADDRESS) + .address(tempo_zone_contracts::ZONE_INBOX_ADDRESS) .event_signature(ZoneInbox::DepositProcessed::SIGNATURE_HASH) .from_block(from_block);