278 lines
9.3 KiB
Rust

//! 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, Balance},
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<u8>,
}
/// 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<Emission> {
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<AccountId>,
) -> 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<u8>,
) -> 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: Balance) -> (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<Program> {
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),
]
}