diff --git a/Cargo.lock b/Cargo.lock index 6dd5b588..d0395448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4524,6 +4524,7 @@ dependencies = [ "borsh", "bridge_core", "clock_core", + "cross_zone_inbox_core", "env_logger", "faucet_core", "hex", @@ -4531,6 +4532,7 @@ dependencies = [ "k256", "lee_core", "log", + "ping_core", "rand 0.8.6", "risc0-binfmt", "risc0-build", @@ -7133,6 +7135,14 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "ping_core" +version = "0.1.0" +dependencies = [ + "lee_core", + "serde", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -7409,6 +7419,7 @@ dependencies = [ "cross_zone_outbox_core", "faucet_core", "lee_core", + "ping_core", "risc0-zkvm", "serde", "token_core", diff --git a/Cargo.toml b/Cargo.toml index 1b15f129..0fd60a92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "programs/vault/core", "programs/cross_zone_outbox/core", "programs/cross_zone_inbox/core", + "programs/ping/core", "lez/sequencer/core", "lez/sequencer/service", "lez/sequencer/service/protocol", @@ -84,6 +85,7 @@ 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" } +ping_core = { path = "programs/ping/core" } test_program_methods = { path = "test_program_methods" } testnet_initial_state = { path = "lez/testnet_initial_state" } keycard_wallet = { path = "lez/keycard_wallet" } diff --git a/artifacts/program_methods/ping_receiver.bin b/artifacts/program_methods/ping_receiver.bin new file mode 100644 index 00000000..e5c1680e Binary files /dev/null and b/artifacts/program_methods/ping_receiver.bin differ diff --git a/lee/state_machine/Cargo.toml b/lee/state_machine/Cargo.toml index 8777db09..7337cb47 100644 --- a/lee/state_machine/Cargo.toml +++ b/lee/state_machine/Cargo.toml @@ -34,6 +34,8 @@ risc0-binfmt = "3.0.2" lee_core = { workspace = true, features = ["test_utils"] } token_core.workspace = true authenticated_transfer_core.workspace = true +cross_zone_inbox_core.workspace = true +ping_core.workspace = true test_program_methods.workspace = true env_logger.workspace = true diff --git a/lee/state_machine/src/cross_zone_dispatch_tests.rs b/lee/state_machine/src/cross_zone_dispatch_tests.rs new file mode 100644 index 00000000..aab90e85 --- /dev/null +++ b/lee/state_machine/src/cross_zone_dispatch_tests.rs @@ -0,0 +1,104 @@ +#![expect( + clippy::tests_outside_test_module, + clippy::arithmetic_side_effects, + reason = "We don't care about these in tests" +)] + +use std::collections::BTreeMap; + +use cross_zone_inbox_core::{ + CrossZoneMessage, InboxConfig, Instruction, inbox_config_account_id, + inbox_seen_shard_account_id, +}; +use lee_core::account::Account; +use ping_core::{ReceiverInstruction, ping_record_pda}; + +use crate::{ + V03State, + program::Program, + public_transaction::{Message, WitnessSet}, + validated_state_diff::ValidatedStateDiff, +}; + +/// Drives `cross_zone_inbox::Dispatch` directly through the state machine +/// (no watcher) and asserts the message is delivered to `ping_receiver`, which +/// records the payload into its own PDA. +#[test] +fn inbox_dispatch_delivers_payload_to_ping_receiver() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + // ping_receiver is a throwaway demo target, registered only in this test state. + state.insert_program(Program::ping_receiver()); + + let inbox_id = Program::cross_zone_inbox().id(); + let receiver_id = Program::ping_receiver().id(); + + let self_zone = [1_u8; 32]; + let src_zone = [2_u8; 32]; + let src_block_id = 5; + + // Seed the inbox config account (inbox-owned) allowing src_zone -> ping_receiver. + let mut allowed_targets = BTreeMap::new(); + allowed_targets.insert(src_zone, vec![receiver_id]); + let config = InboxConfig { + self_zone, + allowed_peers: BTreeMap::new(), + allowed_targets, + }; + let config_id = inbox_config_account_id(inbox_id); + state.force_insert_account( + config_id, + Account { + program_owner: inbox_id, + balance: 0_u128, + data: config + .to_bytes() + .try_into() + .expect("config fits in account data"), + nonce: 0_u128.into(), + }, + ); + + // The payload is the ping_receiver instruction, serialized as risc0 words in + // little-endian bytes (the contract the inbox reverses when forwarding). + let inner = b"hello-cross-zone".to_vec(); + let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record { + payload: inner.clone(), + }) + .expect("serialize ping instruction"); + let payload: Vec = words.iter().flat_map(|word| word.to_le_bytes()).collect(); + + let msg = CrossZoneMessage { + src_zone, + src_block_id, + src_tx_index: 0, + src_program_id: [9_u32; 8], + target_program_id: receiver_id, + payload, + l1_inclusion_witness: None, + }; + + let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id); + let record_id = ping_record_pda(receiver_id); + + let message = Message::try_new( + inbox_id, + vec![config_id, seen_id, record_id], + vec![], + Instruction::Dispatch(msg), + ) + .expect("build dispatch message"); + let tx = crate::PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![])); + + let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) + .expect("dispatch must validate and execute"); + let public_diff = diff.public_diff(); + + let record = public_diff + .get(&record_id) + .expect("ping record account must change"); + assert_eq!( + record.data.clone().into_inner(), + inner, + "ping_receiver must record the delivered payload" + ); +} diff --git a/lee/state_machine/src/lib.rs b/lee/state_machine/src/lib.rs index 129821b5..87dfd513 100644 --- a/lee/state_machine/src/lib.rs +++ b/lee/state_machine/src/lib.rs @@ -33,6 +33,9 @@ mod signature; mod state; mod validated_state_diff; +#[cfg(test)] +mod cross_zone_dispatch_tests; + pub mod program_methods { include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs")); } diff --git a/lee/state_machine/src/program.rs b/lee/state_machine/src/program.rs index bf48afd8..8b71448b 100644 --- a/lee/state_machine/src/program.rs +++ b/lee/state_machine/src/program.rs @@ -12,8 +12,8 @@ use crate::{ AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, BRIDGE_ELF, BRIDGE_ID, CLOCK_ELF, CLOCK_ID, CROSS_ZONE_INBOX_ELF, CROSS_ZONE_INBOX_ID, CROSS_ZONE_OUTBOX_ELF, - CROSS_ZONE_OUTBOX_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, - VAULT_ELF, VAULT_ID, + CROSS_ZONE_OUTBOX_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, PING_RECEIVER_ELF, + PING_RECEIVER_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID, }, }; @@ -190,6 +190,14 @@ impl Program { elf: CROSS_ZONE_INBOX_ELF.to_vec(), } } + + #[must_use] + pub fn ping_receiver() -> Self { + Self { + id: PING_RECEIVER_ID, + elf: PING_RECEIVER_ELF.to_vec(), + } + } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs index 110f3113..949c9920 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -711,6 +711,11 @@ pub mod tests { this.insert(Program::vault().id(), Program::vault()); this.insert(Program::faucet().id(), Program::faucet()); this.insert(Program::bridge().id(), Program::bridge()); + this.insert( + Program::cross_zone_outbox().id(), + Program::cross_zone_outbox(), + ); + this.insert(Program::cross_zone_inbox().id(), Program::cross_zone_inbox()); this }; diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index d7572235..318af4b6 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -22,5 +22,6 @@ bridge_core.workspace = true vault_core.workspace = true cross_zone_outbox_core.workspace = true cross_zone_inbox_core.workspace = true +ping_core.workspace = true risc0-zkvm.workspace = true serde = { workspace = true, default-features = false } diff --git a/program_methods/guest/src/bin/ping_receiver.rs b/program_methods/guest/src/bin/ping_receiver.rs new file mode 100644 index 00000000..567736da --- /dev/null +++ b/program_methods/guest/src/bin/ping_receiver.rs @@ -0,0 +1,47 @@ +use lee_core::{ + account::AccountWithMetadata, + program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs}, +}; +use ping_core::{ReceiverInstruction, ping_record_pda, ping_record_seed}; + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction, + }, + instruction_words, + ) = read_lee_inputs::(); + + assert!( + caller_program_id.is_some(), + "ping_receiver is only callable through a chained call" + ); + + let payload = match instruction { + ReceiverInstruction::Record { payload } => payload, + }; + + let [record] = <[AccountWithMetadata; 1]>::try_from(pre_states) + .expect("Record requires exactly 1 account"); + assert_eq!( + record.account_id, + ping_record_pda(self_program_id), + "Account must be the ping record PDA" + ); + + let mut post_account = record.account.clone(); + post_account.data = payload.try_into().expect("payload fits in account data"); + let post = AccountPostState::new_claimed_if_default(post_account, Claim::Pda(ping_record_seed())); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![record], + vec![post], + ) + .write(); +} diff --git a/programs/ping/core/Cargo.toml b/programs/ping/core/Cargo.toml new file mode 100644 index 00000000..29870630 --- /dev/null +++ b/programs/ping/core/Cargo.toml @@ -0,0 +1,12 @@ +[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 new file mode 100644 index 00000000..f6b5b963 --- /dev/null +++ b/programs/ping/core/src/lib.rs @@ -0,0 +1,25 @@ +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 }, +} + +/// 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) +}