diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index daf990be..e512196f 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 e5f41acb..e6a154cd 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/pinata.bin b/artifacts/program_methods/pinata.bin index a877d206..0a0d44dc 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 d318ae28..b2a2aa03 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 8c001d69..80aca1df 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 cef64fc8..fdad244e 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index cf156804..7b2ce5b1 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 9a887285..26336b78 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 9b6cda17..d307650d 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/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index 7109d1d3..e7ec85ee 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 8c661508..02d7511a 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 610e20b5..074ecc8d 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 373105b7..36d10f3d 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 4eb2aab0..2c4f85f8 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 f55db10b..387f3994 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 1e4777b4..57408dc0 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 ffabd847..5fd89718 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/noop.bin b/artifacts/test_program_methods/noop.bin index a268b059..4778dd92 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 44143e79..6027eebc 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/private_pda_claimer.bin b/artifacts/test_program_methods/private_pda_claimer.bin new file mode 100644 index 00000000..d69eff5c Binary files /dev/null and b/artifacts/test_program_methods/private_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index 9cfa53f9..1fa13515 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/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 35fb37b8..465427fe 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 0c26ef2e..2a18f0d3 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 0fb00640..1739c26a 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 730b3e29..491cc858 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 a0001f20..c190dd88 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/nssa/src/state.rs b/nssa/src/state.rs index a75f61f0..fb6b90a7 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2319,7 +2319,7 @@ pub mod tests { /// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the /// second account, leaving position 1 unbound. #[test] - fn mask_3_without_binding_panics() { + fn private_pda_without_binding_fails() { let program = Program::simple_balance_transfer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); @@ -2333,12 +2333,12 @@ pub mod tests { true, AccountId::new([0; 32]), ); - let mask3_account = + let private_pda_account = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); let visibility_mask = [0, 3]; let result = execute_and_prove( - vec![public_account_1, mask3_account], + vec![public_account_1, private_pda_account], Program::serialize_instruction(10_u128).unwrap(), visibility_mask.to_vec(), vec![(npk, shared_secret)], @@ -2356,7 +2356,7 @@ pub mod tests { /// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim /// and binds the supplied npk to the `account_id`. #[test] - fn mask_3_private_pda_claim_succeeds() { + fn private_pda_claim_succeeds() { let program = Program::pda_claimer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); @@ -2387,7 +2387,7 @@ pub mod tests { /// An npk is supplied that does not match the `pre_state`'s `account_id` under /// `private_pda_account_id(program, claim_seed, npk)`. The claim equality check rejects. #[test] - fn mask_3_wallet_npk_mismatch_panics() { + fn private_pda_npk_mismatch_fails() { // `keys_a` produces the `pre_state`'s `account_id` (the registered pair), `keys_b` is // the mismatched pair supplied in `private_account_keys` for that pre_state. let program = Program::pda_claimer(); @@ -2423,7 +2423,7 @@ pub mod tests { /// is established via the private derivation /// `private_pda_account_id(delegator, seed, npk) == pre.account_id`. #[test] - fn caller_pda_seeds_authorize_mask_3_private_pda_for_callee() { + fn caller_pda_seeds_authorize_private_pda_for_callee() { let delegator = Program::private_pda_delegator(); let noop = Program::noop(); let keys = test_private_account_keys_1(); @@ -2458,7 +2458,7 @@ pub mod tests { /// was set to `true` by the delegator but no proven source supports it, so the consistency /// assertion rejects. #[test] - fn caller_pda_seeds_with_wrong_seed_rejects_mask_3_for_callee() { + fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() { let delegator = Program::private_pda_delegator(); let noop = Program::noop(); let keys = test_private_account_keys_1(); @@ -2495,7 +2495,7 @@ pub mod tests { /// here: after the first claim records `(program, seed) → PDA_alice`, the second claim /// tries to record `(program, seed) → PDA_bob` and panics. #[test] - fn two_mask_3_claims_under_same_seed_are_rejected() { + fn two_private_pda_claims_under_same_seed_are_rejected() { let program = Program::two_pda_claimer(); let keys_a = test_private_account_keys_1(); let keys_b = test_private_account_keys_2(); @@ -2529,13 +2529,12 @@ pub mod tests { /// post-loop `private_pda_bound_positions` assertion in /// `privacy_preserving_circuit.rs`: `noop` emits no `Claim::Pda` and there is no caller /// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires. - // // TODO: a follow-up PR in the Private PDAs series needs to let the wallet supply a // `(seed, original_owner_program_id)` side input per mask-3 `pre_state` so the circuit // can re-verify `private_pda_account_id(owner, seed, npk) == pre.account_id` without a // claim. #[test] - fn mask_3_reuse_across_txs_currently_unsupported() { + fn private_pda_top_level_reuse_rejected_by_binding_check() { let program = Program::noop(); let keys = test_private_account_keys_1(); let npk = keys.npk(); diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 435a0221..d24f9b2b 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -25,16 +25,14 @@ struct ExecutionState { timestamp_validity_window: TimestampValidityWindow, /// Positions (in `pre_states`) of mask-3 accounts whose supplied npk has been bound to /// their `AccountId` via a proven `private_pda_account_id(program_id, seed, npk)` check. - /// Two proof paths populate this set: - /// 1. A `Claim::Pda(seed)` in a program's post_state on that `pre_state`. - /// 2. A caller's `ChainedCall.pda_seeds` entry matching that `pre_state` under the - /// private derivation. - /// Binding is an idempotent property, not an event: the same position can legitimately be - /// bound through both paths in the same tx (e.g. a program claims a private PDA and then - /// delegates it to a callee), and the set uses `contains`, not `assert!(insert)`. After - /// the main loop, every mask-3 position must appear in this set; otherwise the npk is - /// unbound and the circuit rejects. - mask3_bound_positions: HashSet, + /// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on + /// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state` + /// under the private derivation. Binding is an idempotent property, not an event: the same + /// position can legitimately be bound through both paths in the same tx (e.g. a program + /// claims a private PDA and then delegates it to a callee), and the set uses `contains`, + /// not `assert!(insert)`. After the main loop, every mask-3 position must appear in this + /// set; otherwise the npk is unbound and the circuit rejects. + private_pda_bound_positions: HashSet, /// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one /// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and /// one private PDA per distinct npk. Without this check, a single `pda_seeds: [S]` entry in @@ -50,7 +48,7 @@ impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( visibility_mask: &[u8], - mask3_npk_by_position: &HashMap, + private_pda_npk_by_position: &HashMap, program_id: ProgramId, program_outputs: Vec, ) -> Self { @@ -88,7 +86,7 @@ impl ExecutionState { post_states: HashMap::new(), block_validity_window, timestamp_validity_window, - mask3_bound_positions: HashSet::new(), + private_pda_bound_positions: HashSet::new(), pda_family_binding: HashMap::new(), }; @@ -167,7 +165,7 @@ impl ExecutionState { execution_state.validate_and_sync_states( visibility_mask, - mask3_npk_by_position, + private_pda_npk_by_position, chained_call.program_id, caller_program_id, &chained_call.pda_seeds, @@ -191,8 +189,8 @@ impl ExecutionState { for (pos, &mask) in visibility_mask.iter().enumerate() { if mask == 3 { assert!( - execution_state.mask3_bound_positions.contains(&pos), - "mask-3 pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" + execution_state.private_pda_bound_positions.contains(&pos), + "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" ); } } @@ -229,7 +227,7 @@ impl ExecutionState { fn validate_and_sync_states( &mut self, visibility_mask: &[u8], - mask3_npk_by_position: &HashMap, + private_pda_npk_by_position: &HashMap, program_id: ProgramId, caller_program_id: Option, caller_pda_seeds: &[PdaSeed], @@ -274,27 +272,27 @@ impl ExecutionState { // 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. - // 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)> = - caller_program_id.and_then(|caller| { + // 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::from((&caller, seed)) == pre_account_id { - return Some((*seed, false)); + return Some((*seed, false, caller)); } - if let Some(npk) = mask3_npk_by_position.get(&pre_state_position) + if let Some(npk) = + private_pda_npk_by_position.get(&pre_state_position) && private_pda_account_id(&caller, seed, npk) == pre_account_id { - return Some((*seed, true)); + return Some((*seed, true, caller)); } None }) }); - if let Some((seed, is_private_form)) = matched_caller_seed { - let caller = caller_program_id - .expect("matched caller seed implies caller_program_id is set"); + if let Some((seed, is_private_form, caller)) = matched_caller_seed { assert_family_binding( &mut self.pda_family_binding, caller, @@ -302,7 +300,7 @@ impl ExecutionState { pre_account_id, ); if is_private_form { - self.mask3_bound_positions.insert(pre_state_position); + self.private_pda_bound_positions.insert(pre_state_position); } } @@ -366,15 +364,15 @@ impl ExecutionState { ); } Claim::Pda(seed) => { - let npk = mask3_npk_by_position - .get(&pre_state_position) - .expect("mask-3 pre_state must have an npk in the position map"); + 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 = private_pda_account_id(&program_id, &seed, npk); assert_eq!( pre_account_id, pda, "Invalid private PDA claim for account {pre_account_id}" ); - self.mask3_bound_positions.insert(pre_state_position); + self.private_pda_bound_positions.insert(pre_state_position); assert_family_binding( &mut self.pda_family_binding, program_id, @@ -579,7 +577,7 @@ fn compute_circuit_output( // `pre_state.account_id` upstream in `validate_and_sync_states`, either via a // `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which // assert `private_pda_account_id(owner, seed, npk) == account_id`. The post-loop - // assertion in `derive_from_outputs` (see the `mask3_bound_positions` check) + // assertion in `derive_from_outputs` (see the `private_pda_bound_positions` check) // guarantees that every mask-3 position has been through at least one such // binding, so this branch can safely use the wallet npk without re-verifying. let Some((npk, shared_secret)) = private_keys_iter.next() else { @@ -715,7 +713,7 @@ fn main() { // 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 mask3_npk_by_position: HashMap = HashMap::new(); + 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() { @@ -726,7 +724,7 @@ fn main() { ) }); if mask == 3 { - mask3_npk_by_position.insert(pos, *npk); + private_pda_npk_by_position.insert(pos, *npk); } } } @@ -734,7 +732,7 @@ fn main() { let execution_state = ExecutionState::derive_from_outputs( &visibility_mask, - &mask3_npk_by_position, + &private_pda_npk_by_position, program_id, program_outputs, );