mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-07-17 21:29:51 +00:00
chore(cross-zone): remove duplicate top-level programs core crates
Remove the stale top-level programs/ core crates (bridge_lock_core, cross_zone_inbox_core, cross_zone_outbox_core, ping_core, wrapped_token_core), leftover copies from the initial cross-zone port before programs moved to the per-program-crate layout. The live cores under lez/programs/*/core keep the same package names and remain the ones actually used.
This commit is contained in:
parent
cac4921581
commit
b8e21ed276
@ -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"]
|
||||
@ -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<u8>,
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
@ -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<ProgramId>,
|
||||
/// 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<CrossZonePeer>,
|
||||
}
|
||||
|
||||
/// 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<u8>,
|
||||
/// Reserved for a future source-state proof; MUST be `None` in v1.
|
||||
pub l1_inclusion_witness: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// 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<ZoneId, ExpectedPubkey>,
|
||||
pub allowed_targets: BTreeMap<ZoneId, Vec<ProgramId>>,
|
||||
}
|
||||
|
||||
impl InboxConfig {
|
||||
/// Borsh-encoded form stored in the inbox config account.
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
borsh::to_vec(self).expect("InboxConfig serializes")
|
||||
}
|
||||
|
||||
/// Decodes an [`InboxConfig`] from account data.
|
||||
pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result<Self> {
|
||||
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<MessageKey>);
|
||||
|
||||
impl SeenShard {
|
||||
/// Decodes a shard from account data; empty data is an empty shard.
|
||||
pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result<Self> {
|
||||
if bytes.is_empty() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
borsh::from_slice(bytes)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
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<AccountId>,
|
||||
) -> 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<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).
|
||||
#[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<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(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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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<u8>,
|
||||
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<u8>,
|
||||
}
|
||||
|
||||
impl OutboxRecord {
|
||||
/// Borsh-encoded form stored in the outbox PDA's account data.
|
||||
#[must_use]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
borsh::to_vec(self).expect("OutboxRecord serializes")
|
||||
}
|
||||
|
||||
/// Decodes an [`OutboxRecord`] from account data.
|
||||
pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result<Self> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -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"] }
|
||||
@ -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<u8> },
|
||||
}
|
||||
|
||||
/// 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<u8>,
|
||||
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)
|
||||
}
|
||||
@ -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
|
||||
@ -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<ProgramId> {
|
||||
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])
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user