diff --git a/Cargo.lock b/Cargo.lock index 5bd2c5726..b2e7e926e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13469,22 +13469,16 @@ dependencies = [ "reth-provider", "reth-storage-api", "reth-tasks", - "reth-transaction-pool", "revm", "serde", "serde_json", "tempo-alloy", - "tempo-contracts", - "tempo-precompiles", "tempo-primitives", - "tempo-revm", - "tempo-transaction-pool", "tempo-zone-contracts", "tokio", "tracing", "url", "zone-precompiles", - "zone-primitives", ] [[package]] diff --git a/crates/evm/src/database.rs b/crates/evm/src/database.rs index 3033c930e..cb3dcc58f 100644 --- a/crates/evm/src/database.rs +++ b/crates/evm/src/database.rs @@ -270,6 +270,7 @@ mod tests { let slot = U256::from(7); let expected = U256::from(99); let l1 = TestL1::default(); + l1.insert(TIP403_REGISTRY_ADDRESS, slot, anchor - 1, U256::from(98)); l1.insert(TIP403_REGISTRY_ADDRESS, slot, anchor, expected); let mut db = AnchoredZoneDb::new(test_db(anchor), l1); diff --git a/crates/l1/Cargo.toml b/crates/l1/Cargo.toml index 069557690..63d5e60d9 100644 --- a/crates/l1/Cargo.toml +++ b/crates/l1/Cargo.toml @@ -14,15 +14,10 @@ workspace = true # zones tempo-zone-contracts = { workspace = true, features = ["std", "serde", "rpc"] } zone-precompiles = { workspace = true, features = ["std"] } -zone-primitives = { workspace = true, features = ["std", "serde"] } # tempo tempo-alloy.workspace = true -tempo-contracts.workspace = true -tempo-precompiles.workspace = true tempo-primitives.workspace = true -tempo-revm.workspace = true -tempo-transaction-pool.workspace = true # reth reth-metrics.workspace = true @@ -30,7 +25,6 @@ reth-primitives-traits.workspace = true reth-provider.workspace = true reth-storage-api.workspace = true reth-tasks.workspace = true -reth-transaction-pool.workspace = true # alloy alloy-consensus.workspace = true diff --git a/crates/l1/src/block.rs b/crates/l1/src/block.rs index 724fea7f1..203f08e47 100644 --- a/crates/l1/src/block.rs +++ b/crates/l1/src/block.rs @@ -7,8 +7,6 @@ pub struct L1BlockDeposits { pub header: SealedHeader, /// Portal events extracted from this block. pub events: L1PortalEvents, - /// TIP-403 policy events extracted from this block's receipts. - pub policy_events: Vec, /// Deposit queue hash chain value before this block's deposits. pub queue_hash_before: B256, /// Deposit queue hash chain value after this block's deposits. @@ -18,15 +16,13 @@ pub struct L1BlockDeposits { impl L1BlockDeposits { /// Prepare all deposits for the payload builder. /// - /// Decrypts encrypted deposits, checks TIP-403 policy authorization, - /// and ABI-encodes everything into the types the `advanceTempo` call expects. - /// The resulting [`PreparedL1Block`] is ready to be passed through payload - /// attributes to the builder. + /// Decrypts encrypted deposits and ABI-encodes into the types the `advanceTempo` call expects. + /// Mint-recipient policy is enforced by upstream TIP-20 after the L1 state is anchored. + /// The resulting [`PreparedL1Block`] is ready to be passed via payload attributes to the builder. pub async fn prepare( self, sequencer_key: &k256::SecretKey, portal_address: Address, - policy_provider: &crate::state::PolicyProvider, ) -> eyre::Result { use crate::precompiles::ecies; @@ -54,7 +50,7 @@ impl L1BlockDeposits { }); } L1Deposit::Encrypted(d) => { - let mut queued = abi::QueuedDeposit { + let queued = abi::QueuedDeposit { depositType: abi::DepositType::Encrypted, depositData: Bytes::from( abi::EncryptedDeposit { @@ -96,42 +92,9 @@ impl L1BlockDeposits { recipient = %dec.to, token = %d.token, amount = %d.amount, - "Decrypted encrypted deposit, checking policy" + "Decrypted encrypted deposit" ); - // Check TIP-403 policy via the provider (cache-first, RPC fallback). - // Errors are propagated so the engine retries rather than allowing - // unauthorized deposits through. - let authorized = policy_provider - .is_authorized_async( - d.token, - dec.to, - l1_block_number, - crate::state::AuthRole::MintRecipient, - ) - .await?; - - if authorized { - debug!( - target: "zone::engine", - recipient = %dec.to, - token = %d.token, - "Policy authorized encrypted deposit recipient" - ); - } else { - warn!( - target: "zone::engine", - sender = %d.sender, - recipient = %dec.to, - token = %d.token, - amount = %d.amount, - "Encrypted deposit recipient unauthorized; queuing deposit bounce-back" - ); - queued.rejected = true; - queued_deposits.push(queued); - continue; - } - let decryption = abi::DecryptionData { sharedSecret: dec.proof.shared_secret, sharedSecretYParity: dec.proof.shared_secret_y_parity, @@ -221,9 +184,8 @@ impl L1BlockDeposits { /// An L1 block with deposits fully prepared for the payload builder. /// -/// All ECIES decryption, TIP-403 policy checks, and ABI encoding have been -/// performed. The builder only needs to RLP-encode the header and assemble -/// the `advanceTempo` calldata. +/// All ECIES decryption and ABI encoding have been performed. +/// The builder only needs to RLP-encode the header and assemble the `advanceTempo` calldata. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PreparedL1Block { /// The sealed L1 block header. @@ -231,7 +193,7 @@ pub struct PreparedL1Block { /// ABI-encoded queued deposits (regular + encrypted). #[serde(skip)] pub queued_deposits: Vec, - /// Decryption data for non-rejected encrypted deposits, in order. + /// Decryption data for encrypted deposits accepted for on-chain verification, in order. #[serde(skip)] pub decryptions: Vec, /// Tokens newly enabled for bridging in this block. diff --git a/crates/l1/src/lib.rs b/crates/l1/src/lib.rs index c9f07b8b6..f9cd9ec99 100644 --- a/crates/l1/src/lib.rs +++ b/crates/l1/src/lib.rs @@ -76,7 +76,7 @@ use crate::{ SequencerTransferred, TokenEnabled, WithdrawalBounceBack, ZonePortalEvents, }, }, - state::{cache::L1StateCacheInner, tip403::PolicyEvent}, + state::cache::L1StateCacheInner, }; mod block; @@ -93,7 +93,7 @@ pub use deposit::{Deposit, EncryptedDeposit, L1Deposit}; pub use event::{EnabledToken, L1PortalEvents, L1SequencerEvent}; pub use ext::{ChainTempoStateExt, TempoStateExt}; pub use queue::DepositQueue; -pub use state::{L1StateCache, PolicyCache, PolicyProvider}; +pub use state::L1StateCache; pub use subscriber::{L1Subscriber, L1SubscriberConfig}; pub(crate) use event::EnqueueOutcome; diff --git a/crates/l1/src/queue.rs b/crates/l1/src/queue.rs index 8c80bd0ae..3977a181b 100644 --- a/crates/l1/src/queue.rs +++ b/crates/l1/src/queue.rs @@ -56,7 +56,6 @@ impl PendingDeposits { &mut self, header: SealedHeader, events: L1PortalEvents, - policy_events: Vec, ) -> EnqueueOutcome { let block_number = header.number(); let block_hash = header.hash(); @@ -117,7 +116,7 @@ impl PendingDeposits { // of parent hash — the parent was reorged but the zone already // committed to it. The builder will detect the hash mismatch. } - self.append(header, events, policy_events); + self.append(header, events); return EnqueueOutcome::Accepted; } @@ -164,31 +163,21 @@ impl PendingDeposits { // If new_expected == block_number, fall through to accept (it'll be the anchor) } - self.append(header, events, policy_events); + self.append(header, events); EnqueueOutcome::Accepted } /// Enqueue a block during backfill. Accepts or skips duplicates. /// /// Panics on `NeedBackfill` — backfill blocks must be fetched sequentially. - pub(crate) fn enqueue( - &mut self, - header: TempoHeader, - events: L1PortalEvents, - policy_events: Vec, - ) { - match self.try_enqueue(SealedHeader::seal_slow(header), events, policy_events) { + pub(crate) fn enqueue(&mut self, header: TempoHeader, events: L1PortalEvents) { + match self.try_enqueue(SealedHeader::seal_slow(header), events) { EnqueueOutcome::Accepted | EnqueueOutcome::Duplicate => {} other => panic!("enqueue expected Accepted or Duplicate, got {other:?}"), } } - fn append( - &mut self, - header: SealedHeader, - events: L1PortalEvents, - policy_events: Vec, - ) { + fn append(&mut self, header: SealedHeader, events: L1PortalEvents) { let queue_hash_before = self.enqueued_head_hash; for deposit in &events.deposits { self.enqueued_head_hash = deposit.hash_chain(self.enqueued_head_hash); @@ -198,7 +187,6 @@ impl PendingDeposits { self.pending.push(L1BlockDeposits { header, events, - policy_events, queue_hash_before, queue_hash_after, }); @@ -362,10 +350,9 @@ impl DepositQueue { &self, header: SealedHeader, events: L1PortalEvents, - policy_events: Vec, ) -> EnqueueOutcome { let mut queue = self.inner.lock(); - let outcome = queue.try_enqueue(header, events, policy_events); + let outcome = queue.try_enqueue(header, events); if matches!(outcome, EnqueueOutcome::Accepted) { drop(queue); self.notify.notify_one(); @@ -374,26 +361,16 @@ impl DepositQueue { } /// Enqueue an L1 block with its deposits and notify waiters. - pub fn enqueue( - &self, - header: TempoHeader, - events: L1PortalEvents, - policy_events: Vec, - ) { - self.inner.lock().enqueue(header, events, policy_events); + pub fn enqueue(&self, header: TempoHeader, events: L1PortalEvents) { + self.inner.lock().enqueue(header, events); self.notify.notify_one(); } /// Like [`enqueue`](Self::enqueue) but accepts an already-sealed header, /// avoiding a redundant hash computation. - pub fn enqueue_sealed( - &self, - header: SealedHeader, - events: L1PortalEvents, - policy_events: Vec, - ) { + pub fn enqueue_sealed(&self, header: SealedHeader, events: L1PortalEvents) { let mut queue = self.inner.lock(); - match queue.try_enqueue(header, events, policy_events) { + match queue.try_enqueue(header, events) { EnqueueOutcome::Accepted | EnqueueOutcome::Duplicate => {} other => panic!("enqueue_sealed expected Accepted or Duplicate, got {other:?}"), } diff --git a/crates/l1/src/state/mod.rs b/crates/l1/src/state/mod.rs index a249f737b..c5328898b 100644 --- a/crates/l1/src/state/mod.rs +++ b/crates/l1/src/state/mod.rs @@ -5,16 +5,12 @@ //! - [`L1StateCache`] — a shared in-memory cache of L1 contract storage slots. //! - [`L1StateCacheInner`] — the block-versioned cache storage guarded by [`L1StateCache`]. //! - [`L1StateProvider`] — a cache-first, RPC-fallback reader for `eth_getStorageAt`. -//! - [`tip403`] — TIP-403 policy cache and provider. +//! +//! TIP-20 and TIP-403 policy semantics are evaluated by Tempo's upstream precompiles. This +//! module only supplies their exact-block raw L1 storage view. pub mod cache; pub mod provider; -pub mod tip403; -pub mod versioned; pub use cache::{L1StateCache, L1StateCacheInner}; pub use provider::{L1StateProvider, L1StateProviderConfig}; -pub use tip403::{ - AuthRole, PolicyCache, PolicyCacheInner, PolicyEvent, PolicyProvider, PolicyTaskHandle, - PolicyTaskMessage, Tip403Metrics, spawn_policy_resolution_task, spawn_pool_prefetch_task, -}; diff --git a/crates/l1/src/state/tip403/cache.rs b/crates/l1/src/state/tip403/cache.rs deleted file mode 100644 index 142f61732..000000000 --- a/crates/l1/src/state/tip403/cache.rs +++ /dev/null @@ -1,1301 +0,0 @@ -//! Block-versioned in-memory cache of TIP-403 transfer policy state from Tempo L1. -//! -//! The zone sequencer needs to know whether addresses are authorized under the TIP-403 policy -//! of each token enabled on the zone. This cache mirrors the L1 `TIP403Registry` storage -//! layout: -//! -//! - **Token → policy ID**: Each token address maps to a `transferPolicyId` via -//! [`HeightVersioned`](crate::state::versioned::HeightVersioned), tracking the -//! `TransferPolicyUpdate` event. -//! -//! - **Policy records**: Each policy ID maps to a [`CachedPolicy`] containing: -//! - The policy type (whitelist, blacklist, or compound). -//! - Policy set via [`PolicySet`] — a `HashSet` baseline plus per-block deltas -//! mirroring `WhitelistUpdated` / `BlacklistUpdated` events. -//! - Compound sub-policy IDs for sender, recipient, and mint recipient roles. -//! -//! ## Special policies -//! -//! Policy ID `0` always rejects, policy ID `1` always allows. These are handled inline by -//! [`PolicyCacheInner::is_authorized`] without any storage lookups. -//! -//! ## Unknown entries -//! -//! Users with no recorded set event are treated as "unknown" — cache lookups return -//! `None` so the caller falls back to RPC. This avoids silent false negatives when the -//! subscriber started after a user was added to a whitelist. Users who were explicitly added -//! or removed are tracked by [`PolicySet`]. -//! -//! ## Compound policies (TIP-1015) -//! -//! A compound policy delegates authorization to sub-policies based on the user's role -//! (sender, recipient, or mint recipient). The [`is_authorized`](PolicyCacheInner::is_authorized) -//! method accepts an [`AuthRole`] to resolve the correct sub-policy. -//! -//! ## Resync handling -//! -//! The cache has no per-block rollback. Defensive resync paths should call -//! [`PolicyCacheInner::clear`] and let event replay plus RPC fallback repopulate entries. - -use alloy_primitives::Address; -use alloy_provider::DynProvider; -use derive_more::Deref; -use parking_lot::RwLock; -use std::{collections::HashMap, sync::Arc}; -use tempo_alloy::TempoNetwork; -use tempo_contracts::precompiles::ITIP403Registry::PolicyType; -use tracing::info; - -use super::{builtin_authorization, events::PolicyEvent, policy_set::PolicySet}; - -use crate::state::versioned::HeightVersioned; - -/// Thread-safe TIP-403 policy cache backed by an `Arc>`. -#[derive(Debug, Clone, Deref, Default)] -pub struct PolicyCache { - #[deref] - inner: Arc>, -} - -impl PolicyCache { - /// Returns the last L1 block number tracked by the cache. - pub fn last_l1_block(&self) -> u64 { - self.read().last_l1_block() - } - - /// Seeds the cache with the initial L1 block height so RPC fallback queries - /// target the correct block before the subscriber has processed any events. - pub fn set_last_l1_block(&self, block_number: u64) { - self.write().set_last_l1_block(block_number); - } - - /// Collapse versioned entries up to `block_number`. - /// - /// Called by the engine after successfully processing an L1 block. Only the - /// engine should drive this — the subscriber must not advance past blocks the - /// engine hasn't consumed yet. - pub fn advance(&self, block_number: u64) { - self.write().advance(block_number); - } - - /// Query the current `transferPolicyId` for each tracked token and seed it - /// into the cache. This ensures the cache knows about tokens that have never - /// had a `TransferPolicyUpdate` event (i.e. still using the default policy). - /// - /// Fails if any token's `transferPolicyId` cannot be resolved — all enabled - /// tokens must be seeded for the zone to enforce policies correctly. - pub async fn seed_token_policies( - &self, - portal_address: Address, - tracked_tokens: &[Address], - provider: &DynProvider, - ) -> eyre::Result<()> { - use tempo_contracts::precompiles::ITIP20; - - let block_number = self.last_l1_block(); - - let seeded = futures::future::join_all(tracked_tokens.iter().map(|token| { - let tip20 = ITIP20::new(*token, provider); - async move { - let policy_id = tip20 - .transferPolicyId() - .block(alloy_rpc_types_eth::BlockId::number(block_number)) - .call() - .await - .map_err(|e| { - eyre::eyre!( - "failed to seed transferPolicyId for token {token} \ - (portal {portal_address}): {e}" - ) - })?; - Ok::<_, eyre::Report>((*token, policy_id)) - } - })) - .await - .into_iter() - .collect::>>()?; - - let mut w = self.write(); - for (token, policy_id) in seeded { - info!(%token, policy_id, block_number, "Seeded token policy from L1"); - w.set_token_policy(token, block_number, policy_id); - } - - Ok(()) - } -} - -/// Block-versioned cache of TIP-403 policy state from Tempo L1. -/// -/// Mirrors the on-chain `TIP403Registry` storage layout with: -/// - Token → `transferPolicyId` mapping (from TIP-20 `TransferPolicyUpdate` events). -/// - Policy ID → policy record (type, policy set, compound data). -/// -/// This allows the zone sequencer to evaluate transfer authorization without RPC round-trips. -#[derive(Debug, Default)] -pub struct PolicyCacheInner { - /// Per-token transfer policy ID. - tokens: HashMap>, - /// Per-policy-ID records (type, policy set, compound data). - /// - /// Populated from **all** `PolicyCreated`, `CompoundPolicyCreated`, - /// `WhitelistUpdated`, and `BlacklistUpdated` events on the global - /// `TIP403Registry` — not filtered to this zone's tokens. This is - /// intentional: a token can switch to any policy via - /// `TransferPolicyUpdate`, so pre-caching all policies avoids RPC - /// round-trips on policy switch. The memory overhead is negligible. - policies: HashMap, - /// Highest L1 block number processed by the engine. - /// - /// This equals the last block height the engine has processed and - /// should advance in lockstep with the L1 head tracked by the `TempoState` - /// precompile. The - /// [`PolicyResolutionTask`](super::PolicyResolutionTask) reads this to - /// query L1 at the correct block height for cache-miss RPC fallback. - last_l1_block: u64, -} - -impl PolicyCacheInner { - /// Returns the `transferPolicyId` for a token at the given block, or `None` if not cached. - pub fn get_token_policy(&self, token: Address, block_number: u64) -> Option { - self.tokens.get(&token)?.get(block_number) - } - - /// Sets the `transferPolicyId` for a token at the given block. - pub fn set_token_policy(&mut self, token: Address, block_number: u64, policy_id: u64) { - self.tokens - .entry(token) - .or_default() - .set(block_number, policy_id); - } - - /// Sets the policy type for a policy ID. - pub fn set_policy_type(&mut self, policy_id: u64, policy_type: PolicyType) { - self.get_policy_entry(policy_id).policy_type = Some(policy_type); - } - - /// Sets whether `user` is in a policy set at the given block. - pub fn set_policy_status( - &mut self, - policy_id: u64, - user: Address, - block_number: u64, - in_set: bool, - ) { - self.get_policy_entry(policy_id) - .policy_set - .record_status(user, block_number, in_set); - } - - /// Sets compound policy sub-policy IDs and marks the policy as compound. - pub fn set_compound(&mut self, policy_id: u64, compound: CompoundData) { - let entry = self.get_policy_entry(policy_id); - entry.policy_type = Some(PolicyType::COMPOUND); - entry.compound = Some(compound); - } - - /// Returns a reference to the per-policy-ID records for direct inspection. - pub fn policies(&self) -> &HashMap { - &self.policies - } - - /// Returns all token addresses currently tracked by the cache. - pub fn tracked_tokens(&self) -> Vec
{ - self.tokens.keys().copied().collect() - } - - /// Returns the number of token-to-policy mappings in the cache. - pub fn num_token_policies(&self) -> usize { - self.tokens.len() - } - - /// Returns the highest L1 block number processed by the cache. - /// - /// Returns `0` if no events have been applied yet. - pub fn last_l1_block(&self) -> u64 { - self.last_l1_block - } - - /// Sets the highest L1 block number, unless the cache already tracks a higher block. - pub fn set_last_l1_block(&mut self, block_number: u64) { - self.last_l1_block = self.last_l1_block.max(block_number); - } - - /// Returns a mutable reference to the [`CachedPolicy`] for the given policy ID, - /// inserting a default entry if absent. - fn get_policy_entry(&mut self, policy_id: u64) -> &mut CachedPolicy { - self.policies.entry(policy_id).or_default() - } - - /// Check if an address is authorized under a token's transfer policy at the given block. - /// - /// This mirrors the L1 `TIP403Registry.isAuthorized` / `isAuthorizedSender` / - /// `isAuthorizedRecipient` / `isAuthorizedMintRecipient` functions. The `role` parameter - /// selects which sub-policy to check for compound policies; for simple policies it is - /// ignored. - /// - /// Returns `Some(true/false)` if policy data is cached, or `None` when the policy ID, - /// type, or compound data is unknown (caller should fall back to RPC or fail-open). - pub fn is_authorized( - &self, - token: Address, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Option { - let policy_id = self.tokens.get(&token)?.get(block_number)?; - self.check_policy(policy_id, user, block_number, role) - } - - /// Resolve authorization for a policy ID, handling builtins, simple, and compound policies. - /// - /// - **Builtins** (0 = reject all, 1 = allow all): resolved inline. - /// - **Simple** (whitelist/blacklist): checks policy set. - /// - **Compound** (TIP-1015): delegates to the sub-policy selected by `role`. - /// - /// Returns `None` when the policy data is not cached (caller should fail-closed or - /// fall back to RPC depending on context). - pub fn check_policy( - &self, - policy_id: u64, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Option { - if let Some(authorized) = builtin_authorization(policy_id) { - return Some(authorized); - } - - let policy = self.policies.get(&policy_id)?; - let policy_type = policy.policy_type?; - - match policy_type { - PolicyType::WHITELIST => { - if !policy.policy_set.is_known(&user) { - return None; - } - Some(policy.policy_set.contains(user, block_number)) - } - PolicyType::BLACKLIST => { - if !policy.policy_set.is_known(&user) { - return None; - } - Some(!policy.policy_set.contains(user, block_number)) - } - PolicyType::COMPOUND => { - let compound = policy.compound.as_ref()?; - match role { - AuthRole::Sender => { - self.check_simple(compound.sender_policy_id, user, block_number) - } - AuthRole::Recipient => { - self.check_simple(compound.recipient_policy_id, user, block_number) - } - AuthRole::MintRecipient => { - self.check_simple(compound.mint_recipient_policy_id, user, block_number) - } - AuthRole::Transfer => { - // Check both sender AND recipient — short-circuit on sender failure. - let sender_ok = - self.check_simple(compound.sender_policy_id, user, block_number)?; - if !sender_ok { - return Some(false); - } - self.check_simple(compound.recipient_policy_id, user, block_number) - } - } - } - _ => None, - } - } - - /// Check authorization against a simple (non-compound) policy. - /// - /// Handles builtins and whitelist/blacklist. Returns `None` for compound sub-policies - /// (compound-of-compound is invalid on L1). - pub fn check_simple(&self, policy_id: u64, user: Address, block_number: u64) -> Option { - if let Some(authorized) = builtin_authorization(policy_id) { - return Some(authorized); - } - - let policy = self.policies.get(&policy_id)?; - let policy_type = policy.policy_type?; - - match policy_type { - PolicyType::WHITELIST => { - if !policy.policy_set.is_known(&user) { - return None; - } - Some(policy.policy_set.contains(user, block_number)) - } - PolicyType::BLACKLIST => { - if !policy.policy_set.is_known(&user) { - return None; - } - Some(!policy.policy_set.contains(user, block_number)) - } - _ => None, - } - } - - /// Apply a batch of decoded policy events for a single block. - /// - /// This is the primary ingestion path used by [`L1Subscriber`](crate::l1::L1Subscriber). - /// Events are decoded outside the write lock, then applied here in one batch. - /// - /// **NOTE:** When a `TokenPolicyChanged` event points to a policy ID that was created - /// before the subscriber started, the cache will have no set data for that policy. - /// Authorization queries will return `None` (cache miss), causing the - /// [`PolicyProvider`](super::PolicyProvider) to fall back to per-user L1 RPC. Ideally, - /// the subscriber should kick off background pre-fetching of the new policy's type and - /// policy set on `TokenPolicyChanged` to avoid cold-start RPC latency. - pub fn apply_events(&mut self, block_number: u64, events: &[PolicyEvent]) { - for event in events { - match event { - PolicyEvent::MembershipChanged { - policy_id, - account, - in_set, - } => { - self.set_policy_status(*policy_id, *account, block_number, *in_set); - } - PolicyEvent::TokenPolicyChanged { token, policy_id } => { - self.set_token_policy(*token, block_number, *policy_id); - } - PolicyEvent::PolicyCreated { - policy_id, - policy_type, - } => { - self.set_policy_type(*policy_id, *policy_type); - } - PolicyEvent::CompoundPolicyCreated { - policy_id, - sender_policy_id, - recipient_policy_id, - mint_recipient_policy_id, - } => { - self.set_compound( - *policy_id, - CompoundData { - sender_policy_id: *sender_policy_id, - recipient_policy_id: *recipient_policy_id, - mint_recipient_policy_id: *mint_recipient_policy_id, - }, - ); - } - } - } - } - - /// Clears all cached policy data. `last_l1_block` is preserved — the engine - /// will advance it when it reprocesses blocks after a reorg. - pub fn clear(&mut self) { - self.tokens.clear(); - self.policies.clear(); - } - - /// Collapse all history before `min_block` into single baseline entries. - pub fn flatten(&mut self, min_block: u64) { - for v in self.tokens.values_mut() { - v.flatten(min_block); - } - for policy in self.policies.values_mut() { - policy.policy_set.flatten(min_block); - } - } - - /// Advance the baseline to `new_height` for all tracked entries. - /// - /// Only the engine should call this after successfully processing a block. - /// Advancing past unprocessed blocks would fold pending deltas prematurely, - /// causing incorrect authorization decisions for in-flight blocks. The - /// subscriber writes events via [`apply_events`](Self::apply_events) but never - /// advances the cache. - pub fn advance(&mut self, new_height: u64) { - self.last_l1_block = self.last_l1_block.max(new_height); - info!( - target: "zone::policy", - new_height, - tokens = self.tokens.len(), - policies = self.policies.len(), - "Advancing policy cache baseline" - ); - for v in self.tokens.values_mut() { - v.advance(new_height); - } - for policy in self.policies.values_mut() { - policy.policy_set.advance(new_height); - } - } -} - -pub(super) use zone_primitives::policy::AuthRole; - -/// Per-policy-ID cached record, mirroring `TIP403Registry.policy_records[id]`. -#[derive(Debug, Default)] -pub struct CachedPolicy { - /// Policy type. `None` if the `PolicyCreated` event hasn't been observed yet. - pub policy_type: Option, - /// Policy set for simple (non-compound) policies. - pub policy_set: PolicySet, - /// Compound sub-policy IDs. `None` for simple policies. - pub compound: Option, -} - -/// Sub-policy IDs for a compound policy (TIP-1015). -/// -/// Created once via `createCompoundPolicy` on L1 and never modified. -#[derive(Debug, Clone, Copy)] -pub struct CompoundData { - pub sender_policy_id: u64, - pub recipient_policy_id: u64, - pub mint_recipient_policy_id: u64, -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::address; - - const TOKEN: Address = address!("0x20C0000000000000000000000000000000000000"); - const USER_A: Address = address!("0x0000000000000000000000000000000000000001"); - const USER_B: Address = address!("0x0000000000000000000000000000000000000002"); - - // --- PolicySet tests --- - - #[test] - fn policy_set_default_is_not_in_set() { - let set = PolicySet::default(); - assert!(!set.contains(USER_A, 100)); - } - - #[test] - fn policy_set_add_and_remove() { - let mut set = PolicySet::default(); - set.record_status(USER_A, 10, true); - assert!(set.contains(USER_A, 10)); - assert!(set.contains(USER_A, 15)); - assert!(!set.contains(USER_A, 5)); - - set.record_status(USER_A, 20, false); - assert!(set.contains(USER_A, 15)); - assert!(!set.contains(USER_A, 25)); - } - - #[test] - fn policy_set_multiple_users_same_block() { - let mut set = PolicySet::default(); - set.record_status(USER_A, 10, true); - set.record_status(USER_B, 10, true); - - assert!(set.contains(USER_A, 10)); - assert!(set.contains(USER_B, 10)); - } - - #[test] - fn policy_set_advance_folds_deltas() { - let mut set = PolicySet::default(); - set.record_status(USER_A, 10, true); - set.record_status(USER_B, 15, true); - set.record_status(USER_A, 20, false); - - set.advance(15); - - // USER_A added at 10 (folded into baseline), USER_B added at 15 (folded) - assert!(set.contains(USER_A, 15)); - assert!(set.contains(USER_B, 15)); - - // USER_A removed at 20 (still pending) - assert!(!set.contains(USER_A, 25)); - } - - #[test] - fn policy_set_at_or_below_baseline_is_ignored() { - let mut set = PolicySet::default(); - set.record_status(USER_A, 10, true); - set.advance(20); - - // Delayed writes from finalized heights must not rewrite baseline membership. - set.record_status(USER_A, 15, false); - set.record_status(USER_A, 20, false); - assert!(set.contains(USER_A, 20)); - - // Stale writes must not mark unknown users as observed either. - set.record_status(USER_B, 15, false); - assert!(!set.is_known(&USER_B)); - - set.record_status(USER_A, 21, false); - assert!(!set.contains(USER_A, 21)); - } - - #[test] - fn policy_set_initial_baseline_write_is_ignored() { - let mut set = PolicySet::default(); - - set.record_status(USER_A, 0, false); - assert!(!set.is_known(&USER_A)); - assert!(!set.contains(USER_A, 0)); - - set.record_status(USER_B, 0, true); - assert!(!set.is_known(&USER_B)); - assert!(!set.contains(USER_B, 0)); - - set.record_status(USER_B, 1, true); - assert!(set.is_known(&USER_B)); - assert!(set.contains(USER_B, 1)); - } - - // --- PolicyCacheInner tests: simple policies --- - - #[test] - fn special_policy_always_reject() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 0); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(false) - ); - } - - #[test] - fn token_policy_seed_after_genesis_is_cached() { - let mut cache = PolicyCacheInner::default(); - - cache.set_token_policy(TOKEN, 1, 1); - - assert_eq!(cache.get_token_policy(TOKEN, 1), Some(1)); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 1, AuthRole::Transfer), - Some(true) - ); - } - - #[test] - fn special_policy_always_allow() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 1); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - } - - #[test] - fn whitelist_authorized_when_in_set() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 10, true); - cache.set_policy_status(2, USER_B, 10, false); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 10, AuthRole::Transfer), - Some(false) - ); - } - - #[test] - fn blacklist_authorized_when_not_in_set() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 3); - cache.set_policy_type(3, PolicyType::BLACKLIST); - cache.set_policy_status(3, USER_A, 10, true); - cache.set_policy_status(3, USER_B, 10, false); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(false) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 10, AuthRole::Transfer), - Some(true) - ); - } - - #[test] - fn blacklist_unknown_user_returns_none() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 3); - cache.set_policy_type(3, PolicyType::BLACKLIST); - - // USER_A has no set data — unknown, caller should fall back to RPC - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn whitelist_unknown_user_returns_none() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - - // USER_A has no set data — unknown, caller should fall back to RPC - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn returns_none_on_missing_token_policy() { - let cache = PolicyCacheInner::default(); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn returns_none_on_missing_policy_type() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 5); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn block_versioned_policy_change() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 1); - cache.set_token_policy(TOKEN, 20, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 20, true); - - // At block 15: policy_id=1 (always allow) - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 15, AuthRole::Transfer), - Some(true) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 15, AuthRole::Transfer), - Some(true) - ); - - // At block 25: policy_id=2 (whitelist), USER_A in set - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 25, AuthRole::Transfer), - Some(true) - ); - // USER_B never observed → None (fall back to RPC) - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 25, AuthRole::Transfer), - None - ); - } - - #[test] - fn block_versioned_set_change() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 10, false); - cache.set_policy_status(2, USER_A, 20, true); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 15, AuthRole::Transfer), - Some(false) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 25, AuthRole::Transfer), - Some(true) - ); - } - - #[test] - fn clear_removes_all_data() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 10, true); - - cache.clear(); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn flatten_keeps_baseline() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 5, 1); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_token_policy(TOKEN, 20, 3); - - cache.flatten(15); - - // Below baseline: returns the baseline value (2, set at block 10) - assert_eq!(cache.get_token_policy(TOKEN, 5), Some(2)); - assert_eq!(cache.get_token_policy(TOKEN, 10), Some(2)); - assert_eq!(cache.get_token_policy(TOKEN, 15), Some(2)); - assert_eq!(cache.get_token_policy(TOKEN, 20), Some(3)); - } - - #[test] - fn shared_policy_across_tokens() { - let mut cache = PolicyCacheInner::default(); - let token2: Address = address!("0x20C0000000000000000000000000000000000001"); - - // Two tokens share policy 2 - cache.set_token_policy(TOKEN, 10, 2); - cache.set_token_policy(token2, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 10, true); - - // Both tokens see the same policy set (per-policy, no fan-out needed) - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - assert_eq!( - cache.is_authorized(token2, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - } - - #[test] - fn shared_blacklist_across_tokens() { - let mut cache = PolicyCacheInner::default(); - let token2: Address = address!("0x20C0000000000000000000000000000000000001"); - - cache.set_token_policy(TOKEN, 10, 2); - cache.set_token_policy(token2, 10, 2); - cache.set_policy_type(2, PolicyType::BLACKLIST); - cache.set_policy_status(2, USER_A, 10, true); - - // BLACKLIST: authorized when NOT in set - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(false) - ); - assert_eq!( - cache.is_authorized(token2, USER_A, 10, AuthRole::Transfer), - Some(false) - ); - // USER_B never observed → None (fall back to RPC) - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 10, AuthRole::Transfer), - None - ); - assert_eq!( - cache.is_authorized(token2, USER_B, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn tokens_with_different_policies() { - let mut cache = PolicyCacheInner::default(); - let token2: Address = address!("0x20C0000000000000000000000000000000000001"); - - cache.set_token_policy(TOKEN, 10, 2); - cache.set_token_policy(token2, 10, 3); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_type(3, PolicyType::BLACKLIST); - cache.set_policy_status(2, USER_A, 10, true); - cache.set_policy_status(3, USER_A, 10, true); - - // TOKEN uses whitelist policy 2: USER_A whitelisted → authorized - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - // token2 uses blacklist policy 3: USER_A blacklisted → NOT authorized - assert_eq!( - cache.is_authorized(token2, USER_A, 10, AuthRole::Transfer), - Some(false) - ); - } - - #[test] - fn advance_then_lookup() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::BLACKLIST); - cache.set_policy_status(2, USER_A, 10, true); - cache.set_policy_status(2, USER_A, 20, false); - - cache.advance(15); - - // After advancing to 15, baseline includes block-10 state. - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 12, AuthRole::Transfer), - Some(false) - ); // blacklisted - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 25, AuthRole::Transfer), - Some(true) - ); // unblacklisted at 20 - } - - #[test] - fn stale_membership_write_after_advance_cannot_poison_blacklist() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 3); - cache.set_policy_type(3, PolicyType::BLACKLIST); - cache.set_policy_status(3, USER_A, 10, false); - cache.advance(10); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - - cache.set_policy_status(3, USER_A, 12, true); - cache.advance(12); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 12, AuthRole::Transfer), - Some(false) - ); - - // Simulates an RPC fallback result captured before the block-12 blacklist event - // and returning after the engine advanced the cache baseline. - cache.set_policy_status(3, USER_A, 10, false); - cache.set_policy_status(3, USER_A, 12, false); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 13, AuthRole::Transfer), - Some(false) - ); - } - - #[test] - fn stale_membership_write_after_advance_does_not_mark_unknown_observed() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 3); - cache.set_policy_type(3, PolicyType::BLACKLIST); - cache.advance(10); - - cache.set_policy_status(3, USER_B, 10, false); - - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn stale_token_policy_write_after_advance_cannot_revert_policy() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.advance(10); - - cache.set_token_policy(TOKEN, 12, 3); - cache.advance(12); - - cache.set_token_policy(TOKEN, 10, 1); - cache.set_token_policy(TOKEN, 12, 1); - - assert_eq!(cache.get_token_policy(TOKEN, 12), Some(3)); - assert_eq!(cache.get_token_policy(TOKEN, 13), Some(3)); - - cache.set_token_policy(TOKEN, 13, 1); - assert_eq!(cache.get_token_policy(TOKEN, 13), Some(1)); - } - - #[test] - fn flatten_preserves_token_entries() { - let mut cache = PolicyCacheInner::default(); - let token2: Address = address!("0x20C0000000000000000000000000000000000001"); - - cache.set_token_policy(TOKEN, 10, 2); - cache.set_token_policy(token2, 10, 3); - - cache.flatten(15); - - // Both should survive because their token entries have values - assert_eq!(cache.get_token_policy(TOKEN, 15), Some(2)); - assert_eq!(cache.get_token_policy(token2, 15), Some(3)); - } - - #[test] - fn policy_change_mid_block_range() { - let mut cache = PolicyCacheInner::default(); - - // Start with whitelist at block 10, switch to blacklist policy at block 20 - cache.set_token_policy(TOKEN, 10, 2); - cache.set_token_policy(TOKEN, 20, 3); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_type(3, PolicyType::BLACKLIST); - cache.set_policy_status(2, USER_A, 10, true); - cache.set_policy_status(3, USER_A, 10, true); - - // At block 15 (whitelist policy 2), USER_A is in set → authorized - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 15, AuthRole::Transfer), - Some(true) - ); - // At block 25 (blacklist policy 3), USER_A is in set → NOT authorized - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 25, AuthRole::Transfer), - Some(false) - ); - } - - // --- Compound policy tests (TIP-1015) --- - - #[test] - fn compound_policy_sender_check() { - let mut cache = PolicyCacheInner::default(); - // Simple sub-policies - cache.set_policy_type(2, PolicyType::BLACKLIST); // sender policy - cache.set_policy_type(3, PolicyType::WHITELIST); // recipient policy - cache.set_policy_status(2, USER_A, 10, true); // USER_A blacklisted as sender - cache.set_policy_status(3, USER_A, 10, true); // USER_A whitelisted as recipient - - // Compound policy referencing sub-policies - cache.set_compound( - 5, - CompoundData { - sender_policy_id: 2, - recipient_policy_id: 3, - mint_recipient_policy_id: 1, // builtin allow - }, - ); - cache.set_token_policy(TOKEN, 10, 5); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Sender), - Some(false) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Recipient), - Some(true) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::MintRecipient), - Some(true) - ); - } - - #[test] - fn compound_policy_transfer_checks_both() { - let mut cache = PolicyCacheInner::default(); - cache.set_policy_type(2, PolicyType::WHITELIST); // sender - cache.set_policy_type(3, PolicyType::WHITELIST); // recipient - - cache.set_compound( - 5, - CompoundData { - sender_policy_id: 2, - recipient_policy_id: 3, - mint_recipient_policy_id: 1, - }, - ); - cache.set_token_policy(TOKEN, 10, 5); - - // Neither whitelisted → unknown (USER_A never observed) - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - - // Only sender whitelisted → fails on recipient (still unknown for recipient sub-policy) - cache.set_policy_status(2, USER_A, 10, true); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - - // Both whitelisted → authorized - cache.set_policy_status(3, USER_A, 10, true); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - } - - #[test] - fn compound_policy_with_builtin_sub_policies() { - let mut cache = PolicyCacheInner::default(); - // Compound: sender=allow(1), recipient=reject(0), mint=allow(1) - cache.set_compound( - 5, - CompoundData { - sender_policy_id: 1, - recipient_policy_id: 0, - mint_recipient_policy_id: 1, - }, - ); - cache.set_token_policy(TOKEN, 10, 5); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Sender), - Some(true) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Recipient), - Some(false) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::MintRecipient), - Some(true) - ); - // Transfer: sender=true, recipient=false → false - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(false) - ); - } - - #[test] - fn compound_returns_none_when_sub_policy_missing() { - let mut cache = PolicyCacheInner::default(); - // Compound references sub-policy 99 which doesn't exist - cache.set_compound( - 5, - CompoundData { - sender_policy_id: 99, - recipient_policy_id: 3, - mint_recipient_policy_id: 1, - }, - ); - cache.set_token_policy(TOKEN, 10, 5); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Sender), - None - ); - } - - #[test] - fn compound_returns_none_when_compound_data_missing() { - let mut cache = PolicyCacheInner::default(); - // Policy 5 has COMPOUND type but no compound data set - cache.set_policy_type(5, PolicyType::COMPOUND); - cache.set_token_policy(TOKEN, 10, 5); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Sender), - None - ); - } - - #[test] - fn apply_events_with_policy_created() { - let mut cache = PolicyCacheInner::default(); - let events = vec![ - PolicyEvent::PolicyCreated { - policy_id: 2, - policy_type: PolicyType::WHITELIST, - }, - PolicyEvent::MembershipChanged { - policy_id: 2, - account: USER_A, - in_set: true, - }, - PolicyEvent::TokenPolicyChanged { - token: TOKEN, - policy_id: 2, - }, - ]; - - cache.apply_events(10, &events); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - // USER_B never observed → None (fall back to RPC) - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 10, AuthRole::Transfer), - None - ); - } - - // --- `observed` tracking and `advance` interaction tests --- - - #[test] - fn observed_survives_advance() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 10, true); - - // Before advance: known and authorized - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - - // Advance past the set block — folds pending into baseline - cache.advance(20); - - // After advance: still known and still authorized (observed persists) - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 25, AuthRole::Transfer), - Some(true) - ); - - // Unobserved user still returns None after advance - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 25, AuthRole::Transfer), - None - ); - } - - #[test] - fn observed_survives_advance_for_removed_set_entry() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - - // Add then remove USER_A - cache.set_policy_status(2, USER_A, 10, true); - cache.set_policy_status(2, USER_A, 20, false); - - // Advance past both events - cache.advance(25); - - // USER_A was removed but is still "observed" — returns Some(false), not None - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 30, AuthRole::Transfer), - Some(false) - ); - } - - #[test] - fn observed_survives_advance_blacklist() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::BLACKLIST); - cache.set_policy_status(2, USER_A, 10, true); // blacklisted - - cache.advance(20); - - // After advance: observed, blacklisted → not authorized - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 25, AuthRole::Transfer), - Some(false) - ); - - // Unobserved user → None (not Some(true) which would be a false positive) - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 25, AuthRole::Transfer), - None - ); - } - - #[test] - fn clear_resets_observed() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 10, true); - - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - - cache.clear(); - - // After clear, observed is gone — returns None, not Some(false) - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - None - ); - } - - #[test] - fn policy_set_known_direct() { - let mut set = PolicySet::default(); - - // Fresh set: nobody known - assert!(!set.is_known(&USER_A)); - assert!(!set.is_known(&USER_B)); - - // Add USER_A - set.record_status(USER_A, 10, true); - assert!(set.is_known(&USER_A)); - assert!(!set.is_known(&USER_B)); - - // Advance past the event - set.advance(20); - assert!(set.is_known(&USER_A), "observed must survive advance"); - assert!(!set.is_known(&USER_B)); - - // Clear resets everything - set.clear(); - assert!(!set.is_known(&USER_A)); - } - - #[test] - fn multiple_advances_preserve_observed() { - let mut cache = PolicyCacheInner::default(); - cache.set_token_policy(TOKEN, 5, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - - // Events at different blocks - cache.set_policy_status(2, USER_A, 10, true); - cache.set_policy_status(2, USER_B, 20, true); - - // Advance past first event only - cache.advance(15); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 15, AuthRole::Transfer), - Some(true) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 25, AuthRole::Transfer), - Some(true) // still in pending, but observed - ); - - // Advance past second event - cache.advance(25); - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 30, AuthRole::Transfer), - Some(true) - ); - assert_eq!( - cache.is_authorized(TOKEN, USER_B, 30, AuthRole::Transfer), - Some(true) - ); - } - - #[test] - fn apply_events_with_compound_policy() { - let mut cache = PolicyCacheInner::default(); - // Pre-populate simple sub-policies - cache.set_policy_type(2, PolicyType::BLACKLIST); - cache.set_policy_type(3, PolicyType::WHITELIST); - cache.set_policy_status(2, USER_A, 10, false); // explicitly not blacklisted - cache.set_policy_status(3, USER_A, 10, true); - - let events = vec![ - PolicyEvent::CompoundPolicyCreated { - policy_id: 5, - sender_policy_id: 2, - recipient_policy_id: 3, - mint_recipient_policy_id: 1, - }, - PolicyEvent::TokenPolicyChanged { - token: TOKEN, - policy_id: 5, - }, - ]; - - cache.apply_events(10, &events); - - // Sender (blacklist, explicitly not in set → authorized), Recipient (whitelist, in set → authorized) - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::Transfer), - Some(true) - ); - // MintRecipient (builtin allow) - assert_eq!( - cache.is_authorized(TOKEN, USER_A, 10, AuthRole::MintRecipient), - Some(true) - ); - } -} diff --git a/crates/l1/src/state/tip403/events.rs b/crates/l1/src/state/tip403/events.rs deleted file mode 100644 index 16d4babfa..000000000 --- a/crates/l1/src/state/tip403/events.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! TIP-403 and TIP-20 policy event decoding. -//! -//! The L1 subscriber decodes raw receipt logs into [`PolicyEvent`] values outside -//! the cache write lock. The cache then applies those events in block order. - -use alloy_primitives::Address; -use alloy_sol_types::{SolEvent, SolEventInterface}; -use tempo_contracts::precompiles::ITIP403Registry::PolicyType; - -/// A decoded L1 policy event ready to be applied to the cache. -/// -/// The [`L1Subscriber`](crate::l1::L1Subscriber) decodes raw logs into these events -/// outside the cache write lock, then applies them in batch via -/// [`PolicyCacheInner::apply_events`](super::PolicyCacheInner::apply_events). -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum PolicyEvent { - /// A user's membership in a policy set changed (`WhitelistUpdated` / `BlacklistUpdated`). - MembershipChanged { - policy_id: u64, - account: Address, - in_set: bool, - }, - /// A token's transfer policy ID changed (`TransferPolicyUpdate`). - TokenPolicyChanged { token: Address, policy_id: u64 }, - /// A new simple policy was created on L1 (`PolicyCreated`). - PolicyCreated { - policy_id: u64, - policy_type: PolicyType, - }, - /// A new compound policy was created on L1 (`CompoundPolicyCreated`). - CompoundPolicyCreated { - policy_id: u64, - sender_policy_id: u64, - recipient_policy_id: u64, - mint_recipient_policy_id: u64, - }, -} - -impl PolicyEvent { - /// Try to decode an `ITIP403Registry` log into a [`PolicyEvent`]. - /// - /// Handles `WhitelistUpdated`, `BlacklistUpdated`, `PolicyCreated`, and - /// `CompoundPolicyCreated` events. `PolicyAdminUpdated` is logged but ignored - /// (returns `None`). Returns `None` for unrecognised logs. - pub fn decode_registry(log: &alloy_rpc_types_eth::Log) -> Option { - use tempo_contracts::precompiles::ITIP403Registry::{ - BlacklistUpdated, CompoundPolicyCreated, ITIP403RegistryEvents, PolicyCreated, - WhitelistUpdated, - }; - - let event = match ITIP403RegistryEvents::decode_log(&log.inner) { - Ok(decoded) => decoded.data, - Err(e) => { - tracing::warn!(error = %e, "Failed to decode TIP-403 event"); - return None; - } - }; - - match event { - ITIP403RegistryEvents::BlacklistUpdated(BlacklistUpdated { - policyId, - account, - restricted, - .. - }) => { - tracing::info!( - policy_id = policyId, - account = %account, - restricted, - "Decoded BlacklistUpdated" - ); - Some(Self::MembershipChanged { - policy_id: policyId, - account, - in_set: restricted, - }) - } - ITIP403RegistryEvents::WhitelistUpdated(WhitelistUpdated { - policyId, - account, - allowed, - .. - }) => { - tracing::info!( - policy_id = policyId, - account = %account, - allowed, - "Decoded WhitelistUpdated" - ); - Some(Self::MembershipChanged { - policy_id: policyId, - account, - in_set: allowed, - }) - } - ITIP403RegistryEvents::PolicyCreated(PolicyCreated { - policyId, - policyType, - .. - }) => { - tracing::info!( - policy_id = policyId, - policy_type = ?policyType, - "New policy created on L1" - ); - Some(Self::PolicyCreated { - policy_id: policyId, - policy_type: policyType, - }) - } - ITIP403RegistryEvents::CompoundPolicyCreated(CompoundPolicyCreated { - policyId, - senderPolicyId, - recipientPolicyId, - mintRecipientPolicyId, - .. - }) => { - tracing::info!( - policy_id = policyId, - sender_policy_id = senderPolicyId, - recipient_policy_id = recipientPolicyId, - mint_recipient_policy_id = mintRecipientPolicyId, - "Compound policy created on L1" - ); - Some(Self::CompoundPolicyCreated { - policy_id: policyId, - sender_policy_id: senderPolicyId, - recipient_policy_id: recipientPolicyId, - mint_recipient_policy_id: mintRecipientPolicyId, - }) - } - ITIP403RegistryEvents::PolicyAdminUpdated(event) => { - tracing::debug!( - policy_id = event.policyId, - admin = %event.admin, - "Policy admin updated on L1" - ); - None - } - ITIP403RegistryEvents::ReceivePolicyUpdated(event) => { - tracing::debug!( - policy_id = ?event, - "Receive policy updated on L1 (ignored)" - ); - None - } - } - } - - /// Try to decode a TIP-20 `TransferPolicyUpdate` log into a - /// [`PolicyEvent::TokenPolicyChanged`]. - /// - /// The caller should pre-filter by topic hash before calling this; it will - /// return `None` with a warning if the log does not match. - pub fn decode_tip20(log: &alloy_rpc_types_eth::Log) -> Option { - use tempo_contracts::precompiles::ITIP20::TransferPolicyUpdate; - - let event = match TransferPolicyUpdate::decode_log(&log.inner) { - Ok(decoded) => decoded.data, - Err(e) => { - tracing::warn!(error = %e, "Failed to decode TIP-20 TransferPolicyUpdate"); - return None; - } - }; - - let token = log.address(); - tracing::info!( - token = %token, - new_policy_id = event.newPolicyId, - updater = %event.updater, - "Decoded TransferPolicyUpdate" - ); - Some(Self::TokenPolicyChanged { - token, - policy_id: event.newPolicyId, - }) - } -} diff --git a/crates/l1/src/state/tip403/metrics.rs b/crates/l1/src/state/tip403/metrics.rs deleted file mode 100644 index a7c51f900..000000000 --- a/crates/l1/src/state/tip403/metrics.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Metrics for the TIP-403 policy cache and resolution system. - -use reth_metrics::{ - Metrics, - metrics::{Counter, Gauge, Histogram}, -}; - -/// Metrics for the TIP-403 policy cache, provider, and resolution task. -#[derive(Metrics, Clone)] -#[metrics(scope = "tempo_zone_tip403")] -pub struct Tip403Metrics { - /// Total authorization checks performed (cache + RPC). - pub authorization_checks_total: Counter, - - /// Authorization checks served from cache (hits). - pub cache_hits: Counter, - - /// Authorization checks that required L1 RPC fallback (misses). - pub cache_misses: Counter, - - /// L1 RPC calls that failed during authorization resolution. - pub rpc_errors: Counter, - - /// Duration of L1 RPC authorization resolution in seconds. - pub rpc_resolution_duration_seconds: Histogram, - - /// Total pre-fetch requests submitted to the resolution task. - pub prefetch_requests_total: Counter, - - /// Pre-fetch requests that completed successfully. - pub prefetch_successes: Counter, - - /// Pre-fetch requests that failed. - pub prefetch_failures: Counter, - - /// Number of in-flight concurrent resolution futures in the task. - pub prefetch_in_flight: Gauge, - - /// Number of cached policy records. - pub cached_policies: Gauge, - - /// Number of cached token-to-policy mappings. - pub cached_token_policies: Gauge, - - /// Policy events applied from L1 subscriber. - pub listener_events_applied: Counter, -} diff --git a/crates/l1/src/state/tip403/mod.rs b/crates/l1/src/state/tip403/mod.rs deleted file mode 100644 index 6bc249851..000000000 --- a/crates/l1/src/state/tip403/mod.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! TIP-403 policy cache, provider, and resolution task for the zone sequencer. -//! -//! This module tracks TIP-403 transfer policy state from Tempo L1: -//! -//! - [`PolicyCacheInner`] — block-versioned in-memory cache of policy data. -//! - [`PolicyProvider`] — cache-first, RPC-fallback authorization provider. -//! - [`PolicyResolutionTask`] — background task for pre-fetching authorization data. -//! -//! # Data flow -//! -//! ```text -//! L1 -//! (TIP403Registry, TIP-20 tokens, ZonePortal) -//! | ^ -//! events | | RPC fallback -//! | | -//! L1Subscriber PolicyProvider -//! | | | -//! write | read | | -//! v | | -//! PolicyCache-----+ engine + EVM -//! ^ ^ -//! | | pre-fetch -//! seed (startup) pool_prefetch + ResolutionTask -//! ``` -//! -//! The [`L1Subscriber`](crate::l1::L1Subscriber) extracts policy events from -//! `eth_getBlockReceipts` and applies them (creates, set updates, -//! compound configs) to the [`PolicyCache`]. -//! [`PolicyProvider`] serves authorization queries from cache, falling back to L1 -//! RPC on miss and writing the result back for future lookups. -//! -//! # Startup sequence -//! -//! 1. [`PolicyCache::seed_token_policies`] — bulk-fetch current `transferPolicyId` values -//! from L1 for all tracked tokens and populate the cache baseline. -//! 2. [`spawn_policy_resolution_task`] — start the background resolution task -//! (processes pre-fetch requests from the pool and other callers). -//! 3. [`spawn_pool_prefetch_task`] — watch incoming pool transactions and submit -//! sender/recipient addresses for cache warming. -//! 4. Create [`PolicyProvider`] instances — one for the engine payload builder, -//! one for the EVM precompile, both backed by the same [`PolicyCache`]. -//! -//! # Cache miss resolution -//! -//! The zone advances in lockstep with L1, so the L1Subscriber captures every -//! policy change for enabled tokens from the moment it starts. Cache misses only -//! occur for state that predates the subscriber — either because the zone was -//! created at an arbitrary L1 height or because the sequencer restarted with a -//! cold cache. -//! -//! On a miss, [`PolicyProvider::is_authorized`] falls back to an RPC call against -//! L1 at the block being evaluated and writes the result into the cache. This is -//! safe because L1 is authoritative and the zone never runs ahead of it: the -//! queried state is final for that block, and the subscriber will apply any future -//! changes that supersede it. -//! -//! # Key invariants -//! -//! - **Only the engine drives `advance()`**: the L1Subscriber writes events via -//! `apply_events` but never advances the cache baseline. The engine calls -//! `PolicyCache::advance()` after processing each L1 block, ensuring the -//! cache never runs ahead of the engine's view. - -mod cache; -mod events; -mod metrics; -mod policy_set; -mod pool_prefetch; -pub mod provider; -pub mod task; - -pub use cache::{CachedPolicy, CompoundData, PolicyCache, PolicyCacheInner}; -pub use events::PolicyEvent; -pub use policy_set::PolicySet; -use tempo_precompiles::tip403_registry::{ALLOW_ALL_POLICY_ID, REJECT_ALL_POLICY_ID}; -pub use zone_primitives::policy::AuthRole; - -/// Returns authorization result for built-in policies, `None` for user-created ones. -#[inline] -fn builtin_authorization(policy_id: u64) -> Option { - match policy_id { - ALLOW_ALL_POLICY_ID => Some(true), - REJECT_ALL_POLICY_ID => Some(false), - _ => None, - } -} - -pub use metrics::Tip403Metrics; -pub use pool_prefetch::spawn_pool_prefetch_task; -pub use provider::PolicyProvider; -pub use task::{ - PolicyResolutionTask, PolicyTaskHandle, PolicyTaskMessage, spawn_policy_resolution_task, -}; diff --git a/crates/l1/src/state/tip403/policy_set.rs b/crates/l1/src/state/tip403/policy_set.rs deleted file mode 100644 index 2033829bf..000000000 --- a/crates/l1/src/state/tip403/policy_set.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Block-versioned policy sets for TIP-403 policy tracking. - -use alloy_primitives::Address; -use std::collections::{BTreeMap, HashSet}; - -/// Block-versioned policy set for TIP-403 policy tracking. -/// -/// Models a policy set as a baseline [`HashSet`] plus per-block deltas, matching the L1 -/// event model where `WhitelistUpdated` and `BlacklistUpdated` events arrive as -/// `(address, add/remove)` updates per block. -/// -/// Users not explicitly tracked are treated as "not in set", matching the L1 storage default -/// for `policy_set[policyId][user]`. -#[derive(Debug, Default)] -pub struct PolicySet { - /// Addresses in the set at `baseline_height`. - baseline: HashSet
, - /// Block height up to which the baseline is valid. - baseline_height: u64, - /// Per-block set updates above `baseline_height`. - pending: BTreeMap>, - /// All addresses for which we've ever recorded a set event. Survives `advance()` so - /// we can distinguish "explicitly absent from the set" from "never observed by the subscriber". - observed: HashSet
, -} - -impl PolicySet { - /// Check if `user` is in the set at the given block height. - /// - /// Returns `false` for users with no recorded state, matching the L1 storage default. - pub fn contains(&self, user: Address, block_number: u64) -> bool { - if block_number <= self.baseline_height { - return self.baseline.contains(&user); - } - - // Scan pending blocks in reverse for the latest change affecting this user. - for (_, updates) in self.pending.range(..=block_number).rev() { - for update in updates.iter().rev() { - if update.account == user { - return update.in_set; - } - } - } - - self.baseline.contains(&user) - } - - /// Returns `true` if we've ever recorded a set event for `user` (added or removed). - /// - /// When `false`, the caller should not trust [`contains`](Self::contains) returning `false` - /// because the user may have been added before the subscriber started. - pub fn is_known(&self, user: &Address) -> bool { - self.observed.contains(user) || self.baseline.contains(user) - } - - /// Record a set update at the given block height. - /// - /// Updates at or below the baseline height are ignored. The baseline represents finalized - /// engine-consumed state and is only updated by [`advance`](Self::advance), which prevents - /// delayed RPC fallback results from overwriting newer event-derived membership. - pub fn record_status(&mut self, user: Address, block_number: u64, in_set: bool) { - if block_number <= self.baseline_height { - return; - } - - self.observed.insert(user); - self.pending - .entry(block_number) - .or_default() - .push(PolicySetUpdate { - account: user, - in_set, - }); - } - - /// Advance the baseline to `new_height`, folding pending deltas. - pub fn advance(&mut self, new_height: u64) { - if new_height <= self.baseline_height { - return; - } - - let to_apply: Vec = self.pending.range(..=new_height).map(|(k, _)| *k).collect(); - for block in to_apply { - if let Some(updates) = self.pending.remove(&block) { - for update in updates { - if update.in_set { - self.baseline.insert(update.account); - } else { - self.baseline.remove(&update.account); - } - } - } - } - - self.baseline_height = new_height; - } - - /// Equivalent to [`advance`](Self::advance). - pub fn flatten(&mut self, min_block: u64) { - self.advance(min_block); - } - - /// Returns `true` if no set data has been recorded. - pub fn is_empty(&self) -> bool { - self.baseline.is_empty() && self.pending.is_empty() - } - - /// Clears all set data and resets the baseline height. - pub fn clear(&mut self) { - self.baseline.clear(); - self.baseline_height = 0; - self.pending.clear(); - self.observed.clear(); - } -} - -/// A single set update within a block. -#[derive(Debug, Clone, Copy)] -pub(super) struct PolicySetUpdate { - /// The address whose policy-set status changed. - pub account: Address, - /// Whether the address is in the policy set after this update. - pub in_set: bool, -} diff --git a/crates/l1/src/state/tip403/pool_prefetch.rs b/crates/l1/src/state/tip403/pool_prefetch.rs deleted file mode 100644 index 786d11d87..000000000 --- a/crates/l1/src/state/tip403/pool_prefetch.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Pool transaction prefetch task for TIP-403 policy cache warming. -//! -//! Subscribes to new pending transactions from the transaction pool and -//! extracts sender/recipient addresses from TIP-20 transfer calls. For each -//! address, a [`ResolveAuthorization`](super::PolicyTaskMessage::ResolveAuthorization) -//! request is sent to the [`PolicyResolutionTask`](super::PolicyResolutionTask) -//! via the [`PolicyTaskHandle`](super::PolicyTaskHandle), warming the policy cache -//! before block building. - -use std::collections::HashSet; - -use alloy_primitives::TxKind; -use alloy_sol_types::SolCall; -use reth_transaction_pool::TransactionPool; -use tempo_contracts::precompiles::{DEFAULT_FEE_TOKEN, ITIP20}; -use tempo_precompiles::tip20::is_tip20_prefix; -use tempo_revm::TempoTx; -use tempo_transaction_pool::transaction::TempoPooledTransaction; -use tracing::debug; - -use super::{AuthRole, task::PolicyTaskHandle}; - -/// Spawns a background task that watches for new pool transactions and -/// pre-fetches TIP-403 authorization data for sender/recipient addresses. -/// -/// For every incoming transaction the task warms three categories of cache entries: -/// -/// 1. **Fee payer** — the address paying gas fees, resolved as `AuthRole::Sender` -/// against the transaction's fee token (defaults to pathUSD). AA transactions -/// may specify a different fee token or delegate fee payment to another address. -/// 2. **Transfer sender** — for TIP-20 transfer calls, the sender is resolved -/// against the transfer token. For `transferFrom*`, this is the decoded `from` -/// address rather than the transaction sender. -/// 3. **Transfer recipient** — for `transfer`, `transferWithMemo`, `transferFrom`, -/// and `transferFromWithMemo` calls, the `to` address is decoded from calldata -/// and resolved as `AuthRole::Recipient`. -/// 4. **Batch calls** — Tempo AA transactions can include multiple top-level calls, -/// and each call is inspected independently. -/// -/// The resolution task resolves each request at the cache's last engine-processed -/// L1 block, so callers don't need to track block heights. -/// -/// The task is spawned as a non-critical background task — if it stops, block -/// building still works but may incur more synchronous RPC round-trips on cache -/// misses. -pub fn spawn_pool_prefetch_task( - pool: Pool, - handle: PolicyTaskHandle, - task_executor: reth_tasks::Runtime, -) where - Pool: TransactionPool + 'static, -{ - task_executor.spawn_task(Box::pin(async move { - run_pool_prefetch(pool, handle).await; - })); -} - -async fn run_pool_prefetch(pool: Pool, handle: PolicyTaskHandle) -where - Pool: TransactionPool, -{ - let mut new_txs = pool.new_transactions_listener(); - - while let Some(tx_event) = new_txs.recv().await { - let tx = &tx_event.transaction; - let sender = tx.sender(); - - // Resolve the fee token for this transaction (AA txs may specify one, - // otherwise falls back to DEFAULT_FEE_TOKEN / pathUSD). - let fee_token = tx - .transaction - .inner() - .fee_token() - .unwrap_or(DEFAULT_FEE_TOKEN); - - // Resolve the fee payer (may differ from sender for AA txs with delegated fees) - let fee_payer = tx.transaction.inner().fee_payer(sender).unwrap_or(sender); - - let mut prefetched = HashSet::new(); - - // Pre-fetch fee payer authorization for the fee token (every tx pays fees) - if prefetched.insert((fee_token, fee_payer, AuthRole::Sender)) { - debug!(%fee_token, %fee_payer, "Pre-fetching TIP-403 fee token authorization"); - let _ = handle.send_resolve_policy(fee_token, fee_payer, AuthRole::Sender); - } - - for (kind, input) in tx.transaction.inner().calls() { - let TxKind::Call(token) = kind else { - continue; - }; - if !is_tip20_prefix(token) { - continue; - } - - let Some(selector) = input.first_chunk::<4>() else { - continue; - }; - let args = &input[4..]; - - let (transfer_sender, recipient) = if *selector == ITIP20::transferCall::SELECTOR { - let Ok(call) = ITIP20::transferCall::abi_decode_raw(args) else { - continue; - }; - (sender, call.to) - } else if *selector == ITIP20::transferWithMemoCall::SELECTOR { - let Ok(call) = ITIP20::transferWithMemoCall::abi_decode_raw(args) else { - continue; - }; - (sender, call.to) - } else if *selector == ITIP20::transferFromCall::SELECTOR { - let Ok(call) = ITIP20::transferFromCall::abi_decode_raw(args) else { - continue; - }; - (call.from, call.to) - } else if *selector == ITIP20::transferFromWithMemoCall::SELECTOR { - let Ok(call) = ITIP20::transferFromWithMemoCall::abi_decode_raw(args) else { - continue; - }; - (call.from, call.to) - } else { - continue; - }; - - if prefetched.insert((token, transfer_sender, AuthRole::Sender)) { - debug!(%token, %transfer_sender, "Pre-fetching TIP-403 sender authorization"); - let _ = handle.send_resolve_policy(token, transfer_sender, AuthRole::Sender); - } - - if prefetched.insert((token, recipient, AuthRole::Recipient)) { - debug!(%token, recipient = %recipient, "Pre-fetching TIP-403 recipient authorization"); - let _ = handle.send_resolve_policy(token, recipient, AuthRole::Recipient); - } - } - } - - debug!("Pool prefetch task shutting down"); -} diff --git a/crates/l1/src/state/tip403/provider.rs b/crates/l1/src/state/tip403/provider.rs deleted file mode 100644 index a3a435f0a..000000000 --- a/crates/l1/src/state/tip403/provider.rs +++ /dev/null @@ -1,605 +0,0 @@ -//! Cache-first, RPC-fallback provider for TIP-403 policy authorization. -//! -//! [`PolicyProvider`] wraps a [`PolicyCache`] and an L1 HTTP provider. Authorization -//! checks are served from the in-memory cache when possible. On cache miss the provider -//! falls back to `isAuthorized(policyId, user)` via the L1 RPC and writes the result back -//! into the cache so subsequent lookups are instant. -//! -//! This mirrors the [`L1StateProvider`](crate::state::L1StateProvider) pattern used for -//! storage slot reads. - -use alloy_primitives::Address; -use alloy_provider::DynProvider; -use alloy_rpc_types_eth::BlockId; -use eyre::Result; -use tempo_alloy::TempoNetwork; -use tempo_contracts::precompiles::{ - ITIP20, ITIP403Registry, ITIP403Registry::PolicyType, TIP403_REGISTRY_ADDRESS, -}; -use tracing::{debug, info, warn}; - -use super::builtin_authorization; - -use super::{AuthRole, CompoundData, PolicyCache, metrics::Tip403Metrics}; - -/// Cache-first, RPC-fallback provider for TIP-403 policy authorization. -/// -/// Wraps a [`PolicyCache`] (populated by the [`L1Subscriber`](crate::l1::L1Subscriber)) -/// and an L1 HTTP provider. When the cache cannot resolve an authorization query (e.g. the -/// policy existed before the subscriber started), the provider falls back to L1 RPC calls and -/// caches the result for future lookups. -/// -/// # Sync dispatch safety -/// -/// [`is_authorized`](Self::is_authorized) calls `tokio::task::block_in_place` + -/// `runtime_handle.block_on(...)` to execute async RPC work from a blocking context. -/// This is safe when the caller runs on a blocking thread (e.g. the payload builder). -#[derive(Debug, Clone)] -pub struct PolicyProvider { - /// Shared in-memory policy cache, populated by the subscriber and RPC fallback. - cache: PolicyCache, - /// L1 HTTP provider for RPC fallback on cache miss. - provider: DynProvider, - /// Tokio runtime handle for `block_in_place` + `block_on` in sync call sites. - runtime_handle: tokio::runtime::Handle, - /// Metrics for cache hit/miss rates and RPC resolution latency. - metrics: Tip403Metrics, -} - -impl PolicyProvider { - /// Create a new provider from components. - pub fn new( - cache: PolicyCache, - provider: DynProvider, - runtime_handle: tokio::runtime::Handle, - ) -> Self { - Self { - cache, - provider, - runtime_handle, - metrics: Tip403Metrics::default(), - } - } - - /// Returns a reference to the underlying shared policy cache. - pub fn cache(&self) -> &PolicyCache { - &self.cache - } - - /// Returns a reference to the TIP-403 metrics. - pub fn metrics(&self) -> &Tip403Metrics { - &self.metrics - } - - /// Cache-first, RPC-fallback authorization check (sync). - /// - /// Intended for use inside the payload builder which runs on a blocking thread. - /// On cache miss, fetches policy data from L1 via RPC, caches it, and returns - /// the authorization result. - /// - /// # Panics - /// - /// Panics if called from within an async context on the same tokio runtime. - pub fn is_authorized( - &self, - token: Address, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Result { - self.metrics.authorization_checks_total.increment(1); - - // 1. Try cache first - if let Some(result) = self - .cache - .read() - .is_authorized(token, user, block_number, role) - { - self.metrics.cache_hits.increment(1); - return Ok(result); - } - - // 2. Cache miss — fetch from L1 via RPC - self.metrics.cache_misses.increment(1); - debug!( - %token, %user, block_number, ?role, - "Policy cache miss, fetching from L1 RPC" - ); - tokio::task::block_in_place(|| { - self.runtime_handle - .block_on(self.fetch_and_cache(token, user, block_number, role)) - }) - } - - /// Async version for non-blocking contexts. - pub async fn is_authorized_async( - &self, - token: Address, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Result { - self.metrics.authorization_checks_total.increment(1); - - if let Some(result) = self - .cache - .read() - .is_authorized(token, user, block_number, role) - { - self.metrics.cache_hits.increment(1); - return Ok(result); - } - - self.metrics.cache_misses.increment(1); - debug!( - %token, %user, block_number, ?role, - "Policy cache miss, fetching from L1 RPC (async)" - ); - self.fetch_and_cache(token, user, block_number, role).await - } - - /// Fetch authorization data from L1, cache it, and return the result. - async fn fetch_and_cache( - &self, - token: Address, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Result { - let start = std::time::Instant::now(); - - // Resolve the token's transferPolicyId - let policy_id = self.resolve_policy_id(token, block_number).await?; - - // Builtins — no RPC needed - if let Some(authorized) = builtin_authorization(policy_id) { - self.cache - .write() - .set_token_policy(token, block_number, policy_id); - self.metrics - .rpc_resolution_duration_seconds - .record(start.elapsed().as_secs_f64()); - return Ok(authorized); - } - - let result = self - .resolve_policy_authorization(policy_id, user, block_number, role) - .await; - - self.metrics - .rpc_resolution_duration_seconds - .record(start.elapsed().as_secs_f64()); - result - } - - /// Fetch authorization for a simple (whitelist/blacklist) policy. - /// - /// Calls `isAuthorized(policyId, user)` on L1, derives the raw policy-set boolean - /// from the result + policy type, and caches it in the [`PolicySet`](super::PolicySet). - async fn fetch_and_cache_simple( - &self, - policy_id: u64, - user: Address, - block_number: u64, - policy_type: PolicyType, - ) -> Result { - let authorized = self - .rpc_is_authorized(policy_id, user, block_number) - .await?; - - // Derive the raw policy-set value from the authorization result: - // - Whitelist: authorized == in_set - // - Blacklist: authorized == !in_set - let in_set = match policy_type { - PolicyType::WHITELIST => authorized, - PolicyType::BLACKLIST => !authorized, - _ => unreachable!(), - }; - - self.cache - .write() - .set_policy_status(policy_id, user, block_number, in_set); - - info!( - policy_id, %user, block_number, authorized, in_set, - "Cached policy set entry from L1 RPC" - ); - - Ok(authorized) - } - - /// Fetch authorization for a compound policy. - /// - /// Fetches the compound sub-policy structure from L1, resolves the relevant sub-policy - /// for the requested role, and recursively fetches/caches the sub-policy set state. - async fn fetch_and_cache_compound( - &self, - policy_id: u64, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Result { - let compound = self.resolve_compound_data(policy_id, block_number).await?; - - match role { - AuthRole::Sender => { - self.resolve_simple_sub_policy(compound.sender_policy_id, user, block_number) - .await - } - AuthRole::Recipient => { - self.resolve_simple_sub_policy(compound.recipient_policy_id, user, block_number) - .await - } - AuthRole::MintRecipient => { - self.resolve_simple_sub_policy( - compound.mint_recipient_policy_id, - user, - block_number, - ) - .await - } - AuthRole::Transfer => { - // Check both sender AND recipient — short-circuit on sender failure. - let sender_ok = self - .resolve_simple_sub_policy(compound.sender_policy_id, user, block_number) - .await?; - if !sender_ok { - return Ok(false); - } - self.resolve_simple_sub_policy(compound.recipient_policy_id, user, block_number) - .await - } - } - } - - /// Resolve authorization for a simple sub-policy (builtin or whitelist/blacklist). - async fn resolve_simple_sub_policy( - &self, - policy_id: u64, - user: Address, - block_number: u64, - ) -> Result { - // Builtins - if let Some(authorized) = builtin_authorization(policy_id) { - return Ok(authorized); - } - - // Check cache first for this sub-policy. - // If the policy type is known but the user was never observed by the - // subscriber, `check_simple` returns `None` and we fall through to RPC. - { - let cache = self.cache.read(); - if let Some(result) = cache.check_simple(policy_id, user, block_number) { - return Ok(result); - } - } - - // Cache miss — fetch from L1 - let policy_type = self.resolve_policy_type(policy_id, block_number).await?; - self.fetch_and_cache_simple(policy_id, user, block_number, policy_type) - .await - } - - /// Resolve the `transferPolicyId` for a token — cache first, RPC fallback (sync). - /// - /// Intended for the zone TIP-20 precompile which runs on a blocking thread. - /// - /// # Panics - /// - /// Panics if called from within an async context on the same tokio runtime. - pub fn resolve_transfer_policy_id(&self, token: Address) -> Result { - let block_number = self.cache.read().last_l1_block(); - if let Some(id) = self.cache.read().get_token_policy(token, block_number) { - return Ok(id); - } - tokio::task::block_in_place(|| { - self.runtime_handle - .block_on(self.resolve_policy_id(token, block_number)) - }) - } - - /// Resolve the `transferPolicyId` for a token — cache first, RPC fallback. - async fn resolve_policy_id(&self, token: Address, block_number: u64) -> Result { - if let Some(id) = self.cache.read().get_token_policy(token, block_number) { - return Ok(id); - } - - let tip20 = ITIP20::new(token, &self.provider); - let policy_id = tip20 - .transferPolicyId() - .block(BlockId::number(block_number)) - .call() - .await - .map_err(|e| eyre::eyre!("transferPolicyId RPC failed for token {token}: {e}"))?; - - self.cache - .write() - .set_token_policy(token, block_number, policy_id); - info!(%token, policy_id, block_number, "Cached token policy ID from L1 RPC"); - - Ok(policy_id) - } - - /// Resolve the policy type for a policy ID — cache first, RPC fallback. - async fn resolve_policy_type(&self, policy_id: u64, block_number: u64) -> Result { - // Check cache - if let Some(policy) = self.cache.read().policies().get(&policy_id) - && let Some(policy_type) = policy.policy_type - { - return Ok(policy_type); - } - - // Fetch from L1 - let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, &self.provider); - let result = registry - .policyData(policy_id) - .block(BlockId::number(block_number)) - .call() - .await - .map_err(|e| eyre::eyre!("policyData RPC failed for policy {policy_id}: {e}"))?; - - self.cache - .write() - .set_policy_type(policy_id, result.policyType); - info!(policy_id, policy_type = ?result.policyType, "Cached policy type from L1 RPC"); - - Ok(result.policyType) - } - - /// Resolve compound policy data — cache first, RPC fallback. - async fn resolve_compound_data( - &self, - policy_id: u64, - block_number: u64, - ) -> Result { - // Check cache - if let Some(policy) = self.cache.read().policies().get(&policy_id) - && let Some(ref compound) = policy.compound - { - return Ok(*compound); - } - - // Fetch from L1 - let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, &self.provider); - let result = registry - .compoundPolicyData(policy_id) - .block(BlockId::number(block_number)) - .call() - .await - .map_err(|e| { - eyre::eyre!("compoundPolicyData RPC failed for policy {policy_id}: {e}") - })?; - - let compound = CompoundData { - sender_policy_id: result.senderPolicyId, - recipient_policy_id: result.recipientPolicyId, - mint_recipient_policy_id: result.mintRecipientPolicyId, - }; - - self.cache.write().set_compound(policy_id, compound); - info!( - policy_id, - sender = compound.sender_policy_id, - recipient = compound.recipient_policy_id, - mint_recipient = compound.mint_recipient_policy_id, - "Cached compound policy data from L1 RPC" - ); - - Ok(compound) - } - - /// Cache-first, RPC-fallback authorization check by policy ID (no token resolution). - /// - /// Intended for the zone TIP-403 proxy precompile which receives `policyId` - /// directly. Queries cache at `last_l1_block` to ensure deterministic results - /// across nodes. On cache miss, falls back to L1 RPC (using `block_in_place`) - /// and populates the cache so subsequent lookups are instant. - /// - /// # Panics - /// - /// Panics if called from within an async context on the same tokio runtime. - pub fn is_authorized_by_policy( - &self, - policy_id: u64, - user: Address, - role: AuthRole, - ) -> Result { - self.metrics.authorization_checks_total.increment(1); - - // Builtins - if let Some(authorized) = builtin_authorization(policy_id) { - self.metrics.cache_hits.increment(1); - return Ok(authorized); - } - - // Try cache first — use last_l1_block to avoid leaking subscriber-ahead state - let block_number = self.cache.read().last_l1_block(); - if let Some(result) = self - .cache - .read() - .check_policy(policy_id, user, block_number, role) - { - self.metrics.cache_hits.increment(1); - return Ok(result); - } - - // Cache miss — fall back to L1 RPC - self.metrics.cache_misses.increment(1); - debug!( - policy_id, %user, ?role, block_number, - "Policy proxy cache miss, fetching from L1 RPC" - ); - tokio::task::block_in_place(|| { - self.runtime_handle.block_on(self.fetch_and_cache_by_policy( - policy_id, - user, - block_number, - role, - )) - }) - } - - /// Fetch and cache authorization data for a known policy ID (async). - /// - /// Like [`fetch_and_cache`](Self::fetch_and_cache) but skips the token → - /// policy ID resolution step since the caller already has the policy ID. - async fn fetch_and_cache_by_policy( - &self, - policy_id: u64, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Result { - let start = std::time::Instant::now(); - - let result = self - .resolve_policy_authorization(policy_id, user, block_number, role) - .await; - - self.metrics - .rpc_resolution_duration_seconds - .record(start.elapsed().as_secs_f64()); - result - } - - /// Resolve authorization for a policy ID by fetching policy type and set state - /// from L1, caching the results. - /// - /// Shared implementation used by both [`fetch_and_cache`](Self::fetch_and_cache) - /// and [`fetch_and_cache_by_policy`](Self::fetch_and_cache_by_policy). - async fn resolve_policy_authorization( - &self, - policy_id: u64, - user: Address, - block_number: u64, - role: AuthRole, - ) -> Result { - let policy_type = self.resolve_policy_type(policy_id, block_number).await?; - - match policy_type { - PolicyType::WHITELIST | PolicyType::BLACKLIST => { - self.fetch_and_cache_simple(policy_id, user, block_number, policy_type) - .await - } - PolicyType::COMPOUND => { - self.fetch_and_cache_compound(policy_id, user, block_number, role) - .await - } - _ => eyre::bail!("unknown policy type for policy {policy_id}"), - } - } - - /// Cache-first, RPC-fallback policy type resolution (sync). - /// - /// Falls back to L1 RPC via `block_in_place` on cache miss. - /// - /// # Panics - /// - /// Panics if called from within an async context on the same tokio runtime. - pub fn resolve_policy_type_sync(&self, policy_id: u64) -> Result { - if let Some(policy) = self.cache.read().policies().get(&policy_id) - && let Some(policy_type) = policy.policy_type - { - return Ok(policy_type); - } - - let block_number = self.cache.read().last_l1_block(); - debug!( - policy_id, - block_number, "Policy type cache miss, fetching from L1 RPC" - ); - tokio::task::block_in_place(|| { - self.runtime_handle - .block_on(self.resolve_policy_type(policy_id, block_number)) - }) - } - - /// Cache-first, RPC-fallback compound data resolution (sync). - /// - /// Falls back to L1 RPC via `block_in_place` on cache miss. - /// - /// # Panics - /// - /// Panics if called from within an async context on the same tokio runtime. - pub fn resolve_compound_data_sync(&self, policy_id: u64) -> Result { - if let Some(policy) = self.cache.read().policies().get(&policy_id) - && let Some(ref compound) = policy.compound - { - return Ok(*compound); - } - - let block_number = self.cache.read().last_l1_block(); - debug!( - policy_id, - block_number, "Compound data cache miss, fetching from L1 RPC" - ); - tokio::task::block_in_place(|| { - self.runtime_handle - .block_on(self.resolve_compound_data(policy_id, block_number)) - }) - } - - /// Cache-first, RPC-fallback policy existence check (sync). - /// - /// Returns `Ok(true)` if the policy exists, `Ok(false)` if it doesn't, - /// and propagates RPC errors instead of collapsing them into `false`. - /// - /// # Panics - /// - /// Panics if called from within an async context on the same tokio runtime. - pub fn policy_exists_sync(&self, policy_id: u64) -> Result { - if builtin_authorization(policy_id).is_some() { - return Ok(true); - } - - // Cache hit: if we have a policy record with a known type, it exists. - if let Some(policy) = self.cache.read().policies().get(&policy_id) - && policy.policy_type.is_some() - { - return Ok(true); - } - - let block_number = self.cache.read().last_l1_block(); - debug!( - policy_id, - block_number, "Policy exists cache miss, fetching from L1 RPC" - ); - let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, &self.provider); - tokio::task::block_in_place(|| { - self.runtime_handle.block_on(async { - registry - .policyExists(policy_id) - .block(BlockId::number(block_number)) - .call() - .await - .map_err(|e| { - self.metrics.rpc_errors.increment(1); - warn!(policy_id, block_number, %e, "policyExists RPC failed"); - eyre::eyre!("policyExists RPC failed for policy {policy_id}: {e}") - }) - }) - }) - } - - /// Call `isAuthorized(policyId, user)` on the TIP403Registry via L1 RPC. - async fn rpc_is_authorized( - &self, - policy_id: u64, - user: Address, - block_number: u64, - ) -> Result { - let registry = ITIP403Registry::new(TIP403_REGISTRY_ADDRESS, &self.provider); - let authorized = registry - .isAuthorized(policy_id, user) - .block(BlockId::number(block_number)) - .call() - .await - .map_err(|e| { - self.metrics.rpc_errors.increment(1); - warn!(policy_id, %user, block_number, %e, "isAuthorized RPC failed"); - eyre::eyre!("isAuthorized RPC failed for policy {policy_id} user {user}: {e}") - })?; - - Ok(authorized) - } -} diff --git a/crates/l1/src/state/tip403/task.rs b/crates/l1/src/state/tip403/task.rs deleted file mode 100644 index e46ca8c9e..000000000 --- a/crates/l1/src/state/tip403/task.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Background resolution task for pre-fetching TIP-403 authorization data from L1. -//! -//! The [`PolicyResolutionTask`] receives [`PolicyTaskMessage`]s via a channel and resolves -//! them concurrently using [`FuturesUnordered`]. Each request triggers a -//! [`PolicyProvider::is_authorized_async`] call which will populate the -//! [`PolicyCache`] on cache miss, ensuring the payload builder hits the cache -//! at block-building time. -//! -//! Typical producers are the transaction pool (mempool) validation layer, which can -//! submit pre-fetch requests for sender/recipient addresses as transactions arrive. - -use alloy_primitives::Address; -use alloy_provider::DynProvider; -use futures::{FutureExt, StreamExt, stream::FuturesUnordered}; -use tempo_alloy::TempoNetwork; -use tokio::sync::mpsc; -use tracing::{debug, warn}; - -use super::{AuthRole, PolicyCache, metrics::Tip403Metrics}; -use crate::state::PolicyProvider; - -/// Background task that processes [`PolicyTaskMessage`]s concurrently. -/// -/// Drains messages from the channel and resolves them via [`PolicyProvider::is_authorized_async`], -/// populating the shared cache. Results are logged but not returned — the sole purpose -/// is cache warming so the synchronous payload builder path avoids RPC round-trips. -pub struct PolicyResolutionTask { - /// Cache-first, RPC-fallback provider used to resolve authorization queries. - provider: PolicyProvider, - /// Receiver for pre-fetch requests from the mempool / tx validation layer. - rx: mpsc::Receiver, - /// Maximum number of concurrent in-flight RPC resolutions. - max_concurrent: usize, - /// Concurrent resolution futures currently being polled. - in_flight: FuturesUnordered>, - /// Metrics for tracking pre-fetch activity. - metrics: Tip403Metrics, -} - -impl PolicyResolutionTask { - /// Run the resolution loop until the channel is closed. - /// - /// Processes requests concurrently up to `max_concurrent` in-flight futures. - /// Errors are logged and swallowed — a failed resolution simply means the cache - /// remains unpopulated and the builder will fall back to its own RPC path. - pub async fn run(mut self) { - loop { - tokio::select! { - // Accept new messages when below the concurrency limit. - Some(msg) = self.rx.recv(), if self.in_flight.len() < self.max_concurrent => { - self.on_message(msg); - self.metrics.prefetch_in_flight.set(self.in_flight.len() as f64); - } - // Drain completed futures. - Some(()) = self.in_flight.next() => { - self.metrics.prefetch_in_flight.set(self.in_flight.len() as f64); - } - // Channel closed and all in-flight work drained. - else => break, - } - } - - debug!("Policy resolution task shutting down"); - } - - /// Dispatch a message to the appropriate handler. - fn on_message(&mut self, msg: PolicyTaskMessage) { - match msg { - PolicyTaskMessage::ResolveAuthorization { token, user, role } => { - self.metrics.prefetch_requests_total.increment(1); - let provider = self.provider.clone(); - let block_number = provider.cache().read().last_l1_block(); - let metrics = self.metrics.clone(); - self.in_flight.push( - async move { - match provider - .is_authorized_async(token, user, block_number, role) - .await - { - Ok(authorized) => { - metrics.prefetch_successes.increment(1); - debug!( - %token, %user, block_number, - ?role, authorized, - "Pre-fetched policy authorization" - ); - } - Err(e) => { - metrics.prefetch_failures.increment(1); - warn!( - %token, %user, block_number, - ?role, error = %e, - "Policy pre-fetch failed" - ); - } - } - } - .boxed(), - ); - } - } - } -} - -/// Message sent to the [`PolicyResolutionTask`]. -#[derive(Debug, Clone)] -pub enum PolicyTaskMessage { - /// Pre-fetch and cache authorization data for a (token, user) pair. - /// - /// The resolution task queries L1 at the cache's last engine-processed L1 block; - /// callers don't need to specify one. - ResolveAuthorization { - /// The TIP-20 token whose transfer policy to check. - token: Address, - /// The address to check authorization for. - user: Address, - /// Which role to resolve (sender, recipient, mint recipient, or full transfer). - role: AuthRole, - }, -} - -/// Handle for sending pre-fetch requests to the [`PolicyResolutionTask`]. -/// -/// Wraps an `mpsc::Sender` so callers don't need to -/// construct messages manually or carry around the raw channel sender. -#[derive(Debug, Clone)] -pub struct PolicyTaskHandle { - tx: mpsc::Sender, -} - -impl PolicyTaskHandle { - /// Create a new handle from the sending half of the task channel. - pub fn new(tx: mpsc::Sender) -> Self { - Self { tx } - } - - /// Request pre-fetch of authorization data for a (token, user) pair. - /// - /// The resolution task will query L1 at the cache's last engine-processed L1 block. - /// Returns `Ok(())` if the message was queued, or `Err` if the - /// resolution task has shut down. - pub async fn resolve_authorization( - &self, - token: Address, - user: Address, - role: AuthRole, - ) -> Result<(), mpsc::error::SendError> { - self.tx - .send(PolicyTaskMessage::ResolveAuthorization { token, user, role }) - .await - } - - /// Non-blocking version of [`resolve_authorization`](Self::resolve_authorization). - /// - /// Returns `Err` if the channel is full or the task has shut down. - pub fn send_resolve_policy( - &self, - token: Address, - user: Address, - role: AuthRole, - ) -> Result<(), mpsc::error::TrySendError> { - self.tx - .try_send(PolicyTaskMessage::ResolveAuthorization { token, user, role }) - } -} - -/// Spawn the policy resolution task as a critical background task. -/// -/// Returns a [`PolicyTaskHandle`] for sending pre-fetch requests. -pub fn spawn_policy_resolution_task( - cache: PolicyCache, - l1_provider: DynProvider, - max_concurrent: usize, - channel_capacity: usize, - task_executor: reth_tasks::Runtime, -) -> PolicyTaskHandle { - let (tx, rx) = mpsc::channel(channel_capacity); - let handle = PolicyTaskHandle::new(tx); - - task_executor.spawn_critical_task( - "l1-policy-resolution", - Box::pin(async move { - let task = PolicyResolutionTask { - provider: PolicyProvider::new( - cache, - l1_provider, - tokio::runtime::Handle::current(), - ), - rx, - max_concurrent, - in_flight: FuturesUnordered::new(), - metrics: Tip403Metrics::default(), - }; - task.run().await; - }), - ); - - handle -} diff --git a/crates/l1/src/state/versioned.rs b/crates/l1/src/state/versioned.rs deleted file mode 100644 index affab4f53..000000000 --- a/crates/l1/src/state/versioned.rs +++ /dev/null @@ -1,284 +0,0 @@ -//! Block-versioned value primitive for L1 state tracking. -//! -//! [`HeightVersioned`] tracks a value that changes across L1 block heights with a two-tier -//! model: -//! -//! - **Baseline** — a single `Option` representing the merged value at the zone's last -//! processed block. All lookups at or below the baseline height return this value. -//! -//! - **Pending** — a `BTreeMap` of per-block changes ahead of the zone. Lookups above -//! the baseline height check pending entries with "latest at or before" semantics, falling -//! back to the baseline. -//! -//! When the zone advances, [`advance`](HeightVersioned::advance) folds all pending entries up -//! to the new height into the baseline, keeping the pending map small. - -use std::collections::BTreeMap; - -/// A value that tracks changes across L1 block heights with automatic compaction. -/// -/// The baseline represents the finalized value at or below `baseline_height`. Pending entries -/// track future changes that the zone hasn't processed yet. As the zone advances, -/// [`advance`](Self::advance) folds pending entries into the baseline. -#[derive(Debug, Clone)] -pub struct HeightVersioned { - /// The merged value at `baseline_height`. `None` if no value has ever been set. - baseline: Option, - /// The block height up to which the baseline is valid. - baseline_height: u64, - /// Per-block changes above `baseline_height`. - pending: BTreeMap, -} - -impl Default for HeightVersioned { - fn default() -> Self { - Self { - baseline: None, - baseline_height: 0, - pending: BTreeMap::new(), - } - } -} - -impl HeightVersioned { - /// Returns the value at the given block height. - /// - /// - At or below `baseline_height`: returns the baseline. - /// - Above `baseline_height`: returns the latest pending entry at or before `block_number`, - /// falling back to the baseline. - pub fn get(&self, block_number: u64) -> Option { - if block_number <= self.baseline_height { - return self.baseline; - } - - // Check pending for latest entry at or before block_number - self.pending - .range(..=block_number) - .next_back() - .map(|(_, v)| *v) - .or(self.baseline) - } - - /// Records a value at the given block height. - /// - /// Values at or below the baseline height are ignored. The baseline represents finalized - /// engine-consumed state and is only updated by [`advance`](Self::advance), which prevents - /// delayed RPC fallback results from overwriting newer event-derived state. - pub fn set(&mut self, block_number: u64, value: V) { - if block_number <= self.baseline_height { - return; - } - - self.pending.insert(block_number, value); - } - - /// Advance the baseline to `new_height`, folding all pending entries up to that height. - /// - /// After this call, `baseline_height == new_height` and any pending entries ≤ `new_height` - /// have been merged into the baseline. Does nothing if `new_height` is at or below the - /// current baseline. - pub fn advance(&mut self, new_height: u64) { - if new_height <= self.baseline_height { - return; - } - - // Find the latest pending entry at or before new_height — that becomes the new baseline - let folded = self - .pending - .range(..=new_height) - .next_back() - .map(|(_, v)| *v); - - if let Some(value) = folded { - self.baseline = Some(value); - } - - // Remove all pending entries up to and including new_height - let to_remove: Vec = self.pending.range(..=new_height).map(|(k, _)| *k).collect(); - for k in to_remove { - self.pending.remove(&k); - } - - self.baseline_height = new_height; - } - - /// Returns `true` if no value has ever been recorded. - pub fn is_empty(&self) -> bool { - self.baseline.is_none() && self.pending.is_empty() - } - - /// Removes all recorded values and resets the baseline height. - pub fn clear(&mut self) { - self.baseline = None; - self.baseline_height = 0; - self.pending.clear(); - } - - /// Collapse all history before `min_block` into the baseline. - /// - /// Equivalent to [`advance`](Self::advance). Provided for API compatibility with callers - /// that don't track zone progress explicitly. - pub fn flatten(&mut self, min_block: u64) { - self.advance(min_block); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn empty_returns_none() { - let v = HeightVersioned::::default(); - assert_eq!(v.get(100), None); - } - - #[test] - fn get_at_exact_block() { - let mut v = HeightVersioned::default(); - v.set(10, 42u64); - assert_eq!(v.get(10), Some(42)); - } - - #[test] - fn get_returns_latest_before() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - v.set(20, 2); - assert_eq!(v.get(15), Some(1)); - assert_eq!(v.get(20), Some(2)); - assert_eq!(v.get(25), Some(2)); - } - - #[test] - fn get_before_earliest_returns_none() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - assert_eq!(v.get(9), None); - } - - #[test] - fn advance_folds_into_baseline() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - v.set(20, 2); - v.set(30, 3); - - v.advance(20); - - // Baseline is now 2 at height 20 - assert_eq!(v.get(10), Some(2)); // at-or-below baseline → baseline value - assert_eq!(v.get(20), Some(2)); - assert_eq!(v.get(25), Some(2)); // between baseline and next pending - assert_eq!(v.get(30), Some(3)); // pending still works - } - - #[test] - fn advance_preserves_baseline_when_no_pending() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - - v.advance(15); - - // No pending at 15, but baseline was set at 10 - assert_eq!(v.get(15), Some(1)); - assert_eq!(v.get(20), Some(1)); - } - - #[test] - fn advance_below_current_is_noop() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - v.advance(20); - v.advance(15); // should not regress - - assert_eq!(v.get(20), Some(1)); - } - - #[test] - fn set_at_or_below_baseline_is_ignored() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - v.advance(20); - - // Delayed writes from finalized heights must not rewrite the baseline. - v.set(15, 99); - v.set(20, 100); - assert_eq!(v.get(20), Some(1)); - - v.set(21, 2); - assert_eq!(v.get(21), Some(2)); - } - - #[test] - fn set_at_initial_baseline_is_ignored() { - let mut v = HeightVersioned::default(); - - v.set(0, 1u64); - - assert_eq!(v.get(0), None); - assert_eq!(v.get(10), None); - - v.set(1, 1); - assert_eq!(v.get(1), Some(1)); - } - - #[test] - fn flatten_is_advance() { - let mut v = HeightVersioned::default(); - v.set(5, 1u64); - v.set(10, 2); - v.set(20, 3); - - v.flatten(15); - - assert_eq!(v.get(5), Some(2)); - assert_eq!(v.get(10), Some(2)); - assert_eq!(v.get(15), Some(2)); - assert_eq!(v.get(20), Some(3)); - } - - #[test] - fn clear_resets_everything() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - v.advance(10); - v.set(20, 2); - - v.clear(); - - assert!(v.is_empty()); - assert_eq!(v.get(10), None); - assert_eq!(v.get(20), None); - } - - #[test] - fn pending_falls_back_to_baseline() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - v.advance(10); - - // No pending entries, but baseline exists - assert_eq!(v.get(15), Some(1)); - assert_eq!(v.get(100), Some(1)); - } - - #[test] - fn multiple_advances() { - let mut v = HeightVersioned::default(); - v.set(10, 1u64); - v.set(20, 2); - v.set(30, 3); - v.set(40, 4); - - v.advance(15); - assert_eq!(v.get(15), Some(1)); - - v.advance(25); - assert_eq!(v.get(25), Some(2)); - - v.advance(35); - assert_eq!(v.get(35), Some(3)); - assert_eq!(v.get(40), Some(4)); - } -} diff --git a/crates/l1/src/subscriber.rs b/crates/l1/src/subscriber.rs index d34ff35b3..edebbb82a 100644 --- a/crates/l1/src/subscriber.rs +++ b/crates/l1/src/subscriber.rs @@ -14,10 +14,6 @@ pub struct L1SubscriberConfig { /// the portal's on-chain `genesisTempoBlockNumber` (which may be 0 for /// portals not created via ZoneFactory). pub genesis_tempo_block_number: Option, - /// Shared TIP-403 policy cache. The subscriber applies policy events - /// extracted from L1 receipts directly into this cache before enqueuing - /// blocks. - pub policy_cache: crate::state::tip403::PolicyCache, /// Shared L1 state cache. The subscriber updates the cache anchor on each /// confirmed block and clears it on reorgs. pub l1_state_cache: crate::state::cache::L1StateCache, @@ -52,11 +48,6 @@ pub struct L1Subscriber { pub(crate) config: L1SubscriberConfig, pub(crate) local_state: Arc, pub(crate) deposit_queue: DepositQueue, - /// Mutable set of token addresses tracked for TIP-403 policy events. - /// Initialized from config, grows dynamically when `TokenEnabled` events are seen. - pub(crate) tracked_tokens: Vec
, - /// TIP-403 metrics (cache sizes, events applied). - pub(crate) tip403_metrics: crate::state::tip403::Tip403Metrics, /// L1 subscriber metrics for connection health, backfill, and event ingestion. pub(crate) subscriber_metrics: crate::metrics::L1SubscriberMetrics, } @@ -75,15 +66,12 @@ impl L1Subscriber { ) where P: StateProviderFactory + Clone + Send + Sync + 'static, { - let tracked_tokens = config.policy_cache.read().tracked_tokens(); let subscriber = Self { config, local_state: Arc::new(ProviderLocalTempoCheckpointReader { provider: local_state_provider, }), deposit_queue, - tracked_tokens, - tip403_metrics: Default::default(), subscriber_metrics: Default::default(), }; @@ -91,7 +79,7 @@ impl L1Subscriber { "l1-deposit-subscriber", Box::pin(async move { loop { - if let Err(e) = subscriber.clone().run().await { + if let Err(e) = subscriber.run().await { let retry_interval = subscriber.config.retry_connection_interval; subscriber.subscriber_metrics.reconnects.increment(1); error!( @@ -280,10 +268,7 @@ impl L1Subscriber { /// Backfill deposit events from the starting block to the current L1 tip. #[instrument(skip(self, l1_provider))] - async fn sync_to_l1_tip( - &mut self, - l1_provider: &impl Provider, - ) -> eyre::Result<()> { + async fn sync_to_l1_tip(&self, l1_provider: &impl Provider) -> eyre::Result<()> { let Some(mut from) = self.resolve_start_block(l1_provider).await? else { self.subscriber_metrics.current_l1_lag_blocks.set(0.0); return Ok(()); @@ -331,12 +316,12 @@ impl L1Subscriber { /// Backfill L1 blocks from `from..=to` with pipelined RPC fetching. /// /// Fetches headers and receipts for up to `l1_fetch_concurrency` blocks in - /// parallel, then processes them sequentially (event extraction, policy - /// application, enqueue). Receipts are fetched by the corresponding block + /// parallel, then processes them sequentially (event extraction and enqueue). + /// Receipts are fetched by the corresponding block /// hash and validated against the header's receipts root before processing. #[instrument(skip(self, l1_provider), fields(from, to))] async fn backfill( - &mut self, + &self, l1_provider: &impl Provider, from: u64, to: u64, @@ -398,15 +383,13 @@ impl L1Subscriber { while let Some((header, receipts)) = fetched.try_next().await? { let block_number = header.number(); - let (events, policy_events) = self.extract_events(block_number, &receipts); + let portal_events = self.extract_events(block_number, &receipts); self.record_seen_block(block_number, to.saturating_sub(block_number)); let sealed = SealedHeader::seal_slow(header); self.update_l1_state_anchor(block_number, sealed.hash(), sealed.parent_hash()); - self.apply_policy_events(block_number, &policy_events); - self.apply_portal_state_events(block_number, &events); - self.deposit_queue - .enqueue_sealed(sealed, events, policy_events); + self.apply_portal_state_events(block_number, &portal_events); + self.deposit_queue.enqueue_sealed(sealed, portal_events); enqueued += 1; self.subscriber_metrics.blocks_enqueued.increment(1); @@ -448,11 +431,7 @@ impl L1Subscriber { /// prevents the zone from committing to an L1 tip that gets reorged away. /// /// Callers should retry on error (see [`Self::spawn`]). - pub async fn run(mut self) -> eyre::Result<()> { - // Re-read tracked tokens from the policy cache so we pick up any - // tokens discovered during a previous run (before a reconnect). - self.tracked_tokens = self.config.policy_cache.read().tracked_tokens(); - + pub async fn run(&self) -> eyre::Result<()> { let provider = self.connect().await?; // Backfill to the current tip before subscribing. @@ -466,11 +445,7 @@ impl L1Subscriber { // A block is only flushed to the deposit queue once the NEXT block // arrives with a matching parent hash, proving the buffered block // is on the canonical chain. - let mut unconfirmed_tip: Option<( - SealedHeader, - L1PortalEvents, - Vec, - )> = None; + let mut unconfirmed_tip: Option<(SealedHeader, L1PortalEvents)> = None; loop { let stream_wait_start = std::time::Instant::now(); @@ -483,24 +458,22 @@ impl L1Subscriber { }; let block_number = header.number(); let sealed = SealedHeader::seal_slow(header.inner.into_consensus()); - let (events, policy_events) = self.extract_events(block_number, &receipts); + let events = self.extract_events(block_number, &receipts); self.record_seen_block(block_number, 0); // If we have a buffered tip, check if the new block confirms it. - if let Some((tip_header, tip_events, tip_policy_events)) = unconfirmed_tip.take() { + if let Some((tip_header, portal_events)) = unconfirmed_tip.take() { if sealed.parent_hash() == tip_header.hash() { // Confirmed — update the L1 state anchor, apply events, and // flush to the queue. let tip_number = tip_header.number(); - let tip_hash = tip_header.hash(); - let tip_parent = tip_header.parent_hash(); - self.update_l1_state_anchor(tip_number, tip_hash, tip_parent); - self.apply_policy_events(tip_number, &tip_policy_events); - self.apply_portal_state_events(tip_number, &tip_events); - match self - .deposit_queue - .try_enqueue(tip_header, tip_events, tip_policy_events) - { + self.update_l1_state_anchor( + tip_number, + tip_header.hash(), + tip_header.parent_hash(), + ); + self.apply_portal_state_events(tip_number, &portal_events); + match self.deposit_queue.try_enqueue(tip_header, portal_events) { EnqueueOutcome::Accepted => { self.subscriber_metrics.blocks_enqueued.increment(1); } @@ -519,8 +492,7 @@ impl L1Subscriber { } } } else { - // Reorg — discard the buffered tip and clear L1 state and - // policy caches. + // Reorg — discard the buffered tip and clear raw L1 state. self.subscriber_metrics.reorgs_detected.increment(1); warn!( discarded_block = tip_header.number(), @@ -530,61 +502,38 @@ impl L1Subscriber { "Discarding unconfirmed L1 block (reorg)" ); self.config.l1_state_cache.write().clear(); - self.config.policy_cache.write().clear(); } } // Buffer the new block as unconfirmed tip. - unconfirmed_tip = Some((sealed, events, policy_events)); + unconfirmed_tip = Some((sealed, events)); } warn!("L1 block subscription stream ended"); Ok(()) } - /// Extract portal and policy events from pre-fetched receipts (no RPC). + /// Extract portal events from pre-fetched receipts (no RPC). fn extract_events( - &mut self, + &self, block_number: u64, receipts: &[tempo_alloy::rpc::TempoTransactionReceipt], - ) -> (L1PortalEvents, Vec) { - use tempo_contracts::precompiles::{ITIP20::TransferPolicyUpdate, TIP403_REGISTRY_ADDRESS}; - + ) -> L1PortalEvents { let portal_address = self.config.portal_address; let mut portal_events = L1PortalEvents::default(); - let mut policy_events = Vec::new(); for receipt in receipts { for log in receipt.logs() { - let addr = log.address(); - - if addr == portal_address { - let prev_len = portal_events.enabled_tokens.len(); - if let Err(e) = portal_events.push_log(log, block_number) { - warn!(block_number, %e, "Failed to decode portal event from receipt"); - } - if let Some(enabled) = portal_events.enabled_tokens.get(prev_len) { - let token = enabled.token; - if !self.tracked_tokens.contains(&token) { - info!(%token, "New token enabled, adding to tracked tokens"); - self.tracked_tokens.push(token); - } - } - } else if addr == TIP403_REGISTRY_ADDRESS { - if let Some(event) = PolicyEvent::decode_registry(log) { - policy_events.push(event); - } - } else if self.tracked_tokens.contains(&addr) - && log.topics().first() == Some(&TransferPolicyUpdate::SIGNATURE_HASH) - && let Some(event) = PolicyEvent::decode_tip20(log) + if log.address() == portal_address + && let Err(e) = portal_events.push_log(log, block_number) { - policy_events.push(event); + warn!(block_number, %e, "Failed to decode portal event from receipt"); } } } self.record_portal_event_metrics(&portal_events); - (portal_events, policy_events) + portal_events } fn record_seen_block(&self, block_number: u64, lag_blocks: u64) { @@ -640,27 +589,6 @@ impl L1Subscriber { } } - /// Write decoded policy events into the shared cache. - /// - /// The subscriber may write events ahead of the engine's processed L1 height; - /// the engine advances the cache baseline after it accepts the corresponding - /// zone payload. - pub(crate) fn apply_policy_events(&self, block_number: u64, policy_events: &[PolicyEvent]) { - let mut cache = self.config.policy_cache.write(); - cache.apply_events(block_number, policy_events); - if !policy_events.is_empty() { - self.tip403_metrics - .listener_events_applied - .increment(policy_events.len() as u64); - } - self.tip403_metrics - .cached_policies - .set(cache.policies().len() as f64); - self.tip403_metrics - .cached_token_policies - .set(cache.num_token_policies() as f64); - } - /// Write decoded portal state changes into the shared L1 cache at the /// confirmed block height. fn apply_portal_state_events(&self, block_number: u64, portal_events: &L1PortalEvents) { @@ -692,7 +620,6 @@ impl L1Subscriber { "Reorg detected, clearing L1 state cache" ); guard.clear(); - self.config.policy_cache.write().clear(); } guard.update_anchor(NumHash::new(number, hash)); } diff --git a/crates/l1/src/tests.rs b/crates/l1/src/tests.rs index 079e52c0c..5f464cda1 100644 --- a/crates/l1/src/tests.rs +++ b/crates/l1/src/tests.rs @@ -147,15 +147,12 @@ fn test_subscriber( l1_rpc_url: "http://127.0.0.1:8545".to_owned(), portal_address, genesis_tempo_block_number, - policy_cache: crate::PolicyCache::default(), l1_state_cache: crate::L1StateCache::new(HashSet::from([portal_address])), l1_fetch_concurrency: 1, retry_connection_interval: Duration::from_secs(1), }, local_state, deposit_queue: DepositQueue::default(), - tracked_tokens: vec![], - tip403_metrics: Default::default(), subscriber_metrics: Default::default(), } } @@ -399,55 +396,33 @@ fn assert_tempo_header_rejected(input: &[u8]) { } #[test] -fn update_l1_state_anchor_reorg_clears_stale_policy_state() { - use crate::state::tip403::AuthRole; - use tempo_contracts::precompiles::ITIP403Registry::PolicyType; - +fn update_l1_state_anchor_reorg_clears_raw_state() { let subscriber = test_subscriber( Arc::new(SequenceLocalTempoCheckpointReader::new([0])), Some(0), ); let token = address!("0x0000000000000000000000000000000000000011"); - let user = address!("0x0000000000000000000000000000000000000022"); let old_header = make_test_header(10); - let old_hash = header_hash(&old_header); - subscriber.update_l1_state_anchor(10, old_hash, old_header.inner.parent_hash); - { - let mut cache = subscriber.config.policy_cache.write(); - cache.set_token_policy(token, 10, 2); - cache.set_policy_type(2, PolicyType::WHITELIST); - cache.set_policy_status(2, user, 10, true); - cache.advance(10); - } + subscriber.update_l1_state_anchor(10, header_hash(&old_header), old_header.inner.parent_hash); + subscriber.config.l1_state_cache.write().set( + token, + B256::with_last_byte(1), + 10, + B256::with_last_byte(0xaa), + ); let replacement_parent = B256::with_last_byte(0x44); let replacement_header = make_chained_header(11, replacement_parent); subscriber.update_l1_state_anchor(11, header_hash(&replacement_header), replacement_parent); - subscriber.apply_policy_events( - 11, - &[ - PolicyEvent::TokenPolicyChanged { - token, - policy_id: 3, - }, - PolicyEvent::PolicyCreated { - policy_id: 3, - policy_type: PolicyType::WHITELIST, - }, - PolicyEvent::MembershipChanged { - policy_id: 3, - account: user, - in_set: false, - }, - ], - ); - - let cache = subscriber.config.policy_cache.read(); - assert!(cache.policies().get(&2).is_none()); assert_eq!( - cache.is_authorized(token, user, 11, AuthRole::Transfer), - Some(false) + subscriber + .config + .l1_state_cache + .read() + .get(token, B256::with_last_byte(1), 10), + None, + "reorg must clear raw L1 state" ); } @@ -727,18 +702,13 @@ fn test_deposit_queue_hash_chain() { queue.enqueue( make_test_header(1), L1PortalEvents::from_deposits(vec![d1.clone()]), - vec![], ); let hash_after_d1 = queue.enqueued_head_hash(); assert_ne!(hash_after_d1, B256::ZERO); // Verify hash is deterministic let mut queue2 = PendingDeposits::default(); - queue2.enqueue( - make_test_header(1), - L1PortalEvents::from_deposits(vec![d1]), - vec![], - ); + queue2.enqueue(make_test_header(1), L1PortalEvents::from_deposits(vec![d1])); assert_eq!(hash_after_d1, queue2.enqueued_head_hash); let d2 = L1Deposit::Regular(Deposit { @@ -751,11 +721,7 @@ fn test_deposit_queue_hash_chain() { memo: B256::ZERO, }); - queue.enqueue( - make_test_header(2), - L1PortalEvents::from_deposits(vec![d2]), - vec![], - ); + queue.enqueue(make_test_header(2), L1PortalEvents::from_deposits(vec![d2])); let hash_after_d2 = queue.enqueued_head_hash(); assert_ne!(hash_after_d2, hash_after_d1); } @@ -817,7 +783,6 @@ fn test_queue_and_process_deposits_hashes_match() { queue.enqueue( make_test_header(1), L1PortalEvents::from_deposits(deposits.clone()), - vec![], ); let transition = PendingDeposits::transition(B256::ZERO, &deposits); @@ -851,11 +816,10 @@ fn test_drain_returns_block_grouped_deposits() { let h10 = make_test_header(10); let h10_hash = header_hash(&h10); - queue.enqueue(h10, L1PortalEvents::from_deposits(vec![d1]), vec![]); + queue.enqueue(h10, L1PortalEvents::from_deposits(vec![d1])); queue.enqueue( make_chained_header(11, h10_hash), L1PortalEvents::from_deposits(vec![d2]), - vec![], ); let blocks = queue.drain(); @@ -1020,11 +984,7 @@ fn test_enqueue_and_transition_consistency() { // Path 1: enqueue into PendingDeposits let mut pending = PendingDeposits::default(); let header = make_test_header(1); - pending.enqueue( - header, - L1PortalEvents::from_deposits(deposits.clone()), - vec![], - ); + pending.enqueue(header, L1PortalEvents::from_deposits(deposits.clone())); // Path 2: compute transition directly let transition = PendingDeposits::transition(B256::ZERO, &deposits); @@ -1036,13 +996,12 @@ fn test_enqueue_and_transition_consistency() { } #[tokio::test] -async fn test_prepare_rejects_unauthorized_encrypted_deposit_without_decryption_data() { +async fn test_prepare_decrypted_deposit_defers_policy_to_upstream_mint() { use k256::{AffinePoint, ProjectivePoint, Scalar}; - use tempo_contracts::precompiles::ITIP403Registry::PolicyType; let token = address!("0x0000000000000000000000000000000000001000"); let sender = address!("0x0000000000000000000000000000000000001234"); - let unauthorized_recipient = address!("0x000000000000000000000000000000000000BEEF"); + let recipient = address!("0x000000000000000000000000000000000000BEEF"); let portal = address!("0x0000000000000000000000000000000000000ABC"); let block_number = 10; @@ -1054,26 +1013,13 @@ async fn test_prepare_rejects_unauthorized_encrypted_deposit_without_decryption_ let encrypted = crate::precompiles::ecies::encrypt_deposit( &seq_pub_x, seq_pub_y_parity, - unauthorized_recipient, + recipient, B256::ZERO, portal, U256::ZERO, ) .expect("encrypted deposit should be valid"); - let policy_cache = crate::PolicyCache::default(); - { - let mut cache = policy_cache.write(); - cache.set_token_policy(token, block_number, 2); - cache.set_policy_type(2, PolicyType::BLACKLIST); - cache.set_policy_status(2, unauthorized_recipient, block_number, true); - } - let provider = ProviderBuilder::new_with_network::() - .connect_mocked_client(Asserter::new()) - .erased(); - let policy_provider = - crate::PolicyProvider::new(policy_cache, provider, tokio::runtime::Handle::current()); - let block = L1BlockDeposits { header: seal(make_test_header(block_number)), events: L1PortalEvents::from_deposits(vec![L1Deposit::Encrypted(EncryptedDeposit { @@ -1089,15 +1035,14 @@ async fn test_prepare_rejects_unauthorized_encrypted_deposit_without_decryption_ nonce: encrypted.nonce, tag: encrypted.tag, })]), - policy_events: vec![], queue_hash_before: B256::ZERO, queue_hash_after: B256::ZERO, }; let prepared = block - .prepare(&sequencer_key, portal, &policy_provider) + .prepare(&sequencer_key, portal) .await - .expect("cached policy check should prepare block"); + .expect("decrypted deposit should prepare without an engine-side policy read"); assert_eq!(prepared.queued_deposits.len(), 1); assert_eq!( @@ -1105,12 +1050,13 @@ async fn test_prepare_rejects_unauthorized_encrypted_deposit_without_decryption_ DepositType::Encrypted ); assert!( - prepared.queued_deposits[0].rejected, - "unauthorized encrypted recipient must be rejected for deposit bounce-back" + !prepared.queued_deposits[0].rejected, + "policy is enforced by upstream TIP-20 mint execution" ); - assert!( - prepared.decryptions.is_empty(), - "rejected encrypted deposits must not consume DecryptionData" + assert_eq!( + prepared.decryptions.len(), + 1, + "successfully decrypted deposits must provide on-chain decryption data" ); } @@ -1123,12 +1069,12 @@ fn test_last_enqueued_survives_pop_and_drain() { let h100 = make_test_header(100); let h100_hash = header_hash(&h100); - queue.enqueue(h100, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h100, L1PortalEvents::from_deposits(vec![])); let h101 = make_chained_header(101, h100_hash); let h101_hash = header_hash(&h101); - queue.enqueue(h101, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h101, L1PortalEvents::from_deposits(vec![])); let h102 = make_chained_header(102, h101_hash); - queue.enqueue(h102, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h102, L1PortalEvents::from_deposits(vec![])); let last = queue.last_enqueued().unwrap(); assert_eq!(last.number, 102); @@ -1149,11 +1095,10 @@ fn test_last_enqueued_survives_pop_and_drain() { let h102_hash = last.hash; let h103 = make_chained_header(103, h102_hash); let h103_hash = header_hash(&h103); - queue.enqueue(h103, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h103, L1PortalEvents::from_deposits(vec![])); queue.enqueue( make_chained_header(104, h103_hash), L1PortalEvents::from_deposits(vec![]), - vec![], ); assert_eq!(queue.last_enqueued().unwrap().number, 104); @@ -1173,13 +1118,13 @@ fn test_try_enqueue_sequential_append() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); let h2 = make_chained_header(11, h1_hash); assert!(matches!( - queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1192,13 +1137,13 @@ fn test_try_enqueue_gap_returns_need_backfill() { let h1 = make_test_header(10); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); // Skip block 11, try to enqueue 12 let h3 = make_test_header(12); - match queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 11); assert_eq!(to, 11); @@ -1213,15 +1158,11 @@ fn test_try_enqueue_duplicate() { let h1 = make_test_header(10); assert!(matches!( - queue.try_enqueue( - seal(h1.clone()), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h1.clone()), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Duplicate )); } @@ -1233,20 +1174,20 @@ fn test_try_enqueue_reorg_purges_stale() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); let h2 = make_chained_header(11, h1_hash); let h2_hash = header_hash(&h2); assert!(matches!( - queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); let h3 = make_chained_header(12, h2_hash); assert!(matches!( - queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1256,11 +1197,7 @@ fn test_try_enqueue_reorg_purges_stale() { let mut h2_reorg = make_chained_header(11, h1_hash); h2_reorg.inner.gas_limit = 999; assert!(matches!( - queue.try_enqueue( - seal(h2_reorg), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h2_reorg), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1277,19 +1214,19 @@ fn test_try_enqueue_parent_mismatch_at_tip() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); let h2 = make_chained_header(11, h1_hash); assert!(matches!( - queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); // Block 12 with wrong parent hash — purges block 11, needs backfill let h3 = make_chained_header(12, B256::with_last_byte(0xDE)); - match queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 11); assert_eq!(to, 11); @@ -1315,7 +1252,7 @@ fn test_purge_rolls_back_deposit_hash() { memo: B256::ZERO, }); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![d1]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![d1])), EnqueueOutcome::Accepted )); let hash_after_h1 = queue.enqueued_head_hash(); @@ -1331,7 +1268,7 @@ fn test_purge_rolls_back_deposit_hash() { memo: B256::ZERO, }); assert!(matches!( - queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![d2]), vec![]), + queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![d2])), EnqueueOutcome::Accepted )); @@ -1342,11 +1279,7 @@ fn test_purge_rolls_back_deposit_hash() { let mut h2_reorg = make_chained_header(11, h1_hash); h2_reorg.inner.gas_limit = 999; assert!(matches!( - queue.try_enqueue( - seal(h2_reorg), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h2_reorg), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1372,26 +1305,14 @@ fn test_pop_advances_processed_head_hash() { let h1 = make_test_header(1); let h1_hash = header_hash(&h1); - queue.enqueue( - h1, - L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], - ); + queue.enqueue(h1, L1PortalEvents::from_deposits(vec![make_deposit(100)])); let h2 = make_chained_header(2, h1_hash); let h2_hash = header_hash(&h2); - queue.enqueue( - h2, - L1PortalEvents::from_deposits(vec![make_deposit(200)]), - vec![], - ); + queue.enqueue(h2, L1PortalEvents::from_deposits(vec![make_deposit(200)])); let h3 = make_chained_header(3, h2_hash); - queue.enqueue( - h3, - L1PortalEvents::from_deposits(vec![make_deposit(300)]), - vec![], - ); + queue.enqueue(h3, L1PortalEvents::from_deposits(vec![make_deposit(300)])); let hash_after_all = queue.enqueued_head_hash(); @@ -1420,26 +1341,22 @@ fn test_purge_after_pops() { let h1 = make_test_header(1); let h1_hash = header_hash(&h1); - queue.enqueue( - h1, - L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], - ); + queue.enqueue(h1, L1PortalEvents::from_deposits(vec![make_deposit(100)])); let h2 = make_chained_header(2, h1_hash); let h2_hash = header_hash(&h2); - queue.enqueue(h2, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h2, L1PortalEvents::from_deposits(vec![])); let h3 = make_chained_header(3, h2_hash); let h3_hash = header_hash(&h3); - queue.enqueue(h3, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h3, L1PortalEvents::from_deposits(vec![])); let h4 = make_chained_header(4, h3_hash); let h4_hash = header_hash(&h4); - queue.enqueue(h4, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h4, L1PortalEvents::from_deposits(vec![])); let h5 = make_chained_header(5, h4_hash); - queue.enqueue(h5, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h5, L1PortalEvents::from_deposits(vec![])); // Pop blocks 1 and 2 confirm(&mut queue); @@ -1452,11 +1369,7 @@ fn test_purge_after_pops() { let mut h4_reorg = make_chained_header(4, h3_hash); h4_reorg.inner.gas_limit = 999; assert!(matches!( - queue.try_enqueue( - seal(h4_reorg), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h4_reorg), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1475,26 +1388,14 @@ fn test_purge_first_pending_after_pop() { let h1 = make_test_header(1); let h1_hash = header_hash(&h1); - queue.enqueue( - h1, - L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], - ); + queue.enqueue(h1, L1PortalEvents::from_deposits(vec![make_deposit(100)])); let h2 = make_chained_header(2, h1_hash); let h2_hash = header_hash(&h2); - queue.enqueue( - h2, - L1PortalEvents::from_deposits(vec![make_deposit(200)]), - vec![], - ); + queue.enqueue(h2, L1PortalEvents::from_deposits(vec![make_deposit(200)])); let h3 = make_chained_header(3, h2_hash); - queue.enqueue( - h3, - L1PortalEvents::from_deposits(vec![make_deposit(300)]), - vec![], - ); + queue.enqueue(h3, L1PortalEvents::from_deposits(vec![make_deposit(300)])); // Pop block 1 — processed_head_hash advances let popped = confirm(&mut queue); @@ -1507,11 +1408,7 @@ fn test_purge_first_pending_after_pop() { h2_reorg.inner.gas_limit = 777; // This block has a different hash at height 2, so purge_from(0) fires. // Queue becomes empty, then the new block is accepted as anchor. - let outcome = queue.try_enqueue( - seal(h2_reorg), - L1PortalEvents::from_deposits(vec![]), - vec![], - ); + let outcome = queue.try_enqueue(seal(h2_reorg), L1PortalEvents::from_deposits(vec![])); assert!(matches!(outcome, EnqueueOutcome::Accepted)); // After purge and re-anchor, pending has just the new block 2 @@ -1528,11 +1425,7 @@ fn test_backfill_then_duplicate_redelivery() { let h1 = make_test_header(1); let h1_hash = header_hash(&h1); - queue.enqueue( - h1, - L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], - ); + queue.enqueue(h1, L1PortalEvents::from_deposits(vec![make_deposit(100)])); // Try to enqueue block 3 (skipping 2) => NeedBackfill let h2 = make_chained_header(2, h1_hash); @@ -1542,7 +1435,6 @@ fn test_backfill_then_duplicate_redelivery() { match queue.try_enqueue( seal(make_test_header(3)), L1PortalEvents::from_deposits(vec![]), - vec![], ) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 2); @@ -1552,16 +1444,11 @@ fn test_backfill_then_duplicate_redelivery() { } // Backfill: enqueue block 2, then block 3 - queue.enqueue( - h2, - L1PortalEvents::from_deposits(vec![make_deposit(200)]), - vec![], - ); + queue.enqueue(h2, L1PortalEvents::from_deposits(vec![make_deposit(200)])); assert!(matches!( queue.try_enqueue( h3_sealed.clone(), L1PortalEvents::from_deposits(vec![make_deposit(300)]), - vec![], ), EnqueueOutcome::Accepted )); @@ -1574,7 +1461,6 @@ fn test_backfill_then_duplicate_redelivery() { queue.try_enqueue( h3_sealed, L1PortalEvents::from_deposits(vec![make_deposit(300)]), - vec![], ), EnqueueOutcome::Duplicate )); @@ -1589,19 +1475,15 @@ fn test_zero_deposit_block_hash_invariant() { let h1 = make_test_header(1); let h1_hash = header_hash(&h1); - queue.enqueue( - h1, - L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], - ); + queue.enqueue(h1, L1PortalEvents::from_deposits(vec![make_deposit(100)])); let h2 = make_chained_header(2, h1_hash); let h2_hash = header_hash(&h2); - queue.enqueue(h2, L1PortalEvents::from_deposits(vec![]), vec![]); // no deposits + queue.enqueue(h2, L1PortalEvents::from_deposits(vec![])); // no deposits let h3 = make_chained_header(3, h2_hash); let d3 = make_deposit(300); - queue.enqueue(h3, L1PortalEvents::from_deposits(vec![d3.clone()]), vec![]); + queue.enqueue(h3, L1PortalEvents::from_deposits(vec![d3.clone()])); // Block 2 has no deposits => queue_hash_before == queue_hash_after assert_eq!( @@ -1617,11 +1499,7 @@ fn test_zero_deposit_block_hash_invariant() { h2_reorg.inner.gas_limit = 888; let h2_reorg_hash = header_hash(&h2_reorg); assert!(matches!( - queue.try_enqueue( - seal(h2_reorg), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h2_reorg), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1634,11 +1512,7 @@ fn test_zero_deposit_block_hash_invariant() { // Re-enqueue new block 3 with same deposits as original let h3_new = make_chained_header(3, h2_reorg_hash); assert!(matches!( - queue.try_enqueue( - seal(h3_new), - L1PortalEvents::from_deposits(vec![d3]), - vec![] - ), + queue.try_enqueue(seal(h3_new), L1PortalEvents::from_deposits(vec![d3])), EnqueueOutcome::Accepted )); @@ -1660,13 +1534,13 @@ fn test_disconnected_after_full_drain() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); let h2 = make_chained_header(11, h1_hash); assert!(matches!( - queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1678,7 +1552,7 @@ fn test_disconnected_after_full_drain() { // Block 12 arrives with wrong parent — consumed block 11 was reorged let h3_bad = make_chained_header(12, B256::with_last_byte(0xDE)); - match queue.try_enqueue(seal(h3_bad), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h3_bad), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { // Must re-fetch from block 11 (the consumed block that was reorged) assert_eq!(from, 11); @@ -1698,11 +1572,7 @@ fn test_disconnected_after_full_drain() { let h2_reorg = make_test_header(11); let h2_reorg_hash = header_hash(&h2_reorg); assert!(matches!( - queue.try_enqueue( - seal(h2_reorg), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h2_reorg), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Duplicate )); @@ -1711,7 +1581,7 @@ fn test_disconnected_after_full_drain() { // the builder will detect it). let h3 = make_chained_header(12, h2_reorg_hash); assert!(matches!( - queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); assert_eq!(queue.pending_len(), 1); @@ -1728,7 +1598,6 @@ fn test_disconnected_after_partial_drain() { queue.try_enqueue( seal(h1), L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], ), EnqueueOutcome::Accepted )); @@ -1739,14 +1608,13 @@ fn test_disconnected_after_partial_drain() { queue.try_enqueue( seal(h2), L1PortalEvents::from_deposits(vec![make_deposit(200)]), - vec![], ), EnqueueOutcome::Accepted )); let h3 = make_chained_header(12, h2_hash); assert!(matches!( - queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h3), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1757,7 +1625,7 @@ fn test_disconnected_after_partial_drain() { // Block 13 with wrong parent — this is a normal parent mismatch on the // non-empty queue path, should purge block 12 and request backfill let h4_bad = make_chained_header(13, B256::with_last_byte(0xAB)); - match queue.try_enqueue(seal(h4_bad), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h4_bad), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 12); assert_eq!(to, 12); @@ -1773,7 +1641,7 @@ fn test_disconnected_recovery_accepts_correct_block() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1784,7 +1652,7 @@ fn test_disconnected_recovery_accepts_correct_block() { // Wrong parent → NeedBackfill let h2_bad = make_chained_header(11, B256::with_last_byte(0xFF)); assert!(matches!( - queue.try_enqueue(seal(h2_bad), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h2_bad), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::NeedBackfill { .. } )); @@ -1794,7 +1662,6 @@ fn test_disconnected_recovery_accepts_correct_block() { queue.try_enqueue( seal(h2_good), L1PortalEvents::from_deposits(vec![make_deposit(500)]), - vec![], ), EnqueueOutcome::Accepted )); @@ -1808,7 +1675,7 @@ fn test_disconnected_with_multi_block_gap() { let h1 = make_test_header(10); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1818,7 +1685,7 @@ fn test_disconnected_with_multi_block_gap() { // Block 14 arrives — gap of 11..13 plus wrong parent is moot because // the gap check triggers first let h5 = make_test_header(14); - match queue.try_enqueue(seal(h5), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h5), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 11); assert_eq!(to, 13); @@ -1834,13 +1701,13 @@ fn test_duplicate_on_drained_queue() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); let h2 = make_chained_header(11, h1_hash); assert!(matches!( - queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h2), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -1854,7 +1721,6 @@ fn test_duplicate_on_drained_queue() { queue.try_enqueue( seal(make_test_header(10)), L1PortalEvents::from_deposits(vec![]), - vec![], ), EnqueueOutcome::Duplicate )); @@ -1862,7 +1728,6 @@ fn test_duplicate_on_drained_queue() { queue.try_enqueue( seal(make_chained_header(11, h1_hash)), L1PortalEvents::from_deposits(vec![]), - vec![], ), EnqueueOutcome::Duplicate )); @@ -1877,7 +1742,6 @@ fn test_disconnected_preserves_processed_head_hash_and_deposits() { queue.try_enqueue( seal(h1), L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], ), EnqueueOutcome::Accepted )); @@ -1890,7 +1754,7 @@ fn test_disconnected_preserves_processed_head_hash_and_deposits() { // Disconnected block should not alter processed_head_hash let h2_bad = make_chained_header(11, B256::with_last_byte(0xBB)); assert!(matches!( - queue.try_enqueue(seal(h2_bad), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h2_bad), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::NeedBackfill { .. } )); assert_eq!( @@ -1912,7 +1776,7 @@ fn test_reconnect_duplicate_does_not_clear_last_enqueued() { let mut queue = PendingDeposits::default(); let h1 = make_test_header(10); - queue.enqueue(h1.clone(), L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h1.clone(), L1PortalEvents::from_deposits(vec![])); // Drain confirm(&mut queue); @@ -1921,7 +1785,7 @@ fn test_reconnect_duplicate_does_not_clear_last_enqueued() { // Re-deliver same block 10 — must be Duplicate, last_enqueued preserved assert!(matches!( - queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h1), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Duplicate )); assert_eq!( @@ -1942,14 +1806,12 @@ fn test_backfill_overlap_idempotency() { queue.enqueue( h1.clone(), L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], ); let h2 = make_chained_header(11, h1_hash); queue.enqueue( h2.clone(), L1PortalEvents::from_deposits(vec![make_deposit(200)]), - vec![], ); let hash_before = queue.enqueued_head_hash(); @@ -1960,7 +1822,6 @@ fn test_backfill_overlap_idempotency() { queue.try_enqueue( seal(h1), L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], ), EnqueueOutcome::Duplicate )); @@ -1968,7 +1829,6 @@ fn test_backfill_overlap_idempotency() { queue.try_enqueue( seal(h2), L1PortalEvents::from_deposits(vec![make_deposit(200)]), - vec![], ), EnqueueOutcome::Duplicate )); @@ -1984,35 +1844,19 @@ fn test_reorg_within_pending_recomputes_hash() { let h10 = make_test_header(10); let h10_hash = header_hash(&h10); - queue.enqueue( - h10, - L1PortalEvents::from_deposits(vec![make_deposit(100)]), - vec![], - ); + queue.enqueue(h10, L1PortalEvents::from_deposits(vec![make_deposit(100)])); let hash_after_10 = queue.enqueued_head_hash(); let h11 = make_chained_header(11, h10_hash); let h11_hash = header_hash(&h11); - queue.enqueue( - h11, - L1PortalEvents::from_deposits(vec![make_deposit(200)]), - vec![], - ); + queue.enqueue(h11, L1PortalEvents::from_deposits(vec![make_deposit(200)])); let h12 = make_chained_header(12, h11_hash); let h12_hash = header_hash(&h12); - queue.enqueue( - h12, - L1PortalEvents::from_deposits(vec![make_deposit(300)]), - vec![], - ); + queue.enqueue(h12, L1PortalEvents::from_deposits(vec![make_deposit(300)])); let h13 = make_chained_header(13, h12_hash); - queue.enqueue( - h13, - L1PortalEvents::from_deposits(vec![make_deposit(400)]), - vec![], - ); + queue.enqueue(h13, L1PortalEvents::from_deposits(vec![make_deposit(400)])); assert_eq!(queue.pending_len(), 4); @@ -2025,7 +1869,6 @@ fn test_reorg_within_pending_recomputes_hash() { queue.try_enqueue( seal(h11_reorg), L1PortalEvents::from_deposits(vec![make_deposit(999)]), - vec![], ), EnqueueOutcome::Accepted )); @@ -2042,7 +1885,7 @@ fn test_reorg_within_pending_recomputes_hash() { // Can continue building on the new fork let h12_new = make_chained_header(12, h11_reorg_hash); assert!(matches!( - queue.try_enqueue(seal(h12_new), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h12_new), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); assert_eq!(queue.pending_len(), 3); @@ -2056,10 +1899,10 @@ fn test_drained_reorg_same_height_returns_duplicate() { let h10 = make_test_header(10); let h10_hash = header_hash(&h10); - queue.enqueue(h10, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h10, L1PortalEvents::from_deposits(vec![])); let h11 = make_chained_header(11, h10_hash); - queue.enqueue(h11.clone(), L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h11.clone(), L1PortalEvents::from_deposits(vec![])); // Drain confirm(&mut queue); @@ -2067,7 +1910,7 @@ fn test_drained_reorg_same_height_returns_duplicate() { // Re-deliver block 11 with same hash — Duplicate, last_enqueued intact assert!(matches!( - queue.try_enqueue(seal(h11), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h11), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Duplicate )); assert_eq!(queue.last_enqueued().unwrap().number, 11); @@ -2083,10 +1926,10 @@ fn test_pop_sets_last_processed() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); - queue.enqueue(h1, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h1, L1PortalEvents::from_deposits(vec![])); let h2 = make_chained_header(11, h1_hash); - queue.enqueue(h2, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h2, L1PortalEvents::from_deposits(vec![])); let popped = confirm(&mut queue); assert_eq!(queue.last_processed().unwrap().number, 10); @@ -2102,10 +1945,10 @@ fn test_drain_sets_last_processed() { let h1 = make_test_header(10); let h1_hash = header_hash(&h1); - queue.enqueue(h1, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h1, L1PortalEvents::from_deposits(vec![])); let h2 = make_chained_header(11, h1_hash); - queue.enqueue(h2, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h2, L1PortalEvents::from_deposits(vec![])); let drained = queue.drain(); assert_eq!(queue.last_processed().unwrap().number, 11); @@ -2127,11 +1970,11 @@ fn test_reorg_of_consumed_block_skips_stale_during_backfill() { let h10 = make_test_header(10); let h10_hash = header_hash(&h10); - queue.enqueue(h10, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h10, L1PortalEvents::from_deposits(vec![])); let h11 = make_chained_header(11, h10_hash); let _h11_hash = header_hash(&h11); - queue.enqueue(h11, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h11, L1PortalEvents::from_deposits(vec![])); // Engine consumes both blocks confirm(&mut queue); // block 10 @@ -2142,7 +1985,7 @@ fn test_reorg_of_consumed_block_skips_stale_during_backfill() { // New block 12 arrives with parent pointing to NEW (reorged) block 11 let h12_new = make_chained_header(12, B256::with_last_byte(0xDE)); - match queue.try_enqueue(seal(h12_new), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h12_new), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 11); assert_eq!(to, 11); @@ -2158,11 +2001,7 @@ fn test_reorg_of_consumed_block_skips_stale_during_backfill() { h11_reorg.inner.gas_limit = 999; let h11_reorg_hash = header_hash(&h11_reorg); assert!(matches!( - queue.try_enqueue( - seal(h11_reorg), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h11_reorg), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Duplicate )); @@ -2171,7 +2010,7 @@ fn test_reorg_of_consumed_block_skips_stale_during_backfill() { // expected and will be caught by the builder) let h12 = make_chained_header(12, h11_reorg_hash); assert!(matches!( - queue.try_enqueue(seal(h12), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h12), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); @@ -2188,7 +2027,7 @@ fn test_consumed_reorg_gap_uses_last_processed_floor() { let h10 = make_test_header(10); let _h10_hash = header_hash(&h10); - queue.enqueue(h10, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h10, L1PortalEvents::from_deposits(vec![])); confirm(&mut queue); // Simulate last_enqueued being cleared (reorg path) @@ -2196,7 +2035,7 @@ fn test_consumed_reorg_gap_uses_last_processed_floor() { // Block 13 arrives — gap from 11..12 let h13 = make_test_header(13); - match queue.try_enqueue(seal(h13), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h13), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 11); // last_processed.number + 1 assert_eq!(to, 12); @@ -2215,11 +2054,11 @@ fn test_builder_skips_stale_blocks_in_queue() { // Enqueue block 10 (stale — zone already processed it) let h10 = make_test_header(10); let h10_hash = header_hash(&h10); - queue.enqueue(h10, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h10, L1PortalEvents::from_deposits(vec![])); // Enqueue block 11 (the one the builder actually needs) let h11 = make_chained_header(11, h10_hash); - queue.enqueue(h11, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h11, L1PortalEvents::from_deposits(vec![])); // Builder expects block 11 (tempoBlockNumber=10, expected=11). // Peek/confirm loop should skip block 10 and use the correct one. @@ -2244,14 +2083,14 @@ fn test_reorg_consumed_then_continue_on_new_chain() { // Build a 3-block chain: 100, 101, 102 let h100 = make_test_header(100); let h100_hash = header_hash(&h100); - queue.enqueue(h100, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h100, L1PortalEvents::from_deposits(vec![])); let h101 = make_chained_header(101, h100_hash); let h101_hash = header_hash(&h101); - queue.enqueue(h101, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h101, L1PortalEvents::from_deposits(vec![])); let h102 = make_chained_header(102, h101_hash); - queue.enqueue(h102, L1PortalEvents::from_deposits(vec![]), vec![]); + queue.enqueue(h102, L1PortalEvents::from_deposits(vec![])); // Engine consumes all 3 confirm(&mut queue); @@ -2262,7 +2101,7 @@ fn test_reorg_consumed_then_continue_on_new_chain() { // L1 reorgs at 102: new block 103 has different parent let new_parent = B256::with_last_byte(0xAA); let h103 = make_chained_header(103, new_parent); - match queue.try_enqueue(seal(h103), L1PortalEvents::from_deposits(vec![]), vec![]) { + match queue.try_enqueue(seal(h103), L1PortalEvents::from_deposits(vec![])) { EnqueueOutcome::NeedBackfill { from, to } => { assert_eq!(from, 102); assert_eq!(to, 102); @@ -2275,11 +2114,7 @@ fn test_reorg_consumed_then_continue_on_new_chain() { h102_new.inner.gas_limit = 42; let h102_new_hash = header_hash(&h102_new); assert!(matches!( - queue.try_enqueue( - seal(h102_new), - L1PortalEvents::from_deposits(vec![]), - vec![] - ), + queue.try_enqueue(seal(h102_new), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Duplicate )); @@ -2287,14 +2122,14 @@ fn test_reorg_consumed_then_continue_on_new_chain() { let h103 = make_chained_header(103, h102_new_hash); let h103_hash = header_hash(&h103); assert!(matches!( - queue.try_enqueue(seal(h103), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h103), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); // Block 104 continues on the new chain let h104 = make_chained_header(104, h103_hash); assert!(matches!( - queue.try_enqueue(seal(h104), L1PortalEvents::from_deposits(vec![]), vec![]), + queue.try_enqueue(seal(h104), L1PortalEvents::from_deposits(vec![])), EnqueueOutcome::Accepted )); diff --git a/crates/node/README.md b/crates/node/README.md index 7d947e8cb..584642d5c 100644 --- a/crates/node/README.md +++ b/crates/node/README.md @@ -136,8 +136,8 @@ using Tempo's upstream policy implementation over anchored raw L1 state: normal EVM gas, warming, and storage accounting, while persistent writes to L1-owned slots are rejected. 3. **Tempo TIP-20 and TIP-403 precompiles** — execute the upstream business - logic against that composed storage view, replacing the zone's duplicated - policy dispatch. Missing or invalid anchored state fails closed, and zone + logic against that composed storage view, without a separate zone policy + path. Missing or invalid anchored state fails closed, and zone privacy, bridge authorization, admission, and fixed-gas rules remain in the surrounding execution layer. diff --git a/crates/node/src/engine.rs b/crates/node/src/engine.rs index f029a2b08..e2dca0a52 100644 --- a/crates/node/src/engine.rs +++ b/crates/node/src/engine.rs @@ -51,7 +51,7 @@ use tempo_primitives::TempoHeader; use tracing::{error, warn}; use zone_chainspec::ZoneChainSpec; -use zone_l1::{DepositQueue, L1BlockDeposits, PolicyProvider, PreparedL1Block}; +use zone_l1::{DepositQueue, L1BlockDeposits, PreparedL1Block}; use zone_payload::{ZonePayloadAttributes, ZonePayloadTypes}; /// Engine that drives L2 block production from L1 events. @@ -84,9 +84,6 @@ pub struct ZoneEngine { 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 { @@ -99,7 +96,6 @@ impl ZoneEngine { fee_recipient: Address, sequencer_key: k256::SecretKey, portal_address: Address, - policy_provider: PolicyProvider, ) -> Self { Self { chain_spec, @@ -110,7 +106,6 @@ impl ZoneEngine { fee_recipient, sequencer_key, portal_address, - policy_provider, } } @@ -182,17 +177,12 @@ impl ZoneEngine { } } - /// 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. + /// Decrypt encrypted deposits and ABI-encode them into a [`PreparedL1Block`] ready for + /// the payload builder. Mint-recipient policy is enforced during upstream TIP-20 execution + /// against the finalized L1 anchor. async fn prepare_l1_block(&self, l1_block: L1BlockDeposits) -> eyre::Result { l1_block - .prepare( - &self.sequencer_key, - self.portal_address, - &self.policy_provider, - ) + .prepare(&self.sequencer_key, self.portal_address) .await } @@ -271,12 +261,6 @@ impl ZoneEngine { 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 diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index aacf662c4..3b1b452bc 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -55,18 +55,13 @@ use tempo_transaction_pool::{ transaction::{TempoPoolTransactionError, 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, warn}; +use tempo_zone_contracts::{TEMPO_STATE_ADDRESS, ZONE_INBOX_ADDRESS, ZONE_OUTBOX_ADDRESS}; +use tracing::{debug, info}; use zone_chainspec::ZoneChainSpec; 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, - }, + DepositQueue, L1Subscriber, L1SubscriberConfig, TempoStateExt, + state::{L1StateCache, L1StateProvider, L1StateProviderConfig}, }; use zone_p2p::{P2pConfig, P2pNetworkId, Role, spawn_p2p}; use zone_payload::{ @@ -175,14 +170,8 @@ pub struct ZoneNode { 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>, /// Number of zone blocks between withdrawal batch boundaries. withdrawal_batch_interval_blocks: u64, /// Encrypts authenticated-withdrawal sender reveal data during payload construction. @@ -206,13 +195,11 @@ impl ZoneNode { ) -> 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, @@ -230,9 +217,7 @@ impl ZoneNode { l1_config, l1_state_provider_config, l1_state_cache, - policy_cache, portal_address, - initial_tokens: None, withdrawal_batch_interval_blocks: DEFAULT_WITHDRAWAL_BATCH_INTERVAL_BLOCKS, withdrawal_reveal_encryptor: None, private_rpc_config: ZonePrivateRpcConfig::default(), @@ -274,13 +259,6 @@ impl ZoneNode { 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 - } - /// Set the parent L1 chain ID, avoiding a startup RPC lookup. pub fn with_l1_chain_id(mut self, chain_id: u64) -> Self { self.l1_state_provider_config.chain_id = Some(chain_id); @@ -319,11 +297,6 @@ impl ZoneNode { 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, @@ -390,12 +363,8 @@ where 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. @@ -422,9 +391,7 @@ where 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, p2p_config: Option, @@ -440,9 +407,7 @@ where ), deposit_queue, l1_config, - policy_cache, portal_address, - initial_tokens, private_rpc_config, sequencer_config, p2p_config, @@ -470,7 +435,6 @@ where 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::() @@ -481,7 +445,6 @@ where .await? .erased(); - self.resolve_and_seed_tokens(&l1_provider).await?; let p2p_role = self.p2p_config.as_ref().map(P2pConfig::role); if p2p_role == Some(Role::Follower) { // TODO(multi-sequencer): Split L1 observation/cache updates from deposit @@ -493,7 +456,6 @@ where } else { self.spawn_l1_subscriber(&ctx); } - self.spawn_policy_tasks(&l1_provider, &ctx); let task_executor = ctx.node.task_executor().clone(); if let Some(config) = self.p2p_config.take() { @@ -511,7 +473,7 @@ where 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)?; + self.spawn_zone_engine(&ctx, sequencer_addr, sequencer_key)?; } let chain_id = ctx.node.provider().chain_spec().genesis().config.chain_id; @@ -629,61 +591,6 @@ where Ok(()) } - /// 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 block_number = self.policy_cache.last_l1_block(); - let tokens = match ZonePortal::new(portal, l1_provider) - .enabled_tokens_at(alloy_rpc_types_eth::BlockId::number(block_number)) - .await - { - Ok(tokens) => tokens, - Err(err) => { - warn!( - target: "reth::cli", - %err, - block_number, - %portal, - "Failed to discover enabled tokens from L1 for policy cache seeding; continuing without initial token policy seed" - ); - return Ok(()); - } - }; - info!( - target: "reth::cli", - count = tokens.len(), - ?tokens, - block_number, - "Discovered enabled tokens from L1" - ); - tokens - }; - - if let Err(err) = self - .policy_cache - .seed_token_policies(portal, &tracked_tokens, l1_provider) - .await - { - warn!( - target: "reth::cli", - %err, - count = tracked_tokens.len(), - %portal, - "Failed to seed token policies from L1; continuing with RPC fallback" - ); - return Ok(()); - } - 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( @@ -695,40 +602,13 @@ where 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()?)? @@ -742,7 +622,6 @@ where fee_recipient, sequencer_key, self.portal_address, - policy_provider, ); ctx.node .task_executor() @@ -933,9 +812,7 @@ where 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(), self.p2p_config.clone(), diff --git a/crates/node/tests/it/enable_token.rs b/crates/node/tests/it/enable_token.rs index b54be27c9..5e1cb26bc 100644 --- a/crates/node/tests/it/enable_token.rs +++ b/crates/node/tests/it/enable_token.rs @@ -12,12 +12,7 @@ use crate::utils::{DEFAULT_TIMEOUT, L1Fixture, start_local_zone_with_fixture}; // Imports for real-L1 tests use crate::utils::{L1TestNode, ZoneAccount, ZoneTestNode, spawn_sequencer}; use alloy::primitives::B256; -use alloy_provider::{Provider, ProviderBuilder}; -use alloy_rpc_types_eth::BlockId; -use tempo_alloy::TempoNetwork; -use tempo_precompiles::PATH_USD_ADDRESS; -use tempo_zone_contracts::ZonePortal; -use zone_l1::PolicyCache; +use alloy_provider::Provider; /// Enable a new token (AlphaUSD) via a `TokenEnabled` event, then deposit it /// and verify the recipient receives the minted balance. @@ -118,92 +113,6 @@ async fn test_enable_token_and_deposit_same_block() -> eyre::Result<()> { /// 500ms and the L1Subscriber needs to connect, backfill, and subscribe. const L1_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); -#[tokio::test(flavor = "multi_thread")] -async fn test_startup_discovers_enabled_tokens_at_local_l1_height() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - - let l1 = L1TestNode::start().await?; - let portal_address = l1.deploy_zone().await?; - let seed_block = l1.provider().get_block_number().await?; - - let future_salt = B256::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 42, - ]); - let future_token = l1.create_tip20("FutureUSD", "fUSD", future_salt).await?; - l1.enable_token_on_portal(portal_address, future_token) - .await?; - - let portal = ZonePortal::new(portal_address, l1.provider()); - let latest_tokens = portal.enabled_tokens().await?; - assert!( - latest_tokens.contains(&future_token), - "latest portal state should include the newly-enabled token" - ); - - let historical_tokens = portal - .enabled_tokens_at(BlockId::number(seed_block)) - .await?; - assert_eq!( - historical_tokens, - vec![PATH_USD_ADDRESS], - "startup should discover only tokens enabled at the local L1 height" - ); - - let tempo_provider = ProviderBuilder::new_with_network::() - .connect_http(l1.http_url().clone()) - .erased(); - - let latest_seed_cache = PolicyCache::default(); - latest_seed_cache.set_last_l1_block(seed_block); - let err = latest_seed_cache - .seed_token_policies(portal_address, &latest_tokens, &tempo_provider) - .await - .expect_err("latest token discovery should include a future uninitialized token"); - assert!( - err.to_string().contains("Uninitialized"), - "expected uninitialized future token error, got {err}" - ); - - let historical_seed_cache = PolicyCache::default(); - historical_seed_cache.set_last_l1_block(seed_block); - historical_seed_cache - .seed_token_policies(portal_address, &historical_tokens, &tempo_provider) - .await?; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_startup_policy_seed_errors_do_not_abort_launch() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - - let l1 = L1TestNode::start().await?; - let portal_address = l1.deploy_zone().await?; - let seed_block = l1.provider().get_block_number().await?; - - let future_salt = B256::new([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 43, - ]); - let future_token = l1 - .create_tip20("FutureSeedUSD", "fsUSD", future_salt) - .await?; - l1.enable_token_on_portal(portal_address, future_token) - .await?; - - let _zone = ZoneTestNode::start_from_l1_at_block_with_initial_tokens( - l1.http_url(), - l1.ws_url(), - portal_address, - seed_block, - Some(vec![future_token]), - ) - .await?; - - Ok(()) -} - /// Full TokenEnabled pipeline with a real in-process L1 node: /// /// 1. Start L1 dev node. diff --git a/crates/node/tests/it/l1_e2e.rs b/crates/node/tests/it/l1_e2e.rs index 36f1737df..6f1fb570e 100644 --- a/crates/node/tests/it/l1_e2e.rs +++ b/crates/node/tests/it/l1_e2e.rs @@ -168,12 +168,11 @@ async fn test_dev_provisioner_replays_initial_token_event() -> eyre::Result<()> let latest_l1_block = l1.provider().get_block_number().await?; assert!(latest_l1_block > provisioned.anchor_block_number); - let zone = ZoneTestNode::start_from_l1_at_block_with_initial_tokens( + let zone = ZoneTestNode::start_from_l1_at_block( l1.http_url(), l1.ws_url(), provisioned.portal, provisioned.anchor_block_number, - None, ) .await?; zone.wait_for_l2_tempo_finalized(latest_l1_block, L1_TIMEOUT) @@ -454,18 +453,6 @@ async fn test_cross_zone_encrypted_router_bounceback_recipient() -> eyre::Result l1.set_sequencer_encryption_key_with_signer(portal_b, &encryption_key, seq_b_signer.clone()) .await?; - { - use tempo_contracts::precompiles::ITIP403Registry::PolicyType; - - let l1_block = l1.provider().get_block_number().await?; - for policy_cache in [zone_a.policy_cache(), zone_b.policy_cache()] { - let mut cache = policy_cache.write(); - cache.set_policy_type(policy_id, PolicyType::BLACKLIST); - cache.set_token_policy(PATH_USD_ADDRESS, l1_block, policy_id); - cache.set_policy_status(policy_id, blacklisted_recipient, l1_block, true); - } - } - let mut alice = ZoneAccount::from_l1_and_zone(&l1, &zone_a, portal_a); let deposit_amount: u128 = 2_000_000; let cross_amount: u128 = 1_000_000; @@ -1171,12 +1158,10 @@ async fn test_l1_policy_operations_and_zone_advancement() -> eyre::Result<()> { /// /// 1. Start L1 dev node, deploy zone, register encryption key. /// 2. Create a blacklist policy, assign to pathUSD, blacklist the recipient. -/// 3. Fund the policy cache so the zone knows about the blacklist. -/// 4. Make an encrypted deposit targeting the blacklisted recipient. -/// 5. Verify the deposit is refunded to the sender on L1. +/// 3. Make an encrypted deposit targeting the blacklisted recipient. +/// 4. Verify upstream TIP-20 mint enforcement fails and refunds the sender on L1. /// -/// NOTE: This test validates the builder-level policy check in `build_encrypted_deposit`. -/// The zone's policy cache must be pre-populated for the check to trigger. +/// `AnchoredZoneDb` exposes finalized L1 policy state directly to upstream Tempo execution. #[tokio::test(flavor = "multi_thread")] async fn test_encrypted_deposit_blacklisted_recipient() -> eyre::Result<()> { reth_tracing::init_test_tracing(); @@ -1210,23 +1195,7 @@ async fn test_encrypted_deposit_blacklisted_recipient() -> eyre::Result<()> { l1.set_sequencer_encryption_key(portal_address, &encryption_key) .await?; - // --- Step 5: Pre-populate zone's policy cache --- - // The builder checks PolicyCacheInner during encrypted deposit processing. - // Since the L1 subscriber may not have caught up yet, seed it manually. - { - use tempo_contracts::precompiles::ITIP403Registry::PolicyType; - - let policy_cache = zone.policy_cache(); - // Fetch L1 block number before acquiring the write lock to avoid - // holding a parking_lot guard across an await point. - let l1_block = l1.provider().get_block_number().await?; - let mut cache = policy_cache.write(); - cache.set_policy_type(policy_id, PolicyType::BLACKLIST); - cache.set_token_policy(PATH_USD_ADDRESS, l1_block, policy_id); - cache.set_policy_status(policy_id, blacklisted_recipient, l1_block, true); - } - - // --- Step 6: Make an encrypted deposit targeting the blacklisted recipient --- + // --- Step 5: Make an encrypted deposit targeting the blacklisted recipient --- let depositor = ZoneAccount::from_l1_and_zone(&l1, &zone, portal_address); let deposit_amount: u128 = 1_000_000; l1.fund_user(depositor.address(), deposit_amount).await?; @@ -1296,7 +1265,7 @@ async fn test_encrypted_deposit_blacklisted_recipient() -> eyre::Result<()> { ) .await?; - // The blacklisted recipient should NOT have received the deposit + // The blacklisted recipient should NOT have received the deposit. let recipient_balance = zone .balance_of(ZONE_TOKEN_ADDRESS, blacklisted_recipient) .await?; @@ -1360,26 +1329,6 @@ async fn test_blacklisted_sender_transfer_rejected() -> eyre::Result<()> { let zone = ZoneTestNode::start_from_l1(l1.http_url(), l1.ws_url(), portal_address).await?; zone.wait_for_l2_tempo_finalized(0, L1_TIMEOUT).await?; - // Seed the policy cache manually so it has policy data before test execution. - { - use tempo_contracts::precompiles::ITIP403Registry::PolicyType; - - let l1_block = l1.provider().get_block_number().await?; - let mut cache = zone.policy_cache().write(); - cache.set_token_policy(PATH_USD_ADDRESS, l1_block, compound_policy_id); - cache.set_policy_type(compound_policy_id, PolicyType::COMPOUND); - cache.set_compound( - compound_policy_id, - zone_l1::state::tip403::CompoundData { - sender_policy_id, - recipient_policy_id: 1, - mint_recipient_policy_id: 1, - }, - ); - cache.set_policy_type(sender_policy_id, PolicyType::BLACKLIST); - cache.set_policy_status(sender_policy_id, alice, l1_block, true); - } - // --- Step 4: Deposit to Alice via the dev account --- // Alice is blacklisted as a sender, so she can't transfer pathUSD on L1 // herself. The dev account deposits on her behalf (recipient = allow-all). @@ -1415,10 +1364,9 @@ async fn test_blacklisted_sender_transfer_rejected() -> eyre::Result<()> { ) .await?; - // --- Step 5: Alice attempts a transfer → should be rejected --- - // The transfer may be rejected at the pool level (send() returns Err) or - // accepted into a block but reverted (receipt.status() == false). Either - // outcome proves the blacklist is enforced. + // --- Step 5: Alice simulates a transfer → should be rejected --- + // Use an exact stateful call instead of waiting on pool inclusion: policy-invalid + // transactions are allowed to remain pending, so absence of a receipt is not proof. let bob = Address::with_last_byte(0xBB); let alice_provider = alloy::providers::ProviderBuilder::new() @@ -1426,28 +1374,15 @@ async fn test_blacklisted_sender_transfer_rejected() -> eyre::Result<()> { .connect_http(zone.http_url().clone()); let tip20 = tempo_contracts::precompiles::ITIP20::new(ZONE_TOKEN_ADDRESS, &alice_provider); - let send_result = tip20 + let transfer = tip20 .transfer(bob, U256::from(200_000u128)) - .gas_price(tempo_chainspec::spec::TEMPO_T1_BASE_FEE as u128) - .gas(500_000) - .send() + .from(alice) + .call() .await; - - match send_result { - Err(_) => { - // Pool-level rejection — blacklist enforced before inclusion. - } - Ok(pending) => { - // Tx was accepted into the pool — wait for it to be included and - // verify it reverted. - tokio::time::sleep(Duration::from_secs(3)).await; - let receipt = pending.get_receipt().await?; - assert!( - !receipt.status(), - "transfer from blacklisted sender should revert, but succeeded" - ); - } - } + assert!( + transfer.is_err(), + "transfer simulation from blacklisted sender should revert" + ); // Bob should have zero balance let bob_balance = zone.balance_of(ZONE_TOKEN_ADDRESS, bob).await?; diff --git a/crates/node/tests/it/tip403_policy.rs b/crates/node/tests/it/tip403_policy.rs index ddb0fa95a..c0a81ef26 100644 --- a/crates/node/tests/it/tip403_policy.rs +++ b/crates/node/tests/it/tip403_policy.rs @@ -14,7 +14,6 @@ use tempo_contracts::precompiles::{ ITIP403Registry::{self, PolicyType}, }; use tempo_precompiles::{PATH_USD_ADDRESS, TIP_FEE_MANAGER_ADDRESS, TIP403_REGISTRY_ADDRESS}; -use zone_l1::state::tip403::PolicyEvent; use crate::utils::{ DEFAULT_TIMEOUT, PolicySeed, TEST_MNEMONIC, TIP20_TX_GAS, seed_raw_tip20_policy_id, @@ -23,8 +22,7 @@ use crate::utils::{ /// Deposit pathUSD to Alice, then transfer a portion to Bob on the zone. /// -/// TIP-20 transfers use the default `transferPolicyId` of 1 (allow all), -/// so they always succeed regardless of the policy cache state. +/// TIP-20 transfers use the default anchored `transferPolicyId` of 1 (allow all). #[tokio::test(flavor = "multi_thread")] async fn test_tip20_transfer_on_zone() -> eyre::Result<()> { reth_tracing::init_test_tracing(); @@ -500,58 +498,3 @@ async fn test_policy_proxy_uses_block_versioned_raw_state() -> eyre::Result<()> Ok(()) } - -/// `TokenPolicyChanged` event updates the token→policy mapping in the cache. -#[tokio::test(flavor = "multi_thread")] -async fn test_token_policy_change_via_events() -> eyre::Result<()> { - reth_tracing::init_test_tracing(); - - let (zone, mut fixture) = start_local_zone_with_fixture(10).await?; - - fixture.inject_empty_blocks(zone.deposit_queue(), 3); - zone.wait_for_tempo_block_number(3, DEFAULT_TIMEOUT).await?; - - let token = address!("0x0000000000000000000000000000000000BEEF01"); - - // Apply a token policy change event - { - let cache = zone.policy_cache(); - cache.write().apply_events( - 1, - &[PolicyEvent::TokenPolicyChanged { - token, - policy_id: 5, - }], - ); - } - - // Verify via cache read that the token policy was set - { - let cache = zone.policy_cache(); - let r = cache.read(); - let policy_id = r.get_token_policy(token, u64::MAX); - assert_eq!(policy_id, Some(5), "token should map to policy 5"); - } - - // Update the token's policy to 7 - { - let cache = zone.policy_cache(); - cache.write().apply_events( - 2, - &[PolicyEvent::TokenPolicyChanged { - token, - policy_id: 7, - }], - ); - } - - // Verify the update took effect - { - let cache = zone.policy_cache(); - let r = cache.read(); - let policy_id = r.get_token_policy(token, u64::MAX); - assert_eq!(policy_id, Some(7), "token should now map to policy 7"); - } - - Ok(()) -} diff --git a/crates/node/tests/it/utils.rs b/crates/node/tests/it/utils.rs index c32a0199c..71c7a2a8e 100644 --- a/crates/node/tests/it/utils.rs +++ b/crates/node/tests/it/utils.rs @@ -306,23 +306,6 @@ fn install_reference_zone_factory(genesis: &mut Genesis, owner: Address) -> eyre /// without a running L1. The L1Subscriber will fail and retry in the background. const DUMMY_L1_URL: &str = "http://127.0.0.1:1"; -/// Seed the local test policy cache with the default pathUSD transfer policy. -/// -/// Self-contained zone tests boot without a real L1, so startup can't resolve the -/// 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_l1::PolicyCache) { - const LOCAL_POLICY_CACHE_SEED_BLOCK: u64 = 1; - - policy_cache.set_last_l1_block(LOCAL_POLICY_CACHE_SEED_BLOCK); - policy_cache.write().set_token_policy( - PATH_USD_ADDRESS, - LOCAL_POLICY_CACHE_SEED_BLOCK, - ALLOW_ALL_POLICY_ID, - ); -} - // TODO(rusowsky): Remove once Tempo L1 stores transfer policy IDs in the TIP403 precompile. fn pack_transfer_policy_id(policy_id: u64) -> U256 { U256::from(policy_id) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8) @@ -507,7 +490,6 @@ pub(crate) struct ZoneTestNode { http_url: url::Url, deposit_queue: DepositQueue, l1_state_cache: L1StateCache, - policy_cache: zone_l1::PolicyCache, rpc_api_factory: Arc, node_handle: Box, _tasks: Runtime, @@ -536,11 +518,6 @@ impl ZoneTestNode { &self.l1_state_cache } - /// Returns a handle to the policy cache for TIP-403 authorization. - 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, @@ -738,15 +715,12 @@ impl ZoneTestNode { .await } - /// Start a zone node connected to a real L1, anchoring genesis to a specific - /// L1 block and optionally overriding the initial token list used for - /// startup policy cache seeding. - pub(crate) async fn start_from_l1_at_block_with_initial_tokens( + /// Start a zone node connected to a real L1, anchoring genesis to a specific L1 block. + pub(crate) async fn start_from_l1_at_block( l1_http_url: &url::Url, l1_ws_url: &url::Url, portal_address: Address, block_number: u64, - initial_tokens: Option>, ) -> eyre::Result { let (genesis, genesis_block_number) = build_l1_anchored_genesis_at_block(l1_http_url, portal_address, block_number).await?; @@ -760,7 +734,6 @@ impl ZoneTestNode { Some(genesis), signer, 8, - initial_tokens, None, true, ) @@ -785,7 +758,6 @@ impl ZoneTestNode { Some(genesis), signer, withdrawal_batch_interval_blocks, - Some(vec![]), None, true, ) @@ -861,7 +833,6 @@ impl ZoneTestNode { None, signer, 8, - Some(vec![]), Some(p2p_config), true, ) @@ -904,7 +875,6 @@ impl ZoneTestNode { custom_genesis, sequencer_signer, 8, - Some(vec![]), None, true, ) @@ -920,13 +890,11 @@ impl ZoneTestNode { custom_genesis: Option, sequencer_signer: alloy_signer_local::PrivateKeySigner, withdrawal_batch_interval_blocks: u64, - initial_tokens: Option>, p2p_config: Option, spawn_engine: bool, ) -> eyre::Result { let tasks = Runtime::test(); let is_local_dummy_l1 = l1_ws_url == DUMMY_L1_URL; - let l1_provider_url = l1_ws_url.clone(); let mut genesis = custom_genesis.unwrap_or_else(|| { serde_json::from_str(zone_node::genesis::GENESIS_TEMPLATE_JSON) @@ -948,9 +916,6 @@ impl ZoneTestNode { .with_l1_chain_id(1337) .with_l1_state_provider_retry_limits(0, 1); } - if let Some(initial_tokens) = initial_tokens { - zone_node = zone_node.with_initial_tokens(initial_tokens); - } let p2p_enabled = p2p_config.is_some(); if let Some(p2p_config) = p2p_config { zone_node = zone_node.with_p2p(p2p_config); @@ -978,9 +943,7 @@ impl ZoneTestNode { let deposit_queue = zone_node.deposit_queue(); let l1_state_cache = zone_node.l1_state_cache(); - let policy_cache = zone_node.policy_cache(); if is_local_dummy_l1 { - seed_local_policy_cache(&policy_cache); let mut cache = l1_state_cache.write(); seed_raw_tip20_policy_id(&mut cache, 0, PATH_USD_ADDRESS, ALLOW_ALL_POLICY_ID); } @@ -992,15 +955,6 @@ impl ZoneTestNode { .await?; if spawn_engine { - let l1_provider = ProviderBuilder::new_with_network::() - .connect(&l1_provider_url) - .await? - .erased(); - let policy_provider = zone_l1::PolicyProvider::new( - policy_cache.clone(), - l1_provider, - tokio::runtime::Handle::current(), - ); let provider = node_handle.node.provider(); let last_header = provider .sealed_header(provider.best_block_number()?)? @@ -1014,7 +968,6 @@ impl ZoneTestNode { sequencer_signer.address(), SecretKey::from(sequencer_signer.credential()), portal_address, - policy_provider, ); node_handle .node @@ -1048,7 +1001,6 @@ impl ZoneTestNode { deposit_queue, http_url, l1_state_cache, - policy_cache, rpc_api_factory, node_handle: Box::new(node_handle), _tasks: tasks, @@ -2706,10 +2658,6 @@ pub(crate) async fn start_local_zone_with_fixture( let zone = ZoneTestNode::start_local().await?; let fixture = L1Fixture::new(); - // Local tests have no real L1, so the RPC fallback in resolve_transfer_policy_id - // fails. Seed pathUSD with the default allow-all policy (mirrors L1 default). - seed_local_policy_cache(zone.policy_cache()); - fixture.seed_l1_cache( zone.l1_state_cache(), Address::ZERO, @@ -2828,7 +2776,6 @@ pub(crate) async fn start_local_p2p_pair( Some(genesis.clone()), signer.clone(), 8, - Some(vec![]), Some(configs.remove(0)), true, ) @@ -2841,7 +2788,6 @@ pub(crate) async fn start_local_p2p_pair( Some(genesis), signer, 8, - Some(vec![]), Some(configs.remove(0)), false, ) @@ -2849,7 +2795,6 @@ pub(crate) async fn start_local_p2p_pair( let fixture = L1Fixture::new(); for zone in [&leader, &follower] { - seed_local_policy_cache(zone.policy_cache()); fixture.seed_l1_cache( zone.l1_state_cache(), Address::ZERO, @@ -3801,7 +3746,7 @@ impl L1Fixture { self.seed_regular_deposit_policy_state(block.header.inner.number, &deposits); let l1_deposits = deposits.into_iter().map(L1Deposit::Regular).collect(); let events = L1PortalEvents::from_deposits(l1_deposits); - queue.enqueue(block.header.clone(), events, vec![]); + queue.enqueue(block.header.clone(), events); } /// Enqueue a pre-built block into a deposit queue with full portal events. @@ -3819,7 +3764,7 @@ impl L1Fixture { .expect("event receive-policy fixture seed must be admitted"); } } - queue.enqueue(block.header.clone(), events, vec![]); + queue.enqueue(block.header.clone(), events); } /// Create a [`Deposit`] for a specific L1 block. @@ -3853,13 +3798,13 @@ impl L1Fixture { enabled_tokens: tokens, ..Default::default() }; - queue.enqueue(header, events, vec![]); + queue.enqueue(header, events); } /// Inject an empty L1 block (no deposits) into the queue. pub(crate) fn inject_empty_block(&mut self, queue: &DepositQueue) { let header = self.next_header(); - queue.enqueue(header, L1PortalEvents::default(), vec![]); + queue.enqueue(header, L1PortalEvents::default()); } /// Inject `n` empty L1 blocks (no deposits) into the queue. @@ -3875,7 +3820,7 @@ impl L1Fixture { self.seed_regular_deposit_policy_state(header.inner.number, &deposits); let l1_deposits = deposits.into_iter().map(L1Deposit::Regular).collect(); let events = L1PortalEvents::from_deposits(l1_deposits); - queue.enqueue(header, events, vec![]); + queue.enqueue(header, events); } /// Inject an L1 block with mixed regular and encrypted deposits. @@ -3883,7 +3828,7 @@ impl L1Fixture { pub(crate) fn inject_l1_deposits(&mut self, queue: &DepositQueue, deposits: Vec) { let header = self.next_header(); let events = L1PortalEvents::from_deposits(deposits); - queue.enqueue(header, events, vec![]); + queue.enqueue(header, events); } /// Create an [`EncryptedDeposit`] for testing with dummy ECIES parameters. diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 5f6a81b5b..686ec2498 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -9,5 +9,4 @@ extern crate alloc; pub mod constants; mod header; -pub mod policy; pub use header::ZoneHeader; diff --git a/crates/primitives/src/policy.rs b/crates/primitives/src/policy.rs deleted file mode 100644 index 4a7b3c3a9..000000000 --- a/crates/primitives/src/policy.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! TIP-403 policy types shared between the zone node and the prover. -//! -//! These are extracted here so that `zone-precompiles` (which is `no_std`) can -//! reference `AuthRole` without pulling in the full `tempo-zone` dependency tree. - -/// Authorization role for TIP-403 policy checks. -/// -/// Determines which sub-policy is evaluated for compound policies. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum AuthRole { - /// Check both sender AND recipient. For compound policies, short-circuits on sender failure. - Transfer, - /// Check sender authorization only (compound: uses `senderPolicyId`). - Sender, - /// Check recipient authorization only (compound: uses `recipientPolicyId`). - Recipient, - /// Check mint recipient authorization only (compound: uses `mintRecipientPolicyId`). - MintRecipient, -}