From 2c387899c49a0a54accd8460fda3dbae7ec206b5 Mon Sep 17 00:00:00 2001 From: moudyellaz Date: Wed, 24 Jun 2026 10:55:06 +0200 Subject: [PATCH] feat(cross-zone): recognize multiple emitters via a shared extractor --- lez/indexer/core/src/cross_zone_verifier.rs | 50 ++++++++--------- lez/sequencer/core/Cargo.toml | 1 + lez/sequencer/core/src/config.rs | 5 ++ lez/sequencer/core/src/cross_zone_watcher.rs | 59 +++++++++----------- lez/sequencer/core/src/lib.rs | 16 +++++- programs/cross_zone_inbox/core/Cargo.toml | 7 ++- programs/cross_zone_inbox/core/src/lib.rs | 57 +++++++++++++++++++ 7 files changed, 133 insertions(+), 62 deletions(-) diff --git a/lez/indexer/core/src/cross_zone_verifier.rs b/lez/indexer/core/src/cross_zone_verifier.rs index 6514ed87..cf0cc4d4 100644 --- a/lez/indexer/core/src/cross_zone_verifier.rs +++ b/lez/indexer/core/src/cross_zone_verifier.rs @@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, bail}; use common::{block::Block, transaction::LeeTransaction}; use cross_zone_inbox_core::{ CrossZoneMessage, Instruction as InboxInstruction, MessageKey, ZoneId, - build_dispatch_from_emission, message_key, + build_dispatch_from_emission, extract_emission, message_key, }; use futures::StreamExt as _; use lee::program::Program; @@ -18,7 +18,6 @@ use logos_blockchain_core::mantle::ops::channel::ChannelId; use logos_blockchain_zone_sdk::{ CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer, }; -use ping_core::SenderInstruction; use tokio::sync::RwLock; use crate::config::IndexerConfig; @@ -61,7 +60,8 @@ impl PeerBlocks { pub struct CrossZoneVerifier { self_zone: ZoneId, inbox_id: ProgramId, - emitter_id: ProgramId, + ping_sender_id: ProgramId, + bridge_lock_id: ProgramId, peers: PeerBlocks, seen: Arc>>, } @@ -90,7 +90,8 @@ impl CrossZoneVerifier { Some(Self { self_zone, inbox_id: Program::cross_zone_inbox().id(), - emitter_id: Program::ping_sender().id(), + ping_sender_id: Program::ping_sender().id(), + bridge_lock_id: Program::bridge_lock().id(), peers, seen: Arc::new(RwLock::new(HashSet::new())), }) @@ -161,7 +162,7 @@ impl CrossZoneVerifier { ) })?; - let emission = peer_block + let emission_tx = peer_block .body .transactions .get(msg.src_tx_index as usize) @@ -169,23 +170,21 @@ impl CrossZoneVerifier { anyhow::anyhow!("src_tx_index {} out of range in peer block", msg.src_tx_index) })?; - let LeeTransaction::Public(emission) = emission else { + let LeeTransaction::Public(emission_tx) = emission_tx else { bail!("peer emission transaction is not public"); }; - if emission.message().program_id != self.emitter_id { - bail!("peer transaction at src_tx_index is not an emitter transaction"); - } + let message = emission_tx.message(); + let emission = extract_emission( + message.program_id, + &message.instruction_data, + self.ping_sender_id, + self.bridge_lock_id, + ) + .ok_or_else(|| { + anyhow::anyhow!("peer transaction at src_tx_index is not a recognized emitter") + })?; - let SenderInstruction::Send { - target_zone, - target_program_id, - target_accounts, - payload, - .. - } = risc0_zkvm::serde::from_slice(&emission.message().instruction_data) - .context("decode peer emission instruction")?; - - if target_zone != self.self_zone { + if emission.target_zone != self.self_zone { bail!("peer emission targets a different zone"); } @@ -194,10 +193,10 @@ impl CrossZoneVerifier { msg.src_zone, msg.src_block_id, msg.src_tx_index, - self.emitter_id, - target_program_id, - &target_accounts, - payload, + message.program_id, + emission.target_program_id, + &emission.target_accounts, + emission.payload, )) } @@ -269,7 +268,7 @@ mod tests { program::Program, public_transaction::{Message, WitnessSet}, }; - use ping_core::ping_record_pda; + use ping_core::{SenderInstruction, ping_record_pda}; use super::*; @@ -281,7 +280,8 @@ mod tests { CrossZoneVerifier { self_zone: SELF_ZONE, inbox_id: Program::cross_zone_inbox().id(), - emitter_id: Program::ping_sender().id(), + ping_sender_id: Program::ping_sender().id(), + bridge_lock_id: Program::bridge_lock().id(), peers: PeerBlocks::default(), seen: Arc::new(RwLock::new(HashSet::new())), } diff --git a/lez/sequencer/core/Cargo.toml b/lez/sequencer/core/Cargo.toml index 09fef127..568319d6 100644 --- a/lez/sequencer/core/Cargo.toml +++ b/lez/sequencer/core/Cargo.toml @@ -20,6 +20,7 @@ bridge_core.workspace = true vault_core.workspace = true cross_zone_inbox_core = { workspace = true, features = ["host"] } ping_core.workspace = true +bridge_lock_core = { workspace = true, features = ["host"] } logos-blockchain-key-management-system-service.workspace = true logos-blockchain-core.workspace = true diff --git a/lez/sequencer/core/src/config.rs b/lez/sequencer/core/src/config.rs index ce686676..e554db1f 100644 --- a/lez/sequencer/core/src/config.rs +++ b/lez/sequencer/core/src/config.rs @@ -25,6 +25,11 @@ pub enum GenesisAction { SupplyBridgeAccount { balance: u128, }, + /// Seeds a bridge-lock holder's initial bridgeable balance into genesis state. + SupplyBridgeLockHolding { + holder: AccountId, + amount: u128, + }, } pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer}; diff --git a/lez/sequencer/core/src/cross_zone_watcher.rs b/lez/sequencer/core/src/cross_zone_watcher.rs index 8a176f3b..2885ae48 100644 --- a/lez/sequencer/core/src/cross_zone_watcher.rs +++ b/lez/sequencer/core/src/cross_zone_watcher.rs @@ -1,7 +1,7 @@ use std::time::Duration; use common::{block::Block, transaction::LeeTransaction}; -use cross_zone_inbox_core::build_dispatch_from_emission; +use cross_zone_inbox_core::{build_dispatch_from_emission, extract_emission}; use futures::StreamExt as _; use lee::program::Program; use lee_core::program::ProgramId; @@ -11,7 +11,6 @@ use logos_blockchain_zone_sdk::{ CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer, }; use mempool::MemPoolHandle; -use ping_core::SenderInstruction; use crate::{ TransactionOrigin, @@ -30,7 +29,8 @@ pub fn spawn_watchers( ) { let self_zone: [u8; 32] = *bedrock_config.channel_id.as_ref(); let inbox_id = Program::cross_zone_inbox().id(); - let emitter_id = Program::ping_sender().id(); + let ping_sender_id = Program::ping_sender().id(); + let bridge_lock_id = Program::bridge_lock().id(); for peer in cross_zone.peers.clone() { let node = NodeHttpClient::new( @@ -43,7 +43,8 @@ pub fn spawn_watchers( peer.allowed_targets, self_zone, inbox_id, - emitter_id, + ping_sender_id, + bridge_lock_id, poll_interval, mempool_handle.clone(), )); @@ -60,7 +61,8 @@ async fn watch_peer( allowed_targets: Vec, self_zone: [u8; 32], inbox_id: ProgramId, - emitter_id: ProgramId, + ping_sender_id: ProgramId, + bridge_lock_id: ProgramId, poll_interval: Duration, mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>, ) { @@ -93,7 +95,8 @@ async fn watch_peer( peer_zone, self_zone, inbox_id, - emitter_id, + ping_sender_id, + bridge_lock_id, &allowed_targets, &mempool_handle, ) @@ -110,16 +113,17 @@ async fn watch_peer( } /// Scans one peer block for outbound messages and injects a dispatch per match. -/// -/// Option A (M3): the watcher recognizes the demo emitter and reads the outbound -/// message straight off its instruction. M4 replaces this with re-derivation -/// from the outbox PDA write, which removes the emitter-specific decoding. +#[expect( + clippy::too_many_arguments, + reason = "Each parameter is an independent piece of per-block delivery state" +)] async fn deliver_block( block: &Block, peer_zone: [u8; 32], self_zone: [u8; 32], inbox_id: ProgramId, - emitter_id: ProgramId, + ping_sender_id: ProgramId, + bridge_lock_id: ProgramId, allowed_targets: &[ProgramId], mempool_handle: &MemPoolHandle<(TransactionOrigin, LeeTransaction)>, ) { @@ -128,28 +132,19 @@ async fn deliver_block( continue; }; let message = public_tx.message(); - if message.program_id != emitter_id { + let Some(emission) = extract_emission( + message.program_id, + &message.instruction_data, + ping_sender_id, + bridge_lock_id, + ) else { continue; - } - - let SenderInstruction::Send { - target_zone, - target_program_id, - target_accounts, - payload, - .. - } = match risc0_zkvm::serde::from_slice(&message.instruction_data) { - Ok(send) => send, - Err(err) => { - warn!("Watcher could not decode emitter instruction: {err}"); - continue; - } }; - if target_zone != self_zone { + if emission.target_zone != self_zone { continue; } - if !allowed_targets.contains(&target_program_id) { + if !allowed_targets.contains(&emission.target_program_id) { warn!( "Watcher dropping message to disallowed target from peer {}", hex::encode(peer_zone) @@ -162,10 +157,10 @@ async fn deliver_block( peer_zone, block.header.block_id, u32::try_from(index).unwrap_or(u32::MAX), - emitter_id, - target_program_id, - &target_accounts, - payload, + message.program_id, + emission.target_program_id, + &emission.target_accounts, + emission.payload, ); match mempool_handle diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index 69c596e3..7fe2c113 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -603,14 +603,16 @@ fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec build_supply_account_genesis_transaction(account_id, *balance), + } => Some(build_supply_account_genesis_transaction(account_id, *balance)), GenesisAction::SupplyBridgeAccount { balance } => { - build_supply_bridge_account_genesis_transaction(*balance) + Some(build_supply_bridge_account_genesis_transaction(*balance)) } + // Force-inserted below: bridge_lock has no mint transaction. + GenesisAction::SupplyBridgeLockHolding { .. } => None, }) .chain(std::iter::once(clock_invocation(0))) .inspect(|tx| { @@ -621,6 +623,14 @@ fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec, + pub payload: Vec, +} + +/// Extracts the cross-zone emission from a source transaction, recognizing the +/// known emitter programs. Returns `None` for any other program. The watcher and +/// verifier both use this so they agree on what a given source tx emits. +/// +/// Option A: each emitter is decoded explicitly. The principled alternative is to +/// read the outbox PDA write, which would need re-execution of the source tx. +#[cfg(feature = "host")] +#[must_use] +pub fn extract_emission( + program_id: ProgramId, + instruction_data: &[u32], + ping_sender_id: ProgramId, + bridge_lock_id: ProgramId, +) -> Option { + if program_id == 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 == 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 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).