diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 48ca489e..53e74aae 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 05201e8a..faec156d 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 ee02b905..b3679d7f 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 24bc3f1b..50af623d 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 3ef46501..892af356 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 1a111269..af0505b6 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 65ac6317..23cafe17 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 3658fe9c..47b8aad6 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 716dfe1c..449a1827 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 907b1d93..5063ff86 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 960ef2df..ffbe1be5 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 384ee42d..b8e112a9 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 aaad5393..4bd7e371 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 17bf2895..ce262ad3 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 94e56f7b..2dded1eb 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 2a307c46..f7abd7d8 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 e16ddef0..3b521d52 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 3cf31f3a..ea6e4079 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 86d3a8a3..b7c7443d 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 17309bc6..f965a588 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 912374ce..9f0f596d 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 8960f975..81dca34b 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 ac2fc0b4..187b6914 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 1a80ee0c..70ce9cb0 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 90491f16..5eb3b82a 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 b3a438e8..153ac1fb 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 1e96f5a6..5fd0ec0f 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_claimer.bin b/artifacts/test_program_methods/private_pda_claimer.bin deleted file mode 100644 index d69eff5c..00000000 Binary files a/artifacts/test_program_methods/private_pda_claimer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin new file mode 100644 index 00000000..7155e3ab Binary files /dev/null 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 35e51a53..2da131e6 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 886e787e..b42ab836 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 0e18c188..e0dbd7f3 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/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index da4ba82e..86ffa307 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 cddf4ac2..6c332016 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/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs index 4c582b34..716e5c29 100644 --- a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs +++ b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs @@ -52,7 +52,6 @@ fn main() { instruction_data: chained_call_instruction_data, pre_states, pda_seeds: vec![], - private_pda_seeds: vec![], }; // Write the outputs. diff --git a/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs index 05cdfaa5..5ec9aaab 100644 --- a/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs +++ b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs @@ -65,7 +65,6 @@ fn main() { instruction_data: chained_call_instruction_data, pre_states: vec![pre_state_for_chained_call], pda_seeds: vec![PDA_SEED], - private_pda_seeds: vec![], }; // Write the outputs. diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 84f0397a..51741da4 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -65,14 +65,13 @@ pub struct ChainedCall { pub pre_states: Vec, /// The instruction data to pass. pub instruction_data: InstructionData, - /// Public PDA seeds authorized for the callee. Each derives an `AccountId` via - /// `AccountId::from((&caller_program_id, seed))`. + /// PDA seeds authorized for the callee. For each callee `pre_state`, the outer circuit + /// checks whether its `AccountId` matches a public PDA derivation + /// `AccountId::from((&caller, seed))` (mask 0) or a private PDA derivation + /// `private_pda_account_id(&caller, seed, npk)` (mask 3, where `npk` is the wallet-supplied + /// npk for that `pre_state`). Programs stay privacy-agnostic: they emit seeds, the circuit + /// resolves public vs private based on the `pre_state`'s mask. pub pda_seeds: Vec, - /// Private PDA `(seed, npk)` pairs authorized for the callee. Each derives an `AccountId` - /// via `private_pda_account_id(&caller_program_id, seed, npk)`. The npk binds the - /// authorization to a specific group of controllers and is part of the caller program's - /// Risc0-proven output, so the outer circuit can trust it. - pub private_pda_seeds: Vec<(PdaSeed, NullifierPublicKey)>, } impl ChainedCall { @@ -88,7 +87,6 @@ impl ChainedCall { instruction_data: risc0_zkvm::serde::to_vec(instruction) .expect("Serialization to Vec should not fail"), pda_seeds: Vec::new(), - private_pda_seeds: Vec::new(), } } @@ -97,15 +95,6 @@ impl ChainedCall { self.pda_seeds = pda_seeds; self } - - #[must_use] - pub fn with_private_pda_seeds( - mut self, - private_pda_seeds: Vec<(PdaSeed, NullifierPublicKey)>, - ) -> Self { - self.private_pda_seeds = private_pda_seeds; - self - } } /// Represents the final state of an `Account` after a program execution. @@ -131,16 +120,13 @@ pub enum Claim { /// This will give no error if program had authorization in pre state and may be useful /// if program decides to give up authorization for a chained call. Authorized, - /// The program requests ownership of the account through a public PDA. The `AccountId` is - /// `AccountId::from((&program_id, &seed))`. + /// The program requests ownership of the account through a PDA. The `pre_state`'s + /// visibility mask selects the derivation formula: mask 0 uses + /// `AccountId::from((&program_id, &seed))`, mask 3 uses + /// `private_pda_account_id(&program_id, &seed, &npk)` with the wallet-supplied npk for that + /// `pre_state`. Programs stay privacy-agnostic: they emit a seed, the circuit resolves the + /// rest from the mask. Pda(PdaSeed), - /// The program requests ownership of the account through a private PDA. The `AccountId` is - /// `private_pda_account_id(&program_id, &seed, &npk)`. The npk is part of the program's - /// Risc0-proven output, so the outer circuit can trust it. - PrivatePda { - seed: PdaSeed, - npk: NullifierPublicKey, - }, } impl AccountPostState { @@ -530,27 +516,24 @@ pub fn private_pda_account_id( ) } -/// Computes the set of PDA `AccountId`s the callee is authorized to mutate. +/// Computes the set of public-PDA `AccountId`s the callee is authorized to mutate. /// -/// `pda_seeds` produces public PDAs. `private_pda_seeds` produces private PDAs whose derivation -/// includes the caller-supplied npk. All seeds and npks must come from the caller's Risc0-proven -/// [`ChainedCall`], so the outer circuit can trust them. +/// Returns only public-form derivations, suitable for contexts where all accounts are public +/// (e.g. the public-execution path). The privacy circuit must additionally check each mask-3 +/// `pre_state` against `private_pda_account_id(caller, seed, npk)` with the wallet-supplied +/// npk for that `pre_state`. #[must_use] pub fn compute_authorized_pdas( caller_program_id: Option, pda_seeds: &[PdaSeed], - private_pda_seeds: &[(PdaSeed, NullifierPublicKey)], ) -> HashSet { let Some(caller) = caller_program_id else { return HashSet::new(); }; - let public = pda_seeds + pda_seeds .iter() - .map(|seed| AccountId::from((&caller, seed))); - let private = private_pda_seeds - .iter() - .map(|(seed, npk)| private_pda_account_id(&caller, seed, npk)); - public.chain(private).collect() + .map(|seed| AccountId::from((&caller, seed))) + .collect() } /// Reads the NSSA inputs from the guest environment. @@ -944,50 +927,22 @@ mod tests { // ---- compute_authorized_pdas tests ---- - /// With no private PDA seeds, `compute_authorized_pdas` returns public PDA addresses only. + /// `compute_authorized_pdas` returns the public PDA addresses for the caller's seeds. #[test] - fn compute_authorized_pdas_public_only() { + fn compute_authorized_pdas_with_seeds() { let caller: ProgramId = [1; 8]; let seed = PdaSeed::new([2; 32]); - let result = compute_authorized_pdas(Some(caller), &[seed], &[]); + let result = compute_authorized_pdas(Some(caller), &[seed]); let expected = AccountId::from((&caller, &seed)); assert!(result.contains(&expected)); assert_eq!(result.len(), 1); } - /// Private PDA seeds produce private PDA `AccountId`s via the `npk`-inclusive derivation. - #[test] - fn compute_authorized_pdas_private_only() { - let caller: ProgramId = [1; 8]; - let seed = PdaSeed::new([2; 32]); - let npk = NullifierPublicKey([3; 32]); - let result = compute_authorized_pdas(Some(caller), &[], &[(seed, npk)]); - let expected = private_pda_account_id(&caller, &seed, &npk); - assert!(result.contains(&expected)); - let public_id = AccountId::from((&caller, &seed)); - assert!(!result.contains(&public_id)); - assert_eq!(result.len(), 1); - } - - /// Public and private seeds can coexist in a single chained call; both are authorized. - #[test] - fn compute_authorized_pdas_public_and_private() { - let caller: ProgramId = [1; 8]; - let pub_seed = PdaSeed::new([2; 32]); - let priv_seed = PdaSeed::new([4; 32]); - let npk = NullifierPublicKey([3; 32]); - let result = compute_authorized_pdas(Some(caller), &[pub_seed], &[(priv_seed, npk)]); - assert!(result.contains(&AccountId::from((&caller, &pub_seed)))); - assert!(result.contains(&private_pda_account_id(&caller, &priv_seed, &npk))); - assert_eq!(result.len(), 2); - } - /// With no caller (top-level call), the result is always empty. #[test] fn compute_authorized_pdas_no_caller_returns_empty() { let seed = PdaSeed::new([2; 32]); - let npk = NullifierPublicKey([3; 32]); - let result = compute_authorized_pdas(None, &[seed], &[(seed, npk)]); + let result = compute_authorized_pdas(None, &[seed]); assert!(result.is_empty()); } } diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 55a0623b..565e02ba 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -129,12 +129,6 @@ pub enum InvalidProgramBehaviorError { actual: AccountId, }, - #[error("Private PDA claim mismatch: expected {expected:?}, actual {actual:?}")] - MismatchedPrivatePdaClaim { - expected: AccountId, - actual: AccountId, - }, - #[error("Default account {account_id} was modified without being claimed")] DefaultAccountModifiedWithoutClaim { account_id: AccountId }, diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 049de9b9..528bb372 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -85,7 +85,6 @@ pub fn execute_and_prove( instruction_data, pre_states, pda_seeds: vec![], - private_pda_seeds: vec![], }; let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]); diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 11d1c025..129979ee 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -292,16 +292,6 @@ mod tests { } } - #[must_use] - pub fn private_pda_claimer() -> Self { - use test_program_methods::{PRIVATE_PDA_CLAIMER_ELF, PRIVATE_PDA_CLAIMER_ID}; - - Self { - id: PRIVATE_PDA_CLAIMER_ID, - elf: PRIVATE_PDA_CLAIMER_ELF.to_vec(), - } - } - #[must_use] pub fn pda_claimer() -> Self { use test_program_methods::{PDA_CLAIMER_ELF, PDA_CLAIMER_ID}; @@ -312,6 +302,16 @@ mod tests { } } + #[must_use] + pub fn private_pda_delegator() -> Self { + use test_program_methods::{PRIVATE_PDA_DELEGATOR_ELF, PRIVATE_PDA_DELEGATOR_ID}; + + Self { + id: PRIVATE_PDA_DELEGATOR_ID, + elf: PRIVATE_PDA_DELEGATOR_ELF.to_vec(), + } + } + #[must_use] pub fn changer_claimer() -> Self { use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID}; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index b2d62448..2fbfa7d9 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2314,12 +2314,16 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// A mask-3 account with no proven `Claim::PrivatePda` or `ChainedCall.private_pda_seeds` - /// attestation must be rejected by the circuit, since there is no binding from which to verify - /// its npk. + /// A mask-3 account that no program claims via `Claim::Pda` and no caller authorizes via + /// `ChainedCall.pda_seeds` has no binding between its wallet-supplied npk and its + /// `account_id`, 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() { let program = Program::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); let public_account_1 = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -2329,31 +2333,31 @@ pub mod tests { true, AccountId::new([0; 32]), ); - let public_account_2 = + let mask3_account = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); let visibility_mask = [0, 3]; let result = execute_and_prove( - vec![public_account_1, public_account_2], + vec![public_account_1, mask3_account], Program::serialize_instruction(10_u128).unwrap(), visibility_mask.to_vec(), + vec![(npk, shared_secret)], vec![], - vec![], - vec![], + vec![None], &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// Happy path: a program claims a new mask-3 account via `Claim::PrivatePda { seed, npk }`. - /// The circuit derives the `AccountId` via `private_pda_account_id(program_id, seed, npk)` - /// and matches it against the proven claim; the wallet-supplied npk in `private_account_keys` - /// matches the attested npk from the bindings map; a commitment, nullifier and ciphertext are - /// produced. + /// Happy path: a program claims a new mask-3 account via `Claim::Pda(seed)`. The circuit + /// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s + /// position, derives `AccountId` via `private_pda_account_id(program_id, seed, npk)`, and + /// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim + /// and binds the wallet-supplied npk to the `account_id`. #[test] fn mask_3_private_pda_claim_succeeds() { - let program = Program::private_pda_claimer(); + let program = Program::pda_claimer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); @@ -2364,7 +2368,7 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], - Program::serialize_instruction((seed, npk)).unwrap(), + Program::serialize_instruction(seed).unwrap(), vec![3], vec![(npk, shared_secret)], vec![], @@ -2380,11 +2384,11 @@ pub mod tests { assert!(output.public_post_states.is_empty()); } - /// The wallet supplies an npk in `private_account_keys` that differs from the npk attested - /// by the program's `Claim::PrivatePda`. The circuit's mask-3 binding check must reject. + /// The wallet supplies an npk 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() { - let program = Program::private_pda_claimer(); + let program = Program::pda_claimer(); let attested_keys = test_private_account_keys_1(); let wallet_keys = test_private_account_keys_2(); let attested_npk = attested_keys.npk(); @@ -2392,14 +2396,15 @@ pub mod tests { let seed = PdaSeed::new([42; 32]); let shared_secret = SharedSecretKey::new(&[55; 32], &wallet_keys.vpk()); - // The account_id derives from the attested npk (what the program claims). The wallet - // supplies a different npk in private_account_keys, which must fail the binding check. + // account_id is derived from `attested_npk`, but the wallet provides `wallet_npk` for + // this pre_state. `private_pda_account_id(program, seed, wallet_npk) != account_id`, so + // the claim check in the circuit must reject. let account_id = private_pda_account_id(&program.id(), &seed, &attested_npk); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let result = execute_and_prove( vec![pre_state], - Program::serialize_instruction((seed, attested_npk)).unwrap(), + Program::serialize_instruction(seed).unwrap(), vec![3], vec![(wallet_npk, shared_secret)], vec![], @@ -2410,52 +2415,112 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// A program must not be allowed to claim a mask-0 (public) account via `Claim::PrivatePda`. - /// The circuit panics in `validate_and_sync_states` when the visibility and claim kind - /// disagree. + /// Happy path for the caller-seeds authorization of a mask-3 PDA. The delegator claims a + /// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same + /// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state` is already in + /// `self.pre_states` (Occupied branch) and its authorization is established via the private + /// derivation `private_pda_account_id(delegator, seed, npk) == pre.account_id`. This exercises + /// the `authorized_via_private` path in `validate_and_sync_states`. #[test] - fn mask_0_cannot_be_claimed_as_private_pda_panics() { - let program = Program::private_pda_claimer(); + fn caller_pda_seeds_authorize_mask_3_private_pda_for_callee() { + let delegator = Program::private_pda_delegator(); + let noop = Program::noop(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); + let seed = PdaSeed::new([77; 32]); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); - // Public account: program_owner = DEFAULT, account_id arbitrary. - let pre_state = - AccountWithMetadata::new(Account::default(), false, AccountId::new([7; 32])); + let account_id = private_pda_account_id(&delegator.id(), &seed, &npk); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let noop_id = noop.id(); + let program_with_deps = ProgramWithDependencies::new(delegator, [(noop_id, noop)].into()); let result = execute_and_prove( vec![pre_state], - Program::serialize_instruction((seed, npk)).unwrap(), - vec![0], + Program::serialize_instruction((seed, seed, noop_id)).unwrap(), + vec![3], + vec![(npk, shared_secret)], vec![], + vec![None], + &program_with_deps, + ); + + let (output, _proof) = + result.expect("caller-seeds authorization of mask-3 private PDA should succeed"); + assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.new_nullifiers.len(), 1); + } + + /// The delegator chains with a different seed than the one it claimed with. In the callee + /// step, neither public nor private caller-seeds authorization matches; `pre.is_authorized` + /// 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() { + let delegator = Program::private_pda_delegator(); + let noop = Program::noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let claim_seed = PdaSeed::new([77; 32]); + let wrong_delegated_seed = PdaSeed::new([88; 32]); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let account_id = private_pda_account_id(&delegator.id(), &claim_seed, &npk); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let noop_id = noop.id(); + let program_with_deps = ProgramWithDependencies::new(delegator, [(noop_id, noop)].into()); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction((claim_seed, wrong_delegated_seed, noop_id)).unwrap(), + vec![3], + vec![(npk, shared_secret)], vec![], - vec![], - &program.into(), + vec![None], + &program_with_deps, ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// A program must not be allowed to claim a mask-3 (private PDA) account via `Claim::Pda`. - /// Private PDAs use a distinct derivation and must be claimed with `Claim::PrivatePda`. + /// Pins the current limitation: a mask-3 PDA that was claimed in a previous transaction + /// cannot be re-used in a new transaction as-is, because this PR only binds wallet-supplied + /// npks via a fresh `Claim::Pda` or a caller's `ChainedCall.pda_seeds` — neither is present + /// when a program operates on an already-owned private PDA at top level. The circuit rejects. + /// + /// 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_cannot_be_claimed_as_public_pda_panics() { - let program = Program::pda_claimer(); - let seed = PdaSeed::new([42; 32]); + fn mask_3_reuse_across_txs_currently_unsupported() { + let program = Program::noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let seed = PdaSeed::new([99; 32]); - // The account_id does not need to match any private-PDA derivation; the circuit panics on - // the mask-3 / `Claim::Pda` mismatch before any derivation check. - let pre_state = - AccountWithMetadata::new(Account::default(), false, AccountId::new([7; 32])); + // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = true, + // account_id derived via the private formula. + let account_id = private_pda_account_id(&program.id(), &seed, &npk); + let owned_pre_state = AccountWithMetadata::new( + Account { + program_owner: program.id(), + ..Account::default() + }, + true, + account_id, + ); let result = execute_and_prove( - vec![pre_state], - Program::serialize_instruction(seed).unwrap(), + vec![owned_pre_state], + Program::serialize_instruction(()).unwrap(), vec![3], + vec![(npk, shared_secret)], vec![], - vec![], - vec![], + vec![None], &program.into(), ); diff --git a/nssa/src/validated_state_diff.rs b/nssa/src/validated_state_diff.rs index 744402ee..46f240ea 100644 --- a/nssa/src/validated_state_diff.rs +++ b/nssa/src/validated_state_diff.rs @@ -8,8 +8,7 @@ use nssa_core::{ BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, account::{Account, AccountId, AccountWithMetadata}, program::{ - ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_authorized_pdas, private_pda_account_id, - validate_execution, + ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_authorized_pdas, validate_execution, }, }; @@ -99,7 +98,6 @@ impl ValidatedStateDiff { instruction_data: message.instruction_data.clone(), pre_states: input_pre_states, pda_seeds: vec![], - private_pda_seeds: vec![], }; let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); @@ -130,11 +128,8 @@ impl ValidatedStateDiff { chained_call.program_id, program_output ); - let authorized_pdas = compute_authorized_pdas( - caller_program_id, - &chained_call.pda_seeds, - &chained_call.private_pda_seeds, - ); + let authorized_pdas = + compute_authorized_pdas(caller_program_id, &chained_call.pda_seeds); let is_authorized = |account_id: &AccountId| { signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id) @@ -228,7 +223,8 @@ impl ValidatedStateDiff { } Claim::Pda(seed) => { // The program can only claim accounts that correspond to the PDAs it is - // authorized to claim. + // authorized to claim. The public-execution path only sees mask-0 + // accounts, so the public-PDA derivation is the correct formula here. let pda = AccountId::from((&chained_call.program_id, &seed)); ensure!( account_id == pda, @@ -238,16 +234,6 @@ impl ValidatedStateDiff { } ); } - Claim::PrivatePda { seed, npk } => { - let pda = private_pda_account_id(&chained_call.program_id, &seed, &npk); - ensure!( - account_id == pda, - InvalidProgramBehaviorError::MismatchedPrivatePdaClaim { - expected: pda, - actual: account_id - } - ); - } } post.account_mut().program_owner = chained_call.program_id; diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index b7aa399b..734a452c 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -23,18 +23,20 @@ struct ExecutionState { post_states: HashMap, block_validity_window: BlockValidityWindow, timestamp_validity_window: TimestampValidityWindow, - /// Map from private-PDA `AccountId` to the npk used to derive it, sourced entirely from - /// Risc0-proven `Claim::PrivatePda` in `post_states` and `private_pda_seeds` in chained - /// calls. `compute_circuit_output` uses this to verify that the npk supplied via - /// `private_account_keys` for a mask-3 account matches the npk attested by some program's - /// proof. - private_pda_bindings: HashMap, + /// Positions (in `pre_states`) of mask-3 accounts whose wallet-supplied npk has been bound + /// to their `AccountId` via a proven `private_pda_account_id(program_id, seed, npk)` check. + /// The binding happens when the circuit validates a `Claim::Pda(seed)` on that `pre_state`, + /// or when it authorizes that `pre_state` via a caller's `ChainedCall.pda_seeds`. 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, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( visibility_mask: &[u8], + mask3_npk_by_position: &HashMap, program_id: ProgramId, program_outputs: Vec, ) -> Self { @@ -72,7 +74,7 @@ impl ExecutionState { post_states: HashMap::new(), block_validity_window, timestamp_validity_window, - private_pda_bindings: HashMap::new(), + mask3_bound_positions: HashSet::new(), }; let Some(first_output) = program_outputs.first() else { @@ -84,7 +86,6 @@ impl ExecutionState { instruction_data: first_output.instruction_data.clone(), pre_states: first_output.pre_states.clone(), pda_seeds: Vec::new(), - private_pda_seeds: Vec::new(), }; let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); @@ -145,40 +146,21 @@ impl ExecutionState { ); } - // Collect private-PDA bindings from this program_output's proven data. Each - // `private_pda_seeds` entry in an outgoing chained call attests that the caller - // (this program) authorizes the callee to mutate the PDA derived from - // `(self_program_id, seed, npk)`. Each `Claim::PrivatePda` in this program's - // post_states attests that it claims the PDA derived from the same formula with - // its own program_id. - for next_call in &program_output.chained_calls { - for (seed, npk) in &next_call.private_pda_seeds { - let account_id = private_pda_account_id(&chained_call.program_id, seed, npk); - execution_state - .private_pda_bindings - .insert(account_id, *npk); - } - } - for post in &program_output.post_states { - if let Some(Claim::PrivatePda { seed, npk }) = post.required_claim() { - let account_id = private_pda_account_id(&chained_call.program_id, &seed, &npk); - execution_state.private_pda_bindings.insert(account_id, npk); - } - } - for next_call in program_output.chained_calls.iter().rev() { chained_calls.push_front((next_call.clone(), Some(chained_call.program_id))); } - let authorized_pdas = nssa_core::program::compute_authorized_pdas( + let authorized_public_pdas = nssa_core::program::compute_authorized_pdas( caller_program_id, &chained_call.pda_seeds, - &chained_call.private_pda_seeds, ); execution_state.validate_and_sync_states( visibility_mask, + mask3_npk_by_position, chained_call.program_id, - &authorized_pdas, + caller_program_id, + &chained_call.pda_seeds, + &authorized_public_pdas, program_output.pre_states, program_output.post_states, ); @@ -192,6 +174,19 @@ impl ExecutionState { "Inner call without a chained call found", ); + // Every mask-3 pre_state must have had its npk bound to its account_id, either via a + // `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching + // the private derivation. An unbound mask-3 pre_state has no cryptographic link between + // the wallet-supplied npk and the account_id, and must be rejected. + 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" + ); + } + } + // Check that all modified uninitialized accounts were claimed for (account_id, post) in execution_state .pre_states @@ -217,11 +212,15 @@ 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], + mask3_npk_by_position: &HashMap, program_id: ProgramId, - authorized_pdas: &HashSet, + caller_program_id: Option, + caller_pda_seeds: &[nssa_core::program::PdaSeed], + authorized_public_pdas: &HashSet, pre_states: Vec, post_states: Vec, ) { @@ -248,19 +247,40 @@ impl ExecutionState { "Inconsistent pre state for account {pre_account_id}", ); - let previous_is_authorized = self + let (previous_is_authorized, pre_state_position) = self .pre_states .iter() - .find(|acc| acc.account_id == pre_account_id) + .enumerate() + .find(|(_, acc)| acc.account_id == pre_account_id) .map_or_else( || panic!( "Pre state must exist in execution state for account {pre_account_id}", ), - |acc| acc.is_authorized + |(pos, acc)| (acc.is_authorized, pos) ); + let authorized_via_public = authorized_public_pdas.contains(&pre_account_id); + // Mask-3 PDAs are authorized by matching a caller seed against the private + // derivation with this pre_state's npk. The equality check binds the npk. + // 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 authorized_via_private = mask3_npk_by_position + .get(&pre_state_position) + .and_then(|npk| { + let caller = caller_program_id?; + caller_pda_seeds.iter().find(|seed| { + private_pda_account_id(&caller, seed, npk) == pre_account_id + })?; + Some(()) + }) + .is_some(); + if authorized_via_private { + self.mask3_bound_positions.insert(pre_state_position); + } + let is_authorized = - previous_is_authorized || authorized_pdas.contains(&pre_account_id); + previous_is_authorized || authorized_via_public || authorized_via_private; assert_eq!( pre_is_authorized, is_authorized, @@ -287,10 +307,9 @@ impl ExecutionState { .position(|acc| acc.account_id == pre_account_id) .expect("Pre state must exist at this point"); - let is_public_account = visibility_mask[pre_state_position] == 0; - let is_private_pda = visibility_mask[pre_state_position] == 3; - if is_public_account { - match claim { + let mask = visibility_mask[pre_state_position]; + match mask { + 0 => match claim { Claim::Authorized => { // Note: no need to check authorized pdas because we have already // checked consistency of authorization above. @@ -306,35 +325,31 @@ impl ExecutionState { "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" ); } - Claim::PrivatePda { .. } => { - panic!( - "Public account {pre_account_id} cannot be claimed via Claim::PrivatePda" - ); - } - } - } else if is_private_pda { - match claim { + }, + 3 => match claim { Claim::Authorized => { assert!( pre_is_authorized, "Cannot claim unauthorized private PDA {pre_account_id}" ); } - Claim::PrivatePda { seed, npk } => { - let pda = private_pda_account_id(&program_id, &seed, &npk); + 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 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); } - Claim::Pda(_) => { - panic!( - "Private PDA {pre_account_id} must be claimed via Claim::PrivatePda, not Claim::Pda" - ); - } + }, + _ => { + // Mask 1/2: standard private accounts don't enforce the claim semantics. + // Unauthorized private claiming is intentionally allowed since operating + // these accounts requires the npk/nsk keypair anyway. } - } else { - // Mask 1/2: standard private accounts don't use PDA claims. } post.account_mut().program_owner = program_id; @@ -359,7 +374,7 @@ impl ExecutionState { } fn compute_circuit_output( - mut execution_state: ExecutionState, + execution_state: ExecutionState, visibility_mask: &[u8], private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], private_account_nsks: &[NullifierSecretKey], @@ -374,7 +389,6 @@ fn compute_circuit_output( block_validity_window: execution_state.block_validity_window, timestamp_validity_window: execution_state.timestamp_validity_window, }; - let private_pda_bindings = std::mem::take(&mut execution_state.private_pda_bindings); let states_iter = execution_state.into_states_iter(); assert_eq!( @@ -495,21 +509,16 @@ fn compute_circuit_output( .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); } 3 => { - // Private PDA account. The npk supplied via private_account_keys must match the - // npk attested by some program's Risc0-proven output (either a `Claim::PrivatePda` - // in post_states or a `private_pda_seeds` entry in a chained call). The bindings - // map is built entirely from proven data in `derive_from_outputs`. + // Private PDA account. The wallet-supplied npk has already been bound to + // `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) + // 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 { panic!("Missing private account key"); }; - let attested_npk = private_pda_bindings.get(&pre_state.account_id).expect( - "mask-3 account must be attested by a proven Claim::PrivatePda or ChainedCall.private_pda_seeds entry", - ); - assert_eq!( - npk, attested_npk, - "Private PDA npk does not match proven attestation for {}", - pre_state.account_id - ); let (new_nullifier, new_nonce) = if pre_state.is_authorized { // Existing private PDA with authentication (like mask 1) @@ -636,8 +645,33 @@ fn main() { program_id, } = env::read(); - let execution_state = - ExecutionState::derive_from_outputs(&visibility_mask, program_id, program_outputs); + // 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 mask3_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 { + mask3_npk_by_position.insert(pos, *npk); + } + } + } + } + + let execution_state = ExecutionState::derive_from_outputs( + &visibility_mask, + &mask3_npk_by_position, + program_id, + program_outputs, + ); let output = compute_circuit_output( execution_state, diff --git a/test_program_methods/guest/src/bin/chain_caller.rs b/test_program_methods/guest/src/bin/chain_caller.rs index 35545ab0..5c124bed 100644 --- a/test_program_methods/guest/src/bin/chain_caller.rs +++ b/test_program_methods/guest/src/bin/chain_caller.rs @@ -41,7 +41,6 @@ fn main() { instruction_data: instruction_data.clone(), pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], /* <- Account order permutation here */ pda_seeds: pda_seed.iter().copied().collect(), - private_pda_seeds: vec![], }; chained_calls.push(new_chained_call); diff --git a/test_program_methods/guest/src/bin/clock_chain_caller.rs b/test_program_methods/guest/src/bin/clock_chain_caller.rs index b745b32f..cdbe5214 100644 --- a/test_program_methods/guest/src/bin/clock_chain_caller.rs +++ b/test_program_methods/guest/src/bin/clock_chain_caller.rs @@ -32,7 +32,6 @@ fn main() { instruction_data: to_vec(×tamp).unwrap(), pre_states: pre_states.clone(), pda_seeds: vec![], - private_pda_seeds: vec![], }; ProgramOutput::new( diff --git a/test_program_methods/guest/src/bin/flash_swap_callback.rs b/test_program_methods/guest/src/bin/flash_swap_callback.rs index 05b39c03..251833bb 100644 --- a/test_program_methods/guest/src/bin/flash_swap_callback.rs +++ b/test_program_methods/guest/src/bin/flash_swap_callback.rs @@ -71,7 +71,6 @@ fn main() { pre_states: vec![receiver_authorized, vault_pre.clone()], instruction_data: transfer_instruction, pda_seeds: vec![PdaSeed::new([1_u8; 32])], - private_pda_seeds: vec![], }); } // Malicious path (return_funds = false): emit no chained calls. diff --git a/test_program_methods/guest/src/bin/flash_swap_initiator.rs b/test_program_methods/guest/src/bin/flash_swap_initiator.rs index 7d1c56df..27d1f317 100644 --- a/test_program_methods/guest/src/bin/flash_swap_initiator.rs +++ b/test_program_methods/guest/src/bin/flash_swap_initiator.rs @@ -129,7 +129,6 @@ fn main() { pre_states: vec![vault_authorized, receiver_pre.clone()], instruction_data: transfer_instruction, pda_seeds: vec![PdaSeed::new([0_u8; 32])], - private_pda_seeds: vec![], }; // Chained call 2: User callback. @@ -140,7 +139,6 @@ fn main() { pre_states: vec![vault_after_transfer, receiver_after_transfer], instruction_data: callback_instruction_data, pda_seeds: vec![], - private_pda_seeds: vec![], }; // Chained call 3: Self-call to enforce the invariant. @@ -159,7 +157,6 @@ fn main() { pre_states: vec![vault_after_callback], instruction_data: invariant_instruction, pda_seeds: vec![], - private_pda_seeds: vec![], }; // The initiator itself makes no direct state changes. diff --git a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs index be4ad509..f7aba4a0 100644 --- a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs +++ b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs @@ -39,7 +39,6 @@ fn main() { instruction_data, pre_states: vec![authorised_sender, receiver.clone()], pda_seeds: vec![], - private_pda_seeds: vec![], }; ProgramOutput::new( diff --git a/test_program_methods/guest/src/bin/private_pda_claimer.rs b/test_program_methods/guest/src/bin/private_pda_claimer.rs deleted file mode 100644 index d63eb030..00000000 --- a/test_program_methods/guest/src/bin/private_pda_claimer.rs +++ /dev/null @@ -1,34 +0,0 @@ -use nssa_core::{ - NullifierPublicKey, - program::{AccountPostState, Claim, PdaSeed, ProgramInput, ProgramOutput, read_nssa_inputs}, -}; - -type Instruction = (PdaSeed, NullifierPublicKey); - -fn main() { - let ( - ProgramInput { - self_program_id, - caller_program_id, - pre_states, - instruction: (seed, npk), - }, - instruction_words, - ) = read_nssa_inputs::(); - - let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { - return; - }; - - let account_post = - AccountPostState::new_claimed(pre.account.clone(), Claim::PrivatePda { seed, npk }); - - ProgramOutput::new( - self_program_id, - caller_program_id, - instruction_words, - vec![pre], - vec![account_post], - ) - .write(); -} diff --git a/test_program_methods/guest/src/bin/private_pda_delegator.rs b/test_program_methods/guest/src/bin/private_pda_delegator.rs new file mode 100644 index 00000000..fe55045e --- /dev/null +++ b/test_program_methods/guest/src/bin/private_pda_delegator.rs @@ -0,0 +1,51 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, Claim, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; +use risc0_zkvm::serde::to_vec; + +/// Claims the sole `pre_state` as a PDA with `claim_seed`, then chains to `callee_program_id` +/// delegating authorization with `delegated_seed` in `pda_seeds`. When `claim_seed == +/// delegated_seed` this exercises the happy caller-seeds authorization path for mask-3 private +/// PDAs in `validate_and_sync_states`; when they differ, the callee's mask-3 `pre_state` has +/// no matching authorization source and the circuit must reject. +type Instruction = (PdaSeed, PdaSeed, ProgramId); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (claim_seed, delegated_seed, callee_program_id), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { + return; + }; + + let claimed = AccountPostState::new_claimed(pre.account.clone(), Claim::Pda(claim_seed)); + + let mut pre_for_callee = pre.clone(); + pre_for_callee.is_authorized = true; + pre_for_callee.account.program_owner = self_program_id; + + let chained_call = ChainedCall { + program_id: callee_program_id, + instruction_data: to_vec(&()).unwrap(), + pre_states: vec![pre_for_callee], + pda_seeds: vec![delegated_seed], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pre], + vec![claimed], + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs index ecc94db4..212418a2 100644 --- a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs +++ b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs @@ -37,7 +37,6 @@ fn main() { instruction_data: chained_instruction, pre_states, pda_seeds: vec![], - private_pda_seeds: vec![], }; ProgramOutput::new(