mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-29 18:39:30 +00:00
feat(cross-zone): recognize multiple emitters via a shared extractor
This commit is contained in:
parent
5d77359fd8
commit
2c387899c4
@ -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<RwLock<HashSet<MessageKey>>>,
|
||||
}
|
||||
@ -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())),
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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<ProgramId>,
|
||||
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
|
||||
|
||||
@ -603,14 +603,16 @@ fn build_genesis_state(config: &SequencerConfig) -> (lee::V03State, Vec<LeeTrans
|
||||
let genesis_txs = config
|
||||
.genesis
|
||||
.iter()
|
||||
.map(|genesis_tx| match genesis_tx {
|
||||
.filter_map(|genesis_tx| match genesis_tx {
|
||||
GenesisAction::SupplyAccount {
|
||||
account_id,
|
||||
balance,
|
||||
} => 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<LeeTrans
|
||||
.map(LeeTransaction::Public)
|
||||
.collect();
|
||||
|
||||
// Seed bridge-lock holder balances directly: they are not produced by any tx.
|
||||
for action in &config.genesis {
|
||||
if let GenesisAction::SupplyBridgeLockHolding { holder, amount } = action {
|
||||
let (holder_id, account) = bridge_lock_core::build_holding_account(*holder, *amount);
|
||||
state.insert_genesis_account(holder_id, account);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed this zone's cross-zone inbox config so the inbox guest can authorize
|
||||
// inbound peer messages (zone-specific config, not produced by any tx).
|
||||
if let Some(cross_zone) = &config.cross_zone {
|
||||
|
||||
@ -13,7 +13,10 @@ serde = { workspace = true, features = ["alloc"] }
|
||||
risc0-zkvm.workspace = true
|
||||
borsh.workspace = true
|
||||
lee = { workspace = true, optional = true }
|
||||
ping_core = { workspace = true, optional = true }
|
||||
bridge_lock_core = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
# Host-only transaction builder; pulls `lee`, so the risc0 guest builds without it.
|
||||
host = ["dep:lee"]
|
||||
# Host-only transaction builder and emission extractor; pull `lee` and the
|
||||
# emitter cores, so the risc0 guest builds without them.
|
||||
host = ["dep:lee", "dep:ping_core", "dep:bridge_lock_core"]
|
||||
|
||||
@ -200,6 +200,63 @@ pub fn build_inbox_dispatch_tx(
|
||||
)
|
||||
}
|
||||
|
||||
/// The cross-zone emission fields a watcher or verifier reads off a source
|
||||
/// transaction, common to every emitter program.
|
||||
#[cfg(feature = "host")]
|
||||
pub struct Emission {
|
||||
pub target_zone: ZoneId,
|
||||
pub target_program_id: ProgramId,
|
||||
pub target_accounts: Vec<[u8; 32]>,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// 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<Emission> {
|
||||
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).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user