use borsh::{BorshDeserialize, BorshSerialize}; use lee_core::{ account::AccountId, program::{PdaSeed, ProgramId}, }; use serde::{Deserialize, Serialize}; const PING_RECORD_SEED: [u8; 32] = *b"/LEZ/v0.3/PingRecord/0000000000/"; const SENDER_CONFIG_SEED: [u8; 32] = *b"/LEZ/v0.3/PingSenderCfg/0000000/"; const RECEIVER_CONFIG_SEED: [u8; 32] = *b"/LEZ/v0.3/PingReceiverCfg/00000/"; /// Raw 32-byte zone (channel) id, matching the inbox's. pub type ZoneId = [u8; 32]; /// Instruction to `ping_receiver`. /// /// Variants are append-only, for the same reason `SenderInstruction`'s are. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum ReceiverInstruction { /// Record the payload, delivered by the inbox on behalf of a peer source /// this receiver authorizes. /// /// Required accounts (3): the source marker, the receiver config PDA, then /// the record PDA. Record { payload: Vec }, /// Pins the deliverer and the peer sources it may deliver from, written once /// into a default config PDA at genesis. A re-run holding anything different /// is refused; an identical one is a no-op, which is what genesis replay does. /// /// Required accounts (1): the receiver config PDA. InitConfig(ReceiverConfig), } /// Who may deliver to this receiver, and which peer sources they may deliver from. /// /// `ping_receiver` holds nothing worth stealing, so this is not about value. It is /// about the record meaning something: without it any program on any configured /// peer can overwrite the record, and a delivery proves only that some peer sent /// it. #[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] pub struct ReceiverConfig { /// The program allowed to call `Record`: the cross-zone inbox. pub deliverer: ProgramId, /// The `(src_zone, src_program_id)` pairs a delivery may originate from. pub sources: Vec<(ZoneId, ProgramId)>, } impl ReceiverConfig { #[must_use] pub fn to_bytes(&self) -> Vec { borsh::to_vec(self).expect("receiver config serializes") } #[must_use] pub fn from_bytes(bytes: &[u8]) -> Option { borsh::from_slice(bytes).ok() } } /// Instruction to `ping_sender`. `Send`'s emission fields are forwarded verbatim /// into `cross_zone_outbox::Instruction::Emit`. /// /// Variants are append-only. risc0 serde encodes the variant as a bare leading /// tag word, so inserting one ahead of `Send` shifts every existing encoding. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum SenderInstruction { /// Emit a cross-zone message through the pinned outbox. /// /// Required accounts (2): the sender config PDA, then the outbox PDA. Send { target_zone: [u8; 32], target_program_id: ProgramId, target_accounts: Vec<[u8; 32]>, payload: Vec, ordinal: u32, }, /// Pins the outbox program, written once into a default config PDA at /// genesis. A re-run naming a different outbox is refused; an identical one /// is a no-op, which is what genesis replay does. /// /// Required accounts (1): the sender config PDA. InitConfig { outbox_program_id: ProgramId }, } /// The account a `ping_receiver` records the latest delivered payload into. #[must_use] pub fn ping_record_pda(receiver_id: ProgramId) -> AccountId { AccountId::for_public_pda(&receiver_id, &ping_record_seed()) } /// Seed of the record PDA, exposed so the guest can claim the account. #[must_use] pub const fn ping_record_seed() -> PdaSeed { PdaSeed::new(PING_RECORD_SEED) } /// PDA holding the outbox program id, seeded at genesis so the guest can pin the /// program it chains into without importing the outbox image id. #[must_use] pub fn sender_config_account_id(sender_id: ProgramId) -> AccountId { AccountId::for_public_pda(&sender_id, &sender_config_seed()) } #[must_use] pub const fn sender_config_seed() -> PdaSeed { PdaSeed::new(SENDER_CONFIG_SEED) } /// PDA holding the sources `ping_receiver` accepts a delivery from. #[must_use] pub fn receiver_config_account_id(receiver_id: ProgramId) -> AccountId { AccountId::for_public_pda(&receiver_id, &receiver_config_seed()) } #[must_use] pub const fn receiver_config_seed() -> PdaSeed { PdaSeed::new(RECEIVER_CONFIG_SEED) } /// Encodes the pinned outbox program id for the config account's data. #[must_use] pub fn outbox_bytes(outbox_program_id: ProgramId) -> [u8; 32] { let mut bytes = [0_u8; 32]; for (word, chunk) in outbox_program_id.iter().zip(bytes.chunks_exact_mut(4)) { chunk.copy_from_slice(&word.to_le_bytes()); } bytes } /// Decodes the pinned outbox program id from the config account's data. #[must_use] pub fn read_outbox(data: &[u8]) -> Option { if data.len() < 32 { return None; } let mut outbox_program_id = [0_u32; 8]; for (word, chunk) in outbox_program_id.iter_mut().zip(data[..32].chunks_exact(4)) { *word = u32::from_le_bytes(chunk.try_into().unwrap_or_else(|_| unreachable!())); } Some(outbox_program_id) } #[cfg(test)] mod tests { use super::*; /// `extract_emission` decodes `Send` off peer transactions, so its tag word is /// wire format: a variant inserted ahead of it would silently shift every /// existing encoding. #[test] fn send_is_the_first_variant() { let send = SenderInstruction::Send { target_zone: [7; 32], target_program_id: [1; 8], target_accounts: vec![], payload: vec![], ordinal: 0, }; let words = risc0_zkvm::serde::to_vec(&send).expect("Send serializes"); assert_eq!(words[0], 0); } /// `Record` is serialized by the source zone into the emission payload and /// decoded by the destination, so its tag word is wire format. #[test] fn record_is_the_first_variant() { let record = ReceiverInstruction::Record { payload: vec![] }; let words = risc0_zkvm::serde::to_vec(&record).expect("Record serializes"); assert_eq!(words[0], 0); } #[test] fn an_empty_receiver_config_does_not_decode() { assert_eq!(ReceiverConfig::from_bytes(&[]), None); } #[test] fn receiver_config_round_trips() { let config = ReceiverConfig { deliverer: [1; 8], sources: vec![([7; 32], [9; 8])], }; assert_eq!(ReceiverConfig::from_bytes(&config.to_bytes()), Some(config)); } #[test] fn outbox_id_round_trips() { let outbox: ProgramId = [9; 8]; assert_eq!(read_outbox(&outbox_bytes(outbox)), Some(outbox)); } }