diff --git a/artifacts/lez/programs/amm.bin b/artifacts/lez/programs/amm.bin index e67b8a693..ed3f9487e 100644 Binary files a/artifacts/lez/programs/amm.bin and b/artifacts/lez/programs/amm.bin differ diff --git a/artifacts/lez/programs/associated_token_account.bin b/artifacts/lez/programs/associated_token_account.bin index 58522a1eb..de0cfe7e9 100644 Binary files a/artifacts/lez/programs/associated_token_account.bin and b/artifacts/lez/programs/associated_token_account.bin differ diff --git a/artifacts/lez/programs/authenticated_transfer.bin b/artifacts/lez/programs/authenticated_transfer.bin index cd1a9b2ac..376bc806c 100644 Binary files a/artifacts/lez/programs/authenticated_transfer.bin and b/artifacts/lez/programs/authenticated_transfer.bin differ diff --git a/artifacts/lez/programs/bridge.bin b/artifacts/lez/programs/bridge.bin index 9c18a4e2c..ba399e3b1 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/bridge_lock.bin b/artifacts/lez/programs/bridge_lock.bin index 89f1aa682..e285dbd09 100644 Binary files a/artifacts/lez/programs/bridge_lock.bin and b/artifacts/lez/programs/bridge_lock.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index 4d2165518..0ab33fa3c 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/cross_zone_inbox.bin b/artifacts/lez/programs/cross_zone_inbox.bin index 9976ac6f8..b393117c6 100644 Binary files a/artifacts/lez/programs/cross_zone_inbox.bin and b/artifacts/lez/programs/cross_zone_inbox.bin differ diff --git a/artifacts/lez/programs/cross_zone_outbox.bin b/artifacts/lez/programs/cross_zone_outbox.bin index 745957054..cf20de719 100644 Binary files a/artifacts/lez/programs/cross_zone_outbox.bin and b/artifacts/lez/programs/cross_zone_outbox.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index f9abdd3ab..3ddf50de9 100644 Binary files a/artifacts/lez/programs/faucet.bin and b/artifacts/lez/programs/faucet.bin differ diff --git a/artifacts/lez/programs/pinata.bin b/artifacts/lez/programs/pinata.bin index ba555a206..b9b712194 100644 Binary files a/artifacts/lez/programs/pinata.bin and b/artifacts/lez/programs/pinata.bin differ diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin index 23e71794b..5b3bea81b 100644 Binary files a/artifacts/lez/programs/pinata_token.bin and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/ping_receiver.bin b/artifacts/lez/programs/ping_receiver.bin index a4ffbe425..96ecccd8f 100644 Binary files a/artifacts/lez/programs/ping_receiver.bin and b/artifacts/lez/programs/ping_receiver.bin differ diff --git a/artifacts/lez/programs/ping_sender.bin b/artifacts/lez/programs/ping_sender.bin index d2d3d1b54..b110746a9 100644 Binary files a/artifacts/lez/programs/ping_sender.bin and b/artifacts/lez/programs/ping_sender.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 40a96ac24..2c11682ff 100644 Binary files a/artifacts/lez/programs/token.bin and b/artifacts/lez/programs/token.bin differ diff --git a/artifacts/lez/programs/vault.bin b/artifacts/lez/programs/vault.bin index fa90bdbaf..477c26823 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/artifacts/lez/programs/wrapped_token.bin b/artifacts/lez/programs/wrapped_token.bin index 2b12cad1c..a1d10709e 100644 Binary files a/artifacts/lez/programs/wrapped_token.bin and b/artifacts/lez/programs/wrapped_token.bin differ diff --git a/integration_tests/tests/cross_zone_state_machine.rs b/integration_tests/tests/cross_zone_state_machine.rs index 62a881059..47027eb2d 100644 --- a/integration_tests/tests/cross_zone_state_machine.rs +++ b/integration_tests/tests/cross_zone_state_machine.rs @@ -304,7 +304,11 @@ fn inbox_dispatch_delivers_payload_to_ping_receiver() { let message = Message::try_new( inbox_id, - dispatch_accounts(inbox_id, &msg, vec![record_id]), + dispatch_accounts( + inbox_id, + &msg, + vec![receiver_config_account_id(receiver_id), record_id], + ), vec![], InboxInstruction::Dispatch(msg), ) @@ -946,6 +950,94 @@ fn the_outbox_pin_is_written_once_and_replayable() { ); } +/// A token that authorizes nothing mints for nobody. The state a zone reaches with +/// no peers configured, where the config is still seeded so its PDA cannot be +/// claimed by a first initializer. +#[test] +fn a_mint_is_refused_when_the_token_authorizes_no_source() { + let inbox_id = programs::cross_zone_inbox().id(); + let wrapped_token_id = programs::wrapped_token().id(); + let self_zone = [1_u8; 32]; + let src_zone = [2_u8; 32]; + + let mut state = base_state(); + seed_inbox_config(&mut state, self_zone); + seed_wrapped_config(&mut state, vec![]); + + let msg = CrossZoneMessage { + src_zone, + src_block_id: 5, + src_block_hash: SRC_BLOCK_HASH, + src_tx_index: 0, + src_program_id: programs::bridge_lock().id(), + target_program_id: wrapped_token_id, + payload: mint_payload(), + l1_inclusion_witness: None, + }; + let message = Message::try_new( + inbox_id, + dispatch_accounts( + inbox_id, + &msg, + vec![ + wrapped_token_core::config_account_id(wrapped_token_id), + wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT), + ], + ), + vec![], + InboxInstruction::Dispatch(msg), + ) + .expect("build dispatch message"); + let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![])); + + let Err(err) = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) else { + panic!("a token authorizing nothing must not mint"); + }; + assert!( + format!("{err:?}").contains("peer source this token authorizes"), + "rejected for the wrong reason: {err:?}" + ); +} + +/// The marker only means something because the caller is pinned to the inbox. +/// Invoked directly, with the caller handing in the marker themselves, the mint +/// must refuse before it ever looks at it. +#[test] +fn a_top_level_mint_is_refused() { + let inbox_id = programs::cross_zone_inbox().id(); + let wrapped_token_id = programs::wrapped_token().id(); + let src_zone = [2_u8; 32]; + let src_program_id = programs::bridge_lock().id(); + + let mut state = base_state(); + seed_wrapped_config(&mut state, vec![(src_zone, src_program_id)]); + + let marker_id = inbox_source_marker_account_id(inbox_id, &src_zone, src_program_id); + let message = Message::try_new( + wrapped_token_id, + vec![ + marker_id, + wrapped_token_core::config_account_id(wrapped_token_id), + wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT), + ], + vec![], + wrapped_token_core::Instruction::Mint { + recipient: RECIPIENT, + amount: LOCK_AMOUNT, + }, + ) + .expect("build mint message"); + let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![])); + + let Err(err) = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) else { + panic!("a directly invoked mint must not execute"); + }; + assert!( + format!("{err:?}").contains("only callable by the authorized minter"), + "rejected for the wrong reason: {err:?}" + ); +} + /// Drives a hand-built `cross_zone_inbox::Dispatch` (as the watcher would inject) /// and asserts it chains into `wrapped_token::Mint`, crediting the recipient. #[test] @@ -1005,9 +1097,12 @@ fn a_mint_from_an_unrouted_emitter_is_rejected() { .expect("build dispatch message"); let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![])); + let Err(err) = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) else { + panic!("a delivery from a source the token did not authorize must not mint"); + }; assert!( - ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0).is_err(), - "a delivery from an emitter with no route to wrapped_token must not mint" + format!("{err:?}").contains("peer source this token authorizes"), + "rejected for the wrong reason: {err:?}" ); } @@ -1195,7 +1290,11 @@ fn a_delivery_from_a_second_block_at_the_same_id_is_refused() { let record_id = ping_record_pda(receiver_id); let message = Message::try_new( inbox_id, - dispatch_accounts(inbox_id, &msg, vec![record_id]), + dispatch_accounts( + inbox_id, + &msg, + vec![receiver_config_account_id(receiver_id), record_id], + ), vec![], InboxInstruction::Dispatch(msg), ) @@ -1228,7 +1327,11 @@ fn a_delivery_from_a_second_block_at_the_same_id_is_refused() { }; let control_message = Message::try_new( inbox_id, - dispatch_accounts(inbox_id, &control_msg, vec![record_id]), + dispatch_accounts( + inbox_id, + &control_msg, + vec![receiver_config_account_id(receiver_id), record_id], + ), vec![], InboxInstruction::Dispatch(control_msg), ) diff --git a/lez/programs/cross_zone_inbox/core/src/lib.rs b/lez/programs/cross_zone_inbox/core/src/lib.rs index 71ec5f834..9184e0342 100644 --- a/lez/programs/cross_zone_inbox/core/src/lib.rs +++ b/lez/programs/cross_zone_inbox/core/src/lib.rs @@ -260,10 +260,13 @@ pub fn inbox_seen_shard_seed(src_zone: &ZoneId, src_block_id: u64) -> PdaSeed { /// The account naming who sent a delivery, which the inbox passes at position 0 /// of the chained call so the target can authenticate its own sources. /// -/// It is never written and never claimed, so it stays `Account::default()` for -/// ever and the state machine's uninitialized-account rule skips it. The address -/// is the whole message: only the inbox can derive it, and it commits to the pair -/// the target cares about. +/// Nothing writes or claims it, so the state machine's uninitialized-account rule +/// skips it for being unchanged rather than for being default: anyone may send it +/// balance, and the inbox and the targets all round-trip it untouched. +/// +/// The address is derivable by anyone, so it is not a secret and not a +/// capability. What makes it mean something is that a target checks it only after +/// pinning its caller to the inbox, and only the inbox can be that caller. #[must_use] pub fn inbox_source_marker_account_id( inbox_id: ProgramId, diff --git a/lez/programs/ping_core/src/lib.rs b/lez/programs/ping_core/src/lib.rs index b800a67f7..672560b60 100644 --- a/lez/programs/ping_core/src/lib.rs +++ b/lez/programs/ping_core/src/lib.rs @@ -36,9 +36,7 @@ pub enum ReceiverInstruction { /// about the record meaning something: without it any program on any configured /// peer can overwrite the record, and a delivery proves only that some peer sent /// it. -#[derive( - Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] +#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] pub struct ReceiverConfig { /// The program allowed to call `Record`: the cross-zone inbox. pub deliverer: ProgramId, @@ -161,6 +159,29 @@ mod tests { assert_eq!(words[0], 0); } + /// `Record` is serialized by the source zone into the emission payload and + /// decoded by the destination, so its tag word is wire format. + #[test] + fn record_is_the_first_variant() { + let record = ReceiverInstruction::Record { payload: vec![] }; + let words = risc0_zkvm::serde::to_vec(&record).expect("Record serializes"); + assert_eq!(words[0], 0); + } + + #[test] + fn an_empty_receiver_config_does_not_decode() { + assert_eq!(ReceiverConfig::from_bytes(&[]), None); + } + + #[test] + fn receiver_config_round_trips() { + let config = ReceiverConfig { + deliverer: [1; 8], + sources: vec![([7; 32], [9; 8])], + }; + assert_eq!(ReceiverConfig::from_bytes(&config.to_bytes()), Some(config)); + } + #[test] fn outbox_id_round_trips() { let outbox: ProgramId = [9; 8]; diff --git a/lez/programs/wrapped_token/core/src/lib.rs b/lez/programs/wrapped_token/core/src/lib.rs index 4e124a229..2fba37c88 100644 --- a/lez/programs/wrapped_token/core/src/lib.rs +++ b/lez/programs/wrapped_token/core/src/lib.rs @@ -47,9 +47,7 @@ pub enum Instruction { /// The source list is what makes this token authorize its own inbound value /// rather than trusting a central route table to have done it. Borsh because the /// list is variable length. -#[derive( - Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] +#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] pub struct WrappedTokenConfig { /// The program allowed to call `Mint`: the cross-zone inbox. pub minter: ProgramId, @@ -139,6 +137,18 @@ mod tests { assert_eq!(WrappedTokenConfig::from_bytes(&[]), None); } + /// The peer's `bridge_lock` serializes `Mint` into the emission payload, so + /// its tag word is wire format. + #[test] + fn mint_is_the_first_variant() { + let mint = Instruction::Mint { + recipient: [3; 32], + amount: 1, + }; + let words = risc0_zkvm::serde::to_vec(&mint).expect("Mint serializes"); + assert_eq!(words[0], 0); + } + #[test] fn balance_round_trips() { assert_eq!(read_balance(&balance_bytes(42)), 42); diff --git a/test_fixtures/fixtures/prebuilt_sequencer_db.dump b/test_fixtures/fixtures/prebuilt_sequencer_db.dump index 642311ef6..62e8a8028 100644 Binary files a/test_fixtures/fixtures/prebuilt_sequencer_db.dump and b/test_fixtures/fixtures/prebuilt_sequencer_db.dump differ