pub use lee_core::program::PdaSeed; use lee_core::{account::AccountId, program::ProgramId}; use serde::{Deserialize, Serialize}; const BRIDGE_SEED_DOMAIN_SEPARATOR: [u8; 32] = *b"/LEZ/v0.3/BridgeSeed/0000000000/"; const DEPOSIT_RECEIPT_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/BridgeDepositReceipt/0"; #[derive(Serialize, Deserialize)] pub enum Instruction { /// Transfers native tokens from the bridge PDA account to a recipient vault, /// exactly once per `l1_deposit_op_id`. /// /// Required accounts (3): /// - Bridge PDA account /// - Recipient vault PDA account /// - Deposit-receipt PDA account, derived from `l1_deposit_op_id`. Its existence records that /// this op id was already minted; a second application of the same op id finds it present and /// transfers nothing. Deposit { /// Deposit OP ID from L1, stored here to pin each [`Deposit`](Instruction::Deposit) to a /// Deposit Event on L1. l1_deposit_op_id: [u8; 32], vault_program_id: ProgramId, recipient_id: AccountId, amount: u64, }, /// Transfers native tokens from a user account to the bridge PDA account. /// /// Required accounts (2): /// - Sender account /// - Bridge PDA account /// /// `bedrock_account_pk` is consumed by the Sequencer and is not used by the Bridge program /// logic. Withdraw { amount: u64, bedrock_account_pk: [u8; 32], }, } #[must_use] pub const fn compute_bridge_seed() -> PdaSeed { PdaSeed::new(BRIDGE_SEED_DOMAIN_SEPARATOR) } #[must_use] pub fn compute_bridge_account_id(bridge_program_id: ProgramId) -> AccountId { AccountId::for_public_pda(&bridge_program_id, &compute_bridge_seed()) } /// Seed of the deposit-receipt PDA for `l1_deposit_op_id`, exposed so the guest /// can claim the account. Domain-separated from [`compute_bridge_seed`]. #[must_use] pub fn deposit_receipt_seed(l1_deposit_op_id: [u8; 32]) -> PdaSeed { use risc0_zkvm::sha::{Impl, Sha256 as _}; let mut bytes = [0_u8; 64]; bytes[..32].copy_from_slice(&DEPOSIT_RECEIPT_SEED_DOMAIN); bytes[32..].copy_from_slice(&l1_deposit_op_id); let seed: [u8; 32] = Impl::hash_bytes(&bytes) .as_bytes() .try_into() .unwrap_or_else(|_| unreachable!()); PdaSeed::new(seed) } /// The deposit-receipt PDA whose existence marks `l1_deposit_op_id` as minted. #[must_use] pub fn deposit_receipt_account_id( bridge_program_id: ProgramId, l1_deposit_op_id: [u8; 32], ) -> AccountId { AccountId::for_public_pda(&bridge_program_id, &deposit_receipt_seed(l1_deposit_op_id)) } #[cfg(test)] mod tests { use super::*; const BRIDGE_ID: ProgramId = [7; 8]; #[test] fn receipt_id_is_deterministic_per_op_id() { let op = [3_u8; 32]; assert_eq!( deposit_receipt_account_id(BRIDGE_ID, op), deposit_receipt_account_id(BRIDGE_ID, op) ); } #[test] fn distinct_op_ids_and_domains_do_not_collide() { let a = deposit_receipt_account_id(BRIDGE_ID, [1; 32]); let b = deposit_receipt_account_id(BRIDGE_ID, [2; 32]); assert_ne!(a, b, "different op ids must derive different receipts"); // The op-id-derived seed must not alias the plain bridge PDA, even if an // op id ever equals the bridge seed's raw bytes. assert_ne!( deposit_receipt_account_id(BRIDGE_ID, *compute_bridge_seed().as_bytes()), compute_bridge_account_id(BRIDGE_ID) ); } }