diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 1d440d26..148a9403 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index 51b5033b..46326067 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index d70c11c7..ad40805f 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index 9d237c3b..e2a6f120 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 1c5dcd79..d0460713 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index f3f385fd..b0f81f79 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 874eae64..dcbee51a 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index eab13cb2..e0358fa4 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index dd667593..9bd40a30 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 92415e08..0353d78f 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 573356a4..cd74cf3f 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index ad70a3a8..1f966bef 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index c32bfaf0..8a48effd 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index 6e4ce527..e08df712 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 5c912e62..37abf0f7 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 4d1d519e..ebd53621 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index e83d0848..29c660cd 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index 3884f114..a560d477 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 8eb33bb0..c9d0facd 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index 083e7a9f..9b31fd7e 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index a3a8694f..c4a2c039 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 21f27ad7..42d2171d 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 89690cce..d2b99291 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index d63c1114..f57ac2f1 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 061fe244..6b79e074 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index a089fcf1..eb89f4a9 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index 528c590f..092a2191 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 22beb033..559adea4 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index f9140423..d7e81a9f 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 53e4d68d..880e03b1 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index af2d91fd..3a4e811f 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index b990c758..eeb80385 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index 223c596d..b71d87ab 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index d7fccfec..8d749f3c 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index 2ea73fe0..109829d2 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index fe40db94..8018cd80 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -43,16 +43,42 @@ struct ExecutionState { /// `AccountId` entry or as an equality check against the existing one, making the rule: one /// `(program, seed)` → one account per tx. pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>, + /// Map from a mask-3 `pre_state`'s position in `visibility_mask` to the npk supplied for + /// that position in `private_account_keys`. Built once in `derive_from_outputs` by walking + /// `visibility_mask` in lock-step with `private_account_keys`, used later by the claim and + /// caller-seeds authorization paths. + private_pda_npk_by_position: HashMap, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( visibility_mask: &[u8], - private_pda_npk_by_position: &HashMap, + private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], program_id: ProgramId, program_outputs: Vec, ) -> Self { + // Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in + // pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The + // downstream `compute_circuit_output` also consumes the same iterator and its trailing + // assertions catch an over-supply of keys; under-supply surfaces here. + let mut private_pda_npk_by_position: HashMap = HashMap::new(); + { + let mut keys_iter = private_account_keys.iter(); + for (pos, &mask) in visibility_mask.iter().enumerate() { + if matches!(mask, 1..=3) { + let (npk, _) = keys_iter.next().unwrap_or_else(|| { + panic!( + "private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})" + ) + }); + if mask == 3 { + private_pda_npk_by_position.insert(pos, *npk); + } + } + } + } + let block_valid_from = program_outputs .iter() .filter_map(|output| output.block_validity_window.start()) @@ -89,6 +115,7 @@ impl ExecutionState { timestamp_validity_window, private_pda_bound_positions: HashSet::new(), pda_family_binding: HashMap::new(), + private_pda_npk_by_position, }; let Some(first_output) = program_outputs.first() else { @@ -166,7 +193,6 @@ impl ExecutionState { execution_state.validate_and_sync_states( visibility_mask, - private_pda_npk_by_position, chained_call.program_id, caller_program_id, &chained_call.pda_seeds, @@ -221,14 +247,9 @@ impl ExecutionState { } /// Validate program pre and post states and populate the execution state. - #[expect( - clippy::too_many_arguments, - reason = "breaking out a context struct does not buy us anything here" - )] fn validate_and_sync_states( &mut self, visibility_mask: &[u8], - private_pda_npk_by_position: &HashMap, program_id: ProgramId, caller_program_id: Option, caller_pda_seeds: &[PdaSeed], @@ -270,43 +291,16 @@ impl ExecutionState { |(pos, acc)| (acc.is_authorized, pos) ); - // Find which caller seed (if any) authorizes this pre_state, under the - // public or the private derivation. We need the *specific* seed (not just a - // bool) so we can record the `(caller, seed) → account_id` family binding. - // The match arm also returns the caller so the consumer below does not have - // to re-unwrap `caller_program_id`. Only reachable when - // `caller_program_id.is_some()`, top-level flows have no caller-emitted - // seeds, so binding at top level must come from the claim path below. - let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> = caller_program_id - .and_then(|caller| { - caller_pda_seeds.iter().find_map(|seed| { - if AccountId::for_public_pda(&caller, seed) == pre_account_id { - return Some((*seed, false, caller)); - } - if let Some(npk) = - private_pda_npk_by_position.get(&pre_state_position) - && AccountId::for_private_pda(&caller, seed, npk) - == pre_account_id - { - return Some((*seed, true, caller)); - } - None - }) - }); - - if let Some((seed, is_private_form, caller)) = matched_caller_seed { - assert_family_binding( - &mut self.pda_family_binding, - caller, - seed, - pre_account_id, - ); - if is_private_form { - self.private_pda_bound_positions.insert(pre_state_position); - } - } - - let is_authorized = previous_is_authorized || matched_caller_seed.is_some(); + let is_authorized = resolve_authorization_and_record_bindings( + &mut self.pda_family_binding, + &mut self.private_pda_bound_positions, + &self.private_pda_npk_by_position, + pre_account_id, + pre_state_position, + caller_program_id, + caller_pda_seeds, + previous_is_authorized, + ); assert_eq!( pre_is_authorized, is_authorized, @@ -358,31 +352,34 @@ impl ExecutionState { ); } }, - 3 => match claim { - Claim::Authorized => { - assert!( - pre_is_authorized, - "Cannot claim unauthorized private PDA {pre_account_id}" - ); + 3 => { + match claim { + Claim::Authorized => { + assert!( + pre_is_authorized, + "Cannot claim unauthorized private PDA {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let npk = self + .private_pda_npk_by_position + .get(&pre_state_position) + .expect("private PDA pre_state must have an npk in the position map"); + let pda = AccountId::for_private_pda(&program_id, &seed, npk); + assert_eq!( + pre_account_id, pda, + "Invalid private PDA claim for account {pre_account_id}" + ); + self.private_pda_bound_positions.insert(pre_state_position); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); + } } - Claim::Pda(seed) => { - let npk = private_pda_npk_by_position.get(&pre_state_position).expect( - "private PDA pre_state must have an npk in the position map", - ); - let pda = AccountId::for_private_pda(&program_id, &seed, npk); - assert_eq!( - pre_account_id, pda, - "Invalid private PDA claim for account {pre_account_id}" - ); - self.private_pda_bound_positions.insert(pre_state_position); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } - }, + } _ => { // Mask 1/2: standard private accounts don't enforce the claim semantics. // Unauthorized private claiming is intentionally allowed since operating @@ -439,6 +436,54 @@ fn assert_family_binding( } } +/// Resolve the authorization state of a `pre_state` seen again in a chained call and record +/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a +/// previously-seen authorization or a matching caller seed (under the public or private +/// derivation). When a caller seed matches, also records the `(caller, seed) → account_id` +/// family binding and, for the private form, marks the position in +/// `private_pda_bound_positions`. Only reachable when `caller_program_id.is_some()`, +/// top-level flows have no caller-emitted seeds, so binding at top level must come from the +/// claim path. Free function so callers can pass individual `&mut self.*` field borrows +/// without holding a borrow on the surrounding struct's other fields. +#[expect( + clippy::too_many_arguments, + reason = "breaking out a context struct does not buy us anything here" +)] +fn resolve_authorization_and_record_bindings( + pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>, + private_pda_bound_positions: &mut HashSet, + private_pda_npk_by_position: &HashMap, + pre_account_id: AccountId, + pre_state_position: usize, + caller_program_id: Option, + caller_pda_seeds: &[PdaSeed], + previous_is_authorized: bool, +) -> bool { + let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> = + caller_program_id.and_then(|caller| { + caller_pda_seeds.iter().find_map(|seed| { + if AccountId::for_public_pda(&caller, seed) == pre_account_id { + return Some((*seed, false, caller)); + } + if let Some(npk) = private_pda_npk_by_position.get(&pre_state_position) + && AccountId::for_private_pda(&caller, seed, npk) == pre_account_id + { + return Some((*seed, true, caller)); + } + None + }) + }); + + if let Some((seed, is_private_form, caller)) = matched_caller_seed { + assert_family_binding(pda_family_binding, caller, seed, pre_account_id); + if is_private_form { + private_pda_bound_positions.insert(pre_state_position); + } + } + + previous_is_authorized || matched_caller_seed.is_some() +} + fn compute_circuit_output( execution_state: ExecutionState, visibility_mask: &[u8], @@ -718,30 +763,9 @@ fn main() { program_id, } = env::read(); - // Build a position → npk map for mask-3 pre_states. `private_account_keys` is consumed in - // pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The - // downstream `compute_circuit_output` also consumes the same iterator and its trailing - // assertions catch an over-supply of keys; under-supply surfaces here. - let mut private_pda_npk_by_position: HashMap = HashMap::new(); - { - let mut keys_iter = private_account_keys.iter(); - for (pos, &mask) in visibility_mask.iter().enumerate() { - if matches!(mask, 1..=3) { - let (npk, _) = keys_iter.next().unwrap_or_else(|| { - panic!( - "private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})" - ) - }); - if mask == 3 { - private_pda_npk_by_position.insert(pos, *npk); - } - } - } - } - let execution_state = ExecutionState::derive_from_outputs( &visibility_mask, - &private_pda_npk_by_position, + &private_account_keys, program_id, program_outputs, );