//! Host-side cross-zone helpers that need program ids (`programs`) or the state //! machine (`lee`), kept out of the guest-pure cores. Mirrors `system_accounts`: //! it resolves builtin program ids and bakes them into transactions and genesis //! accounts for the watcher (sequencer) and verifier (indexer). //! //! This crate is the reference LEZ-to-LEZ adapter: it re-derives each delivery //! byte-for-byte from a peer LEZ zone's finalized blocks, valid only because the //! peer runs identical LEZ code. A non-LEZ peer needs a separate adapter with its //! own block-reading, emission-extraction, delivery-building, and trust model; a //! shared trait is best lifted from that first real adapter, not from this one. use std::collections::BTreeMap; pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer}; use cross_zone_inbox_core::{ CrossZoneMessage, InboxConfig, Instruction, ZoneId, inbox_config_account_id, inbox_seen_shard_account_id, }; use lee::program::Program; use lee_core::{ account::{Account, AccountId}, program::ProgramId, }; use serde::{Deserialize, Serialize}; /// The cross-zone emission fields a watcher or verifier reads off a source /// transaction, common to every emitter program. pub struct Emission { pub target_zone: ZoneId, pub target_program_id: ProgramId, pub target_accounts: Vec<[u8; 32]>, pub payload: Vec, } /// A cross-zone builtin a zone deploys at genesis via `DeployProgram`. /// /// Keeps the cross-zone programs out of the production builtin set. A zone lists /// the ones it uses (a sender needs the outbox and its emitter, a receiver the /// inbox and its targets); [`CrossZoneProgram::ALL`] deploys the whole set. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CrossZoneProgram { Outbox, Inbox, PingSender, PingReceiver, BridgeLock, WrappedToken, } impl CrossZoneProgram { /// Every cross-zone builtin, for a zone that participates in all flows. pub const ALL: [Self; 6] = [ Self::Outbox, Self::Inbox, Self::PingSender, Self::PingReceiver, Self::BridgeLock, Self::WrappedToken, ]; /// Resolves to the builtin program to register in genesis state. #[must_use] pub const fn program(self) -> Program { match self { Self::Outbox => programs::cross_zone_outbox(), Self::Inbox => programs::cross_zone_inbox(), Self::PingSender => programs::ping_sender(), Self::PingReceiver => programs::ping_receiver(), Self::BridgeLock => programs::bridge_lock(), Self::WrappedToken => programs::wrapped_token(), } } } /// Whether a program may only be invoked by sequencer-origin transactions. /// /// The cross-zone inbox is injected solely by the watcher; a user-submitted call /// must be rejected at ingress, since `TransactionOrigin` is not carried in the /// block. #[must_use] pub fn is_sequencer_only_program(program_id: ProgramId) -> bool { program_id == programs::cross_zone_inbox().id() } /// Extracts the cross-zone emission from a source transaction. /// /// Recognizes the known emitter programs (`ping_sender`, `bridge_lock`). The /// watcher and verifier both use this so they agree on what a given source tx /// emits. #[must_use] pub fn extract_emission(program_id: ProgramId, instruction_data: &[u32]) -> Option { if program_id == programs::ping_sender().id() { let ping_core::SenderInstruction::Send { target_zone, target_program_id, target_accounts, payload, .. } = risc0_zkvm::serde::from_slice(instruction_data).ok()?; Some(Emission { target_zone, target_program_id, target_accounts, payload, }) } else if program_id == programs::bridge_lock().id() { let bridge_lock_core::Instruction::Lock { target_zone, target_program_id, target_accounts, payload, .. } = risc0_zkvm::serde::from_slice(instruction_data).ok()?; Some(Emission { target_zone, target_program_id, target_accounts, payload, }) } else { None } } /// Builds the sequencer-origin dispatch transaction. Pure for fixed inputs, so /// the watcher's injected tx and the indexer's re-derived tx are byte-identical. fn build_inbox_dispatch_tx( inbox_id: ProgramId, msg: &CrossZoneMessage, target_account_ids: Vec, ) -> lee::PublicTransaction { let mut account_ids = Vec::with_capacity(target_account_ids.len().saturating_add(2)); account_ids.push(inbox_config_account_id(inbox_id)); account_ids.push(inbox_seen_shard_account_id( inbox_id, &msg.src_zone, msg.src_block_id, )); account_ids.extend(target_account_ids); let message = lee::public_transaction::Message::try_new( inbox_id, account_ids, vec![], Instruction::Dispatch(msg.clone()), ) .expect("inbox dispatch instruction must serialize"); lee::PublicTransaction::new( message, lee::public_transaction::WitnessSet::from_raw_parts(vec![]), ) } /// Builds the dispatch transaction for one peer emission. /// /// Both the sequencer's watcher and the indexer's verifier go through this so /// their transactions are byte-identical for the same emission (the basis of the /// Option B check). #[must_use] pub fn build_dispatch_from_emission( src_zone: ZoneId, src_block_id: u64, src_tx_index: u32, src_program_id: ProgramId, target_program_id: ProgramId, target_accounts: &[[u8; 32]], payload: Vec, ) -> lee::PublicTransaction { let msg = CrossZoneMessage { src_zone, src_block_id, src_tx_index, src_program_id, target_program_id, payload, l1_inclusion_witness: None, }; let target_ids = target_accounts .iter() .copied() .map(AccountId::new) .collect(); build_inbox_dispatch_tx(programs::cross_zone_inbox().id(), &msg, target_ids) } /// Builds the inbox config account a zone seeds into genesis state. /// /// Lets the inbox guest authorize inbound peer messages. The sequencer and /// indexer seed the same account from the same config, keeping their replayed /// state consistent. #[must_use] pub fn build_inbox_config_account( self_zone: ZoneId, cross_zone: &CrossZoneConfig, ) -> (AccountId, Account) { let inbox_id = programs::cross_zone_inbox().id(); let mut allowed_targets = BTreeMap::new(); for peer in &cross_zone.peers { allowed_targets.insert(peer.channel_id, peer.allowed_targets.clone()); } let config = InboxConfig { self_zone, allowed_peers: BTreeMap::new(), allowed_targets, }; let account = Account { program_owner: inbox_id, balance: 0, data: config .to_bytes() .try_into() .expect("inbox config fits in account data"), nonce: 0_u128.into(), }; (inbox_config_account_id(inbox_id), account) } /// Builds the genesis holding account funding a holder's bridgeable balance. /// /// Owned by `bridge_lock`, data is the LE balance. Not produced by any /// transaction, so the sequencer and indexer both seed it through this one /// builder. #[must_use] pub fn build_holding_account(holder: AccountId, amount: u128) -> (AccountId, Account) { let account = Account { program_owner: programs::bridge_lock().id(), data: bridge_lock_core::balance_bytes(amount) .to_vec() .try_into() .expect("balance fits in account data"), ..Default::default() }; (holder, account) } /// Resolves a list of [`CrossZoneProgram`] selectors to the builtin programs to /// register in genesis state. #[must_use] pub fn deployed_programs(programs: &[CrossZoneProgram]) -> Vec { programs.iter().map(|p| p.program()).collect() } /// The wrapped-token config account, pinning the cross-zone inbox as the /// authorized minter without importing the inbox id into the guest. Fixed for /// every zone, part of the cross-zone genesis setup. fn wrapped_token_config_account() -> (AccountId, Account) { let wrapped_token_id = programs::wrapped_token().id(); ( wrapped_token_core::config_account_id(wrapped_token_id), Account { program_owner: wrapped_token_id, data: wrapped_token_core::minter_bytes(programs::cross_zone_inbox().id()) .to_vec() .try_into() .expect("minter id fits in account data"), ..Default::default() }, ) } /// The receiving-side cross-zone genesis accounts a zone seeds. /// /// The wrapped-token config and this zone's inbox config, for a zone that /// receives (one with `cross_zone` config). Bridge-lock holdings are seeded /// separately via [`build_holding_account`], since they belong to the locking /// (source) side and must not depend on receiving config. #[must_use] pub fn genesis_accounts(self_zone: ZoneId, config: &CrossZoneConfig) -> Vec<(AccountId, Account)> { vec![ wrapped_token_config_account(), build_inbox_config_account(self_zone, config), ] }