diff --git a/programs/bridge_lock/core/Cargo.toml b/programs/bridge_lock/core/Cargo.toml deleted file mode 100644 index 2c9e2f58..00000000 --- a/programs/bridge_lock/core/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "bridge_lock_core" -version = "0.1.0" -edition = "2024" -license = { workspace = true } - -[lints] -workspace = true - -[dependencies] -lee_core.workspace = true -serde = { workspace = true, features = ["alloc"] } -lee = { workspace = true, optional = true } - -[features] -# Host-only genesis helper; pulls `lee`, so the risc0 guest builds without it. -host = ["dep:lee"] diff --git a/programs/bridge_lock/core/src/lib.rs b/programs/bridge_lock/core/src/lib.rs deleted file mode 100644 index eae1d681..00000000 --- a/programs/bridge_lock/core/src/lib.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Core types for the bridge-lock program, the source side of the cross-zone -//! token bridge. A holder locks part of their balance into an escrow and emits a -//! cross-zone message minting the wrapped token on the target zone. - -use lee_core::{ - account::AccountId, - program::{PdaSeed, ProgramId}, -}; -use serde::{Deserialize, Serialize}; - -const ESCROW_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/BridgeLockEscrow/0000/"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum Instruction { - /// Lock `amount` of the holder's balance and emit a cross-zone message - /// minting the wrapped token on `target_zone`. The emission fields mirror - /// `cross_zone_outbox::Instruction::Emit` so the watcher reads them directly. - /// - /// Required accounts (3): holder holding (authorized), escrow PDA, outbox PDA. - Lock { - amount: u128, - target_zone: [u8; 32], - target_program_id: ProgramId, - target_accounts: Vec<[u8; 32]>, - payload: Vec, - outbox_program_id: ProgramId, - ordinal: u32, - }, -} - -/// PDA accumulating all locked balance on this zone. -#[must_use] -pub fn escrow_account_id(bridge_lock_id: ProgramId) -> AccountId { - AccountId::for_public_pda(&bridge_lock_id, &escrow_seed()) -} - -#[must_use] -pub fn escrow_seed() -> PdaSeed { - PdaSeed::new(ESCROW_SEED_DOMAIN) -} - -/// Reads a bridgeable balance from account data; empty data is a zero balance. -#[must_use] -pub fn read_balance(data: &[u8]) -> u128 { - if data.len() < 16 { - return 0; - } - u128::from_le_bytes(data[..16].try_into().unwrap_or_else(|_| unreachable!())) -} - -#[must_use] -pub fn balance_bytes(amount: u128) -> [u8; 16] { - amount.to_le_bytes() -} - -/// Builds the genesis holding account funding a holder's bridgeable balance: -/// owned by bridge_lock, data is the LE balance, at the holder's account id. It -/// is not produced by any transaction, so the sequencer and the indexer both -/// seed it through this one builder to keep their genesis states identical. -#[cfg(feature = "host")] -#[must_use] -pub fn build_holding_account( - holder: AccountId, - amount: u128, -) -> (AccountId, lee_core::account::Account) { - let account = lee_core::account::Account { - program_owner: lee::program::Program::bridge_lock().id(), - data: balance_bytes(amount) - .to_vec() - .try_into() - .expect("balance fits in account data"), - ..Default::default() - }; - (holder, account) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn balance_round_trips() { - assert_eq!(read_balance(&balance_bytes(7)), 7); - assert_eq!(read_balance(&[]), 0); - } - - #[test] - fn escrow_is_stable() { - let id: ProgramId = [4; 8]; - assert_eq!(escrow_account_id(id), escrow_account_id(id)); - } -} diff --git a/programs/cross_zone_inbox/core/Cargo.toml b/programs/cross_zone_inbox/core/Cargo.toml deleted file mode 100644 index 374a287d..00000000 --- a/programs/cross_zone_inbox/core/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "cross_zone_inbox_core" -version = "0.1.0" -edition = "2024" -license = { workspace = true } - -[lints] -workspace = true - -[dependencies] -lee_core.workspace = true -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 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"] diff --git a/programs/cross_zone_inbox/core/src/lib.rs b/programs/cross_zone_inbox/core/src/lib.rs deleted file mode 100644 index edb90424..00000000 --- a/programs/cross_zone_inbox/core/src/lib.rs +++ /dev/null @@ -1,378 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use lee_core::{ - account::AccountId, - program::{PdaSeed, ProgramId}, -}; -use serde::{Deserialize, Serialize}; - -/// Raw 32-byte zone (channel) id; the host maps it to the zone-sdk `ChannelId`. -pub type ZoneId = [u8; 32]; - -/// Block-signing public key pinned per peer zone. -pub type ExpectedPubkey = [u8; 32]; - -/// Content-addressed replay key for a delivered message. -pub type MessageKey = [u8; 32]; - -/// Source blocks per seen-set shard, so no single seen account grows without bound. -pub const EPOCH_BLOCKS: u64 = 10_000; - -const MESSAGE_KEY_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneMsgKey/00000/"; -const INBOX_CONFIG_SEED: [u8; 32] = *b"/LEZ/v0.3/CrossZoneInboxCfg/000/"; -const INBOX_SEEN_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneInboxSeen/00/"; - -/// A peer zone whose outbox a zone watches for inbound cross-zone messages. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CrossZonePeer { - /// The peer's Bedrock channel; its 32 bytes double as the peer's zone id. - pub channel_id: ZoneId, - /// Programs on the local zone a message from this peer is allowed to target. - pub allowed_targets: Vec, - /// The peer's block-signing public key, pinned to reject blocks inscribed by - /// anyone other than that zone's sequencer. `None` skips the check (the - /// channel signer is still authenticated by the zone-sdk). - #[serde(default)] - pub expected_block_signing_pubkey: Option<[u8; 32]>, -} - -/// Cross-zone configuration shared by a zone's sequencer (watcher) and indexer -/// (verifier): the peers it reads from Bedrock and, per peer, the local programs -/// they may deliver to. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CrossZoneConfig { - pub peers: Vec, -} - -/// A finalized outbound message observed on a peer zone, addressed to a program -/// on this zone. The watcher fills it from the peer's block; it is never -/// self-reported by a user. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct CrossZoneMessage { - pub src_zone: ZoneId, - pub src_block_id: u64, - pub src_tx_index: u32, - pub src_program_id: ProgramId, - pub target_program_id: ProgramId, - pub payload: Vec, - /// Reserved for a future source-state proof; MUST be `None` in v1. - pub l1_inclusion_witness: Option>, -} - -/// Peer and per-peer target allowlists, plus this inbox's own zone id. -#[derive( - Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize, -)] -pub struct InboxConfig { - pub self_zone: ZoneId, - pub allowed_peers: BTreeMap, - pub allowed_targets: BTreeMap>, -} - -impl InboxConfig { - /// Borsh-encoded form stored in the inbox config account. - #[must_use] - pub fn to_bytes(&self) -> Vec { - borsh::to_vec(self).expect("InboxConfig serializes") - } - - /// Decodes an [`InboxConfig`] from account data. - pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result { - borsh::from_slice(bytes) - } -} - -/// The replay keys seen for one `(src_zone, epoch)` shard. -#[derive(Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct SeenShard(pub BTreeSet); - -impl SeenShard { - /// Decodes a shard from account data; empty data is an empty shard. - pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result { - if bytes.is_empty() { - return Ok(Self::default()); - } - borsh::from_slice(bytes) - } - - #[must_use] - pub fn to_bytes(&self) -> Vec { - borsh::to_vec(self).expect("SeenShard serializes") - } - - #[must_use] - pub fn contains(&self, key: &MessageKey) -> bool { - self.0.contains(key) - } - - /// Inserts a key; returns true if it was newly inserted. - pub fn insert(&mut self, key: MessageKey) -> bool { - self.0.insert(key) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum Instruction { - /// Delivers a finalized peer message to its target program. - Dispatch(CrossZoneMessage), -} - -/// Content-addressed replay key: `(src_zone, src_block_id, src_tx_index)` hashed -/// under a domain separator. Watcher-independent and immune to proof -/// malleability, since it keys on block id plus index rather than a tx hash. -#[must_use] -pub fn message_key(src_zone: &ZoneId, src_block_id: u64, src_tx_index: u32) -> MessageKey { - use risc0_zkvm::sha::{Impl, Sha256 as _}; - - let mut bytes = Vec::with_capacity(MESSAGE_KEY_DOMAIN.len() + 32 + 8 + 4); - bytes.extend_from_slice(&MESSAGE_KEY_DOMAIN); - bytes.extend_from_slice(src_zone); - bytes.extend_from_slice(&src_block_id.to_le_bytes()); - bytes.extend_from_slice(&src_tx_index.to_le_bytes()); - - Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .unwrap_or_else(|_| unreachable!()) -} - -/// The config account holding the allowlists. -#[must_use] -pub fn inbox_config_account_id(inbox_id: ProgramId) -> AccountId { - AccountId::for_public_pda(&inbox_id, &PdaSeed::new(INBOX_CONFIG_SEED)) -} - -/// The seen-set shard for the `(src_zone, epoch)` the message falls in. -#[must_use] -pub fn inbox_seen_shard_account_id( - inbox_id: ProgramId, - src_zone: &ZoneId, - src_block_id: u64, -) -> AccountId { - AccountId::for_public_pda(&inbox_id, &inbox_seen_shard_seed(src_zone, src_block_id)) -} - -/// Seed of the seen-shard PDA, exposed so the guest can claim the account. -#[must_use] -pub fn inbox_seen_shard_seed(src_zone: &ZoneId, src_block_id: u64) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256 as _}; - - let src_epoch = src_block_id / EPOCH_BLOCKS; - let mut bytes = Vec::with_capacity(INBOX_SEEN_SEED_DOMAIN.len() + 32 + 8); - bytes.extend_from_slice(&INBOX_SEEN_SEED_DOMAIN); - bytes.extend_from_slice(src_zone); - bytes.extend_from_slice(&src_epoch.to_le_bytes()); - - let seed: [u8; 32] = Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .unwrap_or_else(|_| unreachable!()); - PdaSeed::new(seed) -} - -/// Builds the sequencer-origin dispatch transaction. Pure, so the watcher's -/// injected tx and the indexer's re-derived tx are byte-identical for the same -/// inputs (the basis of the Option B check). `target_account_ids` are the -/// inbox's chained-call targets; deriving them is target-specific. -#[cfg(feature = "host")] -#[must_use] -pub fn build_inbox_dispatch_tx( - inbox_id: ProgramId, - msg: &CrossZoneMessage, - target_account_ids: Vec, -) -> lee::PublicTransaction { - let mut account_ids = Vec::with_capacity(2 + target_account_ids.len()); - 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![]), - ) -} - -/// 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, -} - -/// 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). -#[cfg(feature = "host")] -#[must_use] -pub fn build_dispatch_from_emission( - inbox_id: ProgramId, - 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, -) -> 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(inbox_id, &msg, target_ids) -} - -/// Builds the inbox config account a zone seeds into genesis state so the inbox -/// guest can authorize inbound peer messages. The sequencer and indexer seed the -/// same account from the same config, keeping their replayed state consistent. -#[cfg(feature = "host")] -#[must_use] -pub fn build_inbox_config_account( - self_zone: ZoneId, - cross_zone: &CrossZoneConfig, -) -> (AccountId, lee_core::account::Account) { - let inbox_id = lee::program::Program::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 = lee_core::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) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn zone(b: u8) -> ZoneId { - [b; 32] - } - - #[test] - fn message_key_is_stable_and_content_addressed() { - assert_eq!(message_key(&zone(1), 7, 3), message_key(&zone(1), 7, 3)); - assert_ne!(message_key(&zone(1), 7, 3), message_key(&zone(2), 7, 3)); - assert_ne!(message_key(&zone(1), 7, 3), message_key(&zone(1), 8, 3)); - assert_ne!(message_key(&zone(1), 7, 3), message_key(&zone(1), 7, 4)); - } - - #[test] - fn seen_shards_split_on_epoch_boundary() { - let id: ProgramId = [9; 8]; - assert_eq!( - inbox_seen_shard_account_id(id, &zone(1), 0), - inbox_seen_shard_account_id(id, &zone(1), EPOCH_BLOCKS - 1), - ); - assert_ne!( - inbox_seen_shard_account_id(id, &zone(1), EPOCH_BLOCKS - 1), - inbox_seen_shard_account_id(id, &zone(1), EPOCH_BLOCKS), - ); - } - - #[cfg(feature = "host")] - #[test] - fn build_inbox_dispatch_tx_is_deterministic() { - let inbox: ProgramId = [5; 8]; - let msg = CrossZoneMessage { - src_zone: zone(1), - src_block_id: 42, - src_tx_index: 2, - src_program_id: [6; 8], - target_program_id: [7; 8], - payload: vec![1, 2, 3, 4], - l1_inclusion_witness: None, - }; - let targets = vec![AccountId::new([8; 32]), AccountId::new([9; 32])]; - - let tx1 = build_inbox_dispatch_tx(inbox, &msg, targets.clone()); - let tx2 = build_inbox_dispatch_tx(inbox, &msg, targets); - assert_eq!(tx1, tx2); - } -} diff --git a/programs/cross_zone_outbox/core/Cargo.toml b/programs/cross_zone_outbox/core/Cargo.toml deleted file mode 100644 index c7876286..00000000 --- a/programs/cross_zone_outbox/core/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "cross_zone_outbox_core" -version = "0.1.0" -edition = "2024" -license = { workspace = true } - -[lints] -workspace = true - -[dependencies] -lee_core.workspace = true -serde = { workspace = true, features = ["alloc"] } -risc0-zkvm.workspace = true -borsh.workspace = true diff --git a/programs/cross_zone_outbox/core/src/lib.rs b/programs/cross_zone_outbox/core/src/lib.rs deleted file mode 100644 index b3449598..00000000 --- a/programs/cross_zone_outbox/core/src/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use lee_core::{ - account::AccountId, - program::{PdaSeed, ProgramId}, -}; -use serde::{Deserialize, Serialize}; - -/// Raw 32-byte zone (channel) id; the host maps it to the zone-sdk `ChannelId`. -pub type ZoneId = [u8; 32]; - -const OUTBOX_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneOutbox/00000/"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum Instruction { - /// Records an outbound cross-zone message as a write to a self-owned PDA. - /// - /// Required accounts (1): - /// - Outbox PDA account - Emit { - target_zone: ZoneId, - target_program_id: ProgramId, - /// Accounts the destination inbox must hand to the target program's - /// chained call. The emitter specifies them; the watcher forwards them - /// verbatim so the inbox stays target-agnostic. - target_accounts: Vec<[u8; 32]>, - payload: Vec, - ordinal: u32, - }, -} - -/// The message as stored in an outbox PDA. The destination zone's watcher reads -/// this from the inscribed block; the source coordinates are filled by the -/// watcher, not stored here. -#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct OutboxRecord { - pub target_zone: ZoneId, - pub target_program_id: ProgramId, - pub target_accounts: Vec<[u8; 32]>, - pub payload: Vec, -} - -impl OutboxRecord { - /// Borsh-encoded form stored in the outbox PDA's account data. - #[must_use] - pub fn to_bytes(&self) -> Vec { - borsh::to_vec(self).expect("OutboxRecord serializes") - } - - /// Decodes an [`OutboxRecord`] from account data. - pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result { - borsh::from_slice(bytes) - } -} - -/// PDA holding one emitted message, keyed by destination zone and a per-zone -/// ordinal. -#[must_use] -pub fn outbox_pda(outbox_id: ProgramId, target_zone: &ZoneId, ordinal: u32) -> AccountId { - AccountId::for_public_pda(&outbox_id, &outbox_pda_seed(target_zone, ordinal)) -} - -/// Seed of an outbox message PDA, exposed so the guest can claim the account. -#[must_use] -pub fn outbox_pda_seed(target_zone: &ZoneId, ordinal: u32) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256 as _}; - - let mut bytes = Vec::with_capacity(OUTBOX_SEED_DOMAIN.len() + target_zone.len() + 4); - bytes.extend_from_slice(&OUTBOX_SEED_DOMAIN); - bytes.extend_from_slice(target_zone); - bytes.extend_from_slice(&ordinal.to_le_bytes()); - - let seed: [u8; 32] = Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .unwrap_or_else(|_| unreachable!()); - PdaSeed::new(seed) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn outbox_pda_is_unique_per_zone_and_ordinal() { - let id: ProgramId = [3; 8]; - let zone_a = [1; 32]; - let zone_b = [2; 32]; - - assert_eq!(outbox_pda(id, &zone_a, 0), outbox_pda(id, &zone_a, 0)); - assert_ne!(outbox_pda(id, &zone_a, 0), outbox_pda(id, &zone_a, 1)); - assert_ne!(outbox_pda(id, &zone_a, 0), outbox_pda(id, &zone_b, 0)); - } -} diff --git a/programs/ping/core/Cargo.toml b/programs/ping/core/Cargo.toml deleted file mode 100644 index 29870630..00000000 --- a/programs/ping/core/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "ping_core" -version = "0.1.0" -edition = "2024" -license = { workspace = true } - -[lints] -workspace = true - -[dependencies] -lee_core.workspace = true -serde = { workspace = true, features = ["alloc"] } diff --git a/programs/ping/core/src/lib.rs b/programs/ping/core/src/lib.rs deleted file mode 100644 index 268e5b98..00000000 --- a/programs/ping/core/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -use lee_core::{ - account::AccountId, - program::{PdaSeed, ProgramId}, -}; -use serde::{Deserialize, Serialize}; - -const PING_RECORD_SEED: [u8; 32] = *b"/LEZ/v0.3/PingRecord/0000000000/"; - -/// Instruction delivered to `ping_receiver` by the inbox: record the payload. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ReceiverInstruction { - Record { payload: Vec }, -} - -/// Instruction to `ping_sender`: forwarded verbatim into `cross_zone_outbox::Instruction::Emit`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum SenderInstruction { - Send { - outbox_program_id: ProgramId, - target_zone: [u8; 32], - target_program_id: ProgramId, - target_accounts: Vec<[u8; 32]>, - payload: Vec, - ordinal: u32, - }, -} - -/// 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 fn ping_record_seed() -> PdaSeed { - PdaSeed::new(PING_RECORD_SEED) -} diff --git a/programs/wrapped_token/core/Cargo.toml b/programs/wrapped_token/core/Cargo.toml deleted file mode 100644 index ef0aabbc..00000000 --- a/programs/wrapped_token/core/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "wrapped_token_core" -version = "0.1.0" -edition = "2024" -license = { workspace = true } - -[lints] -workspace = true - -[dependencies] -lee_core.workspace = true -serde = { workspace = true, features = ["alloc"] } -risc0-zkvm.workspace = true diff --git a/programs/wrapped_token/core/src/lib.rs b/programs/wrapped_token/core/src/lib.rs deleted file mode 100644 index ea4ffd97..00000000 --- a/programs/wrapped_token/core/src/lib.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Core types for the wrapped-token program, the destination side of the -//! cross-zone bridge. Only the cross-zone inbox may mint; the guest enforces -//! this by reading the authorized minter from a genesis-seeded config account. - -use lee_core::{ - account::AccountId, - program::{PdaSeed, ProgramId}, -}; -use serde::{Deserialize, Serialize}; - -const CONFIG_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenConfig/00/"; -const HOLDING_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenHold/00000"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum Instruction { - /// Credit `amount` wrapped tokens to `recipient`'s holding. Delivered only by - /// the cross-zone inbox. - /// - /// Required accounts (2): the wrapped-token config PDA, then the recipient's - /// holding PDA. - Mint { recipient: [u8; 32], amount: u128 }, -} - -/// PDA holding the authorized minter program id (the cross-zone inbox), seeded at -/// genesis so the guest can pin its caller without importing the inbox image id. -#[must_use] -pub fn config_account_id(wrapped_token_id: ProgramId) -> AccountId { - AccountId::for_public_pda(&wrapped_token_id, &config_seed()) -} - -#[must_use] -pub fn config_seed() -> PdaSeed { - PdaSeed::new(CONFIG_SEED_DOMAIN) -} - -/// PDA holding one recipient's wrapped-token balance. -#[must_use] -pub fn holding_account_id(wrapped_token_id: ProgramId, recipient: &[u8; 32]) -> AccountId { - AccountId::for_public_pda(&wrapped_token_id, &holding_seed(recipient)) -} - -#[must_use] -pub fn holding_seed(recipient: &[u8; 32]) -> PdaSeed { - use risc0_zkvm::sha::{Impl, Sha256 as _}; - - let mut bytes = Vec::with_capacity(HOLDING_SEED_DOMAIN.len() + recipient.len()); - bytes.extend_from_slice(&HOLDING_SEED_DOMAIN); - bytes.extend_from_slice(recipient); - let seed: [u8; 32] = Impl::hash_bytes(&bytes) - .as_bytes() - .try_into() - .unwrap_or_else(|_| unreachable!()); - PdaSeed::new(seed) -} - -/// Encodes the authorized minter program id for the config account's data. -#[must_use] -pub fn minter_bytes(minter: ProgramId) -> [u8; 32] { - let mut bytes = [0_u8; 32]; - for (word, chunk) in minter.iter().zip(bytes.chunks_exact_mut(4)) { - chunk.copy_from_slice(&word.to_le_bytes()); - } - bytes -} - -/// Decodes the authorized minter program id from the config account's data. -#[must_use] -pub fn read_minter(data: &[u8]) -> Option { - if data.len() < 32 { - return None; - } - let mut minter = [0_u32; 8]; - for (word, chunk) in minter.iter_mut().zip(data[..32].chunks_exact(4)) { - *word = u32::from_le_bytes(chunk.try_into().unwrap_or_else(|_| unreachable!())); - } - Some(minter) -} - -/// Reads a wrapped-token balance from account data; empty data is a zero balance. -#[must_use] -pub fn read_balance(data: &[u8]) -> u128 { - if data.len() < 16 { - return 0; - } - u128::from_le_bytes(data[..16].try_into().unwrap_or_else(|_| unreachable!())) -} - -#[must_use] -pub fn balance_bytes(amount: u128) -> [u8; 16] { - amount.to_le_bytes() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn minter_round_trips() { - let minter: ProgramId = [1, 2, 3, 4, 5, 6, 7, 8]; - assert_eq!(read_minter(&minter_bytes(minter)), Some(minter)); - } - - #[test] - fn balance_round_trips() { - assert_eq!(read_balance(&balance_bytes(42)), 42); - assert_eq!(read_balance(&[]), 0); - } - - #[test] - fn holding_is_unique_per_recipient() { - let id: ProgramId = [9; 8]; - assert_ne!( - holding_account_id(id, &[1; 32]), - holding_account_id(id, &[2; 32]) - ); - assert_eq!( - holding_account_id(id, &[1; 32]), - holding_account_id(id, &[1; 32]) - ); - } -}