mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-29 18:39:30 +00:00
feat(cross-zone): add inbox and outbox core crates (envelope, replay key, dispatch builder)
This commit is contained in:
parent
26bc23482a
commit
43ce9b5932
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -1796,6 +1796,25 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone_inbox_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee",
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone_outbox_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
|
||||
@ -24,6 +24,8 @@ members = [
|
||||
"programs/faucet/core",
|
||||
"programs/bridge/core",
|
||||
"programs/vault/core",
|
||||
"programs/cross_zone_outbox/core",
|
||||
"programs/cross_zone_inbox/core",
|
||||
"lez/sequencer/core",
|
||||
"lez/sequencer/service",
|
||||
"lez/sequencer/service/protocol",
|
||||
@ -80,6 +82,8 @@ authenticated_transfer_core = { path = "programs/authenticated_transfer/core" }
|
||||
faucet_core = { path = "programs/faucet/core" }
|
||||
bridge_core = { path = "programs/bridge/core" }
|
||||
vault_core = { path = "programs/vault/core" }
|
||||
cross_zone_outbox_core = { path = "programs/cross_zone_outbox/core" }
|
||||
cross_zone_inbox_core = { path = "programs/cross_zone_inbox/core" }
|
||||
test_program_methods = { path = "test_program_methods" }
|
||||
testnet_initial_state = { path = "lez/testnet_initial_state" }
|
||||
keycard_wallet = { path = "lez/keycard_wallet" }
|
||||
|
||||
18
programs/cross_zone_inbox/core/Cargo.toml
Normal file
18
programs/cross_zone_inbox/core/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[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
|
||||
lee = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
# Host-only transaction builder; pulls `lee`, so the risc0 guest builds without it.
|
||||
host = ["dep:lee"]
|
||||
186
programs/cross_zone_inbox/core/src/lib.rs
Normal file
186
programs/cross_zone_inbox/core/src/lib.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
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 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.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct InboxConfig {
|
||||
pub allowed_peers: BTreeMap<ZoneId, ExpectedPubkey>,
|
||||
pub allowed_targets: BTreeMap<ZoneId, Vec<ProgramId>>,
|
||||
}
|
||||
|
||||
#[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, &seen_shard_seed(src_zone, src_block_id))
|
||||
}
|
||||
|
||||
fn 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![]),
|
||||
)
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
13
programs/cross_zone_outbox/core/Cargo.toml
Normal file
13
programs/cross_zone_outbox/core/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[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
|
||||
61
programs/cross_zone_outbox/core/src/lib.rs
Normal file
61
programs/cross_zone_outbox/core/src/lib.rs
Normal file
@ -0,0 +1,61 @@
|
||||
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,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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_seed(target_zone, ordinal))
|
||||
}
|
||||
|
||||
fn outbox_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));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user