diff --git a/.deny.toml b/.deny.toml index 320a9eda..fb1ce3cf 100644 --- a/.deny.toml +++ b/.deny.toml @@ -16,7 +16,6 @@ ignore = [ { id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" }, { id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, { id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration" }, - { id = "RUSTSEC-2026-0145", reason = "`astral-tokio-tar` v0.6.1 is pulled transitively via testcontainers (integration_tests dev/test path); waiting on upstream fix" }, ] yanked = "deny" unused-ignored-advisory = "deny" diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 3fbea6d0..2635cab1 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 e6cdba59..155f2ba8 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 0cdaf90d..812a5ced 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 06b983ce..62e16cb9 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/faucet.bin b/artifacts/program_methods/faucet.bin index c4a82241..9b1fbbe4 100644 Binary files a/artifacts/program_methods/faucet.bin and b/artifacts/program_methods/faucet.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index d8634938..6c8849e6 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 adfd3cb6..baf619e3 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 5aaae0ea..16c5a34b 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 f11ca891..d7e11257 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/program_methods/vault.bin b/artifacts/program_methods/vault.bin index 4bf1bfdf..b2cdea20 100644 Binary files a/artifacts/program_methods/vault.bin and b/artifacts/program_methods/vault.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 20a85c7c..7116dbbb 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/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin index eb1cfff0..3dc5a5ec 100644 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and b/artifacts/test_program_methods/auth_transfer_proxy.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 521149d9..5f8209b0 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 460209d4..e2164be6 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 67f29a28..c99ca2c0 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 351680f5..dc44c14f 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 e084b74a..19a43bbc 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 5a4e11c2..98d3ea85 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 6ebf389c..fdd80cbd 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/faucet_chain_caller.bin b/artifacts/test_program_methods/faucet_chain_caller.bin index beeae731..656e7ab5 100644 Binary files a/artifacts/test_program_methods/faucet_chain_caller.bin and b/artifacts/test_program_methods/faucet_chain_caller.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index 202b551c..b4e35f49 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 744c864b..ae9d945f 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 351e9ec6..828813e3 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 29a8604a..bd996ff8 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 b10e1781..3cb564f2 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 5268710c..d90292f8 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 7b6de7b0..dfca513a 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 41559320..d4698fdf 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 7d1ae5fa..df524beb 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 63c1a0e3..be0f301b 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 3c42374d..81a5de3e 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/pda_fund_spend_proxy.bin b/artifacts/test_program_methods/pda_fund_spend_proxy.bin index 03e85c2e..913c8146 100644 Binary files a/artifacts/test_program_methods/pda_fund_spend_proxy.bin and b/artifacts/test_program_methods/pda_fund_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pda_spend_proxy.bin b/artifacts/test_program_methods/pda_spend_proxy.bin new file mode 100644 index 00000000..9366e66a Binary files /dev/null and b/artifacts/test_program_methods/pda_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 8b92c83c..b65ce430 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 db60530e..8e1857f8 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 40a5087b..0ea6687a 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 949f4ac7..183b8ee2 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 2f87b681..02d776f2 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 2e7cb900..5598072f 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 0a0371fe..0485135f 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 a711fb3d..86d71dfb 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/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index feb5e5e8..a77ccf34 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -704,6 +704,7 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> { npk, ssk, identifier: 1337, + seed: None, }, ], &program_with_deps, diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index f9969d98..84bfcb2f 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -6,27 +6,37 @@ use std::{path::PathBuf, time::Duration}; use anyhow::{Context as _, Result}; +use authenticated_transfer_core::Instruction as AuthTransferInstruction; +use common::transaction::NSSATransaction; use integration_tests::{ - NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, + NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, verify_commitment_is_in_state, }; +use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use log::info; use nssa::{ - AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies, + AccountId, PrivacyPreservingTransaction, ProgramId, + privacy_preserving_transaction::{ + circuit::{ProgramWithDependencies, execute_and_prove}, + message::Message, + witness_set::WitnessSet, + }, program::Program, }; -use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey, program::PdaSeed}; +use nssa_core::{ + InputAccountIdentity, NullifierPublicKey, + account::{Account, AccountWithMetadata}, + encryption::ViewingPublicKey, + program::PdaSeed, +}; +use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::{ PrivacyPreservingAccount, WalletCore, cli::{Command, account::AccountSubcommand}, }; -/// Funds a private PDA via the proxy program with a chained call to `auth_transfer`. -/// -/// A direct call to `auth_transfer` cannot establish the PDA-to-npk binding because it uses -/// `Claim::Authorized` rather than `Claim::Pda`. Routing through the proxy provides the binding -/// via `pda_seeds` in the chained call to `auth_transfer`. +/// Funds a private PDA by calling `auth_transfer` directly. #[expect( clippy::too_many_arguments, reason = "test helper — grouping args would obscure intent" @@ -34,32 +44,68 @@ use wallet::{ async fn fund_private_pda( wallet: &WalletCore, sender: AccountId, - pda_account_id: AccountId, npk: NullifierPublicKey, vpk: ViewingPublicKey, identifier: u128, seed: PdaSeed, + authority_program_id: ProgramId, amount: u128, - proxy_program: &ProgramWithDependencies, - auth_transfer_id: ProgramId, + auth_transfer: &ProgramWithDependencies, ) -> Result<()> { - wallet - .send_privacy_preserving_tx( - vec![ - PrivacyPreservingAccount::Public(sender), - PrivacyPreservingAccount::PrivatePdaForeign { - account_id: pda_account_id, - npk, - vpk, - identifier, - }, - ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)) - .context("failed to serialize pda_fund_spend_proxy fund instruction")?, - proxy_program, - ) + let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier); + let sender_account = wallet + .get_account_public(sender) .await - .map_err(|e| anyhow::anyhow!("{e}"))?; + .map_err(|e| anyhow::anyhow!("failed to get sender account: {e}"))?; + let sender_sk = wallet + .get_account_public_signing_key(sender) + .context("sender signing key not found")?; + + let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender); + let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id); + + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + + let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .context("failed to serialize auth_transfer instruction")?; + + let account_identities = vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk, + ssk, + identifier, + seed: Some((seed, authority_program_id)), + }, + ]; + + let (output, proof) = execute_and_prove( + vec![sender_pre, pda_pre], + instruction, + account_identities, + auth_transfer, + ) + .map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?; + + let message = Message::try_from_circuit_output( + vec![sender], + vec![sender_account.nonce], + vec![(npk, vpk, epk)], + output, + ) + .map_err(|e| anyhow::anyhow!("message build failed: {e}"))?; + + let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + wallet + .sequencer_client + .send_transaction(NSSATransaction::PrivacyPreserving(tx)) + .await + .map_err(|e| anyhow::anyhow!("send transaction failed: {e}"))?; + Ok(()) } @@ -78,7 +124,7 @@ async fn spend_private_pda( seed: PdaSeed, amount: u128, spend_program: &ProgramWithDependencies, - auth_transfer_id: nssa::ProgramId, + auth_transfer_id: ProgramId, ) -> Result<()> { wallet .send_privacy_preserving_tx( @@ -90,8 +136,8 @@ async fn spend_private_pda( identifier: 0, }, ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)) - .context("failed to serialize pda_fund_spend_proxy instruction")?, + Program::serialize_instruction((seed, amount, auth_transfer_id)) + .context("failed to serialize pda_spend_proxy instruction")?, spend_program, ) .await @@ -124,9 +170,9 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { let proxy = { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../artifacts/test_program_methods") - .join(NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY); + .join(NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY); Program::new(std::fs::read(&path).with_context(|| format!("reading {path:?}"))?) - .context("invalid pda_fund_spend_proxy binary")? + .context("invalid pda_spend_proxy binary")? }; let auth_transfer = Program::authenticated_transfer_program(); let proxy_id = proxy.id(); @@ -134,6 +180,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { let seed = PdaSeed::new([42; 32]); let amount: u128 = 100; + let auth_transfer_program = ProgramWithDependencies::new(auth_transfer.clone(), [].into()); let spend_program = ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); @@ -151,14 +198,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { fund_private_pda( ctx.wallet(), sender_0, - alice_pda_0_id, alice_npk, alice_vpk.clone(), 0, seed, + proxy_id, amount, - &spend_program, - auth_transfer_id, + &auth_transfer_program, ) .await?; @@ -166,14 +212,13 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { fund_private_pda( ctx.wallet(), sender_1, - alice_pda_1_id, alice_npk, alice_vpk.clone(), 1, seed, + proxy_id, amount, - &spend_program, - auth_transfer_id, + &auth_transfer_program, ) .await?; diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 63c188ef..b1c2e44f 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -5,7 +5,7 @@ use crate::{ NullifierSecretKey, SharedSecretKey, account::{Account, AccountWithMetadata}, encryption::Ciphertext, - program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow}, + program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow}, }; #[derive(Serialize, Deserialize)] @@ -60,15 +60,28 @@ pub enum InputAccountIdentity { npk: NullifierPublicKey, ssk: SharedSecretKey, identifier: Identifier, + /// When `Some((seed, authority_program_id))`, the circuit binds this position via the + /// external derivation check + /// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) == + /// pre_state.account_id` rather than requiring a `Claim::Pda` or caller + /// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized + /// == false`. + seed: Option<(PdaSeed, ProgramId)>, }, - /// Update of an existing private PDA, authorized, with membership proof. `npk` is derived - /// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a + /// Update of an existing private PDA, with membership proof. `npk` is derived + /// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a /// previously-seen authorization in a chained call. PrivatePdaUpdate { ssk: SharedSecretKey, nsk: NullifierSecretKey, membership_proof: MembershipProof, identifier: Identifier, + /// When `Some((seed, authority_program_id))`, the circuit binds this position via the + /// external derivation check + /// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) == + /// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish + /// the binding. The `pre_state` must have `is_authorized == false`. + seed: Option<(PdaSeed, ProgramId)>, }, } diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 915c8d3e..902f5eaa 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -461,6 +461,7 @@ mod tests { npk, ssk: shared_secret, identifier, + seed: None, }], &program.clone().into(), ) @@ -488,7 +489,7 @@ mod tests { let seed = PdaSeed::new([42; 32]); let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); - // PDA (new, mask 3) + // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); @@ -506,6 +507,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + seed: None, }], &program_with_deps, ); @@ -557,6 +559,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + seed: None, }, InputAccountIdentity::Public, ], @@ -747,7 +750,7 @@ mod tests { /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. #[test] fn private_pda_update_encrypts_pda_kind_with_identifier() { - let program = Program::pda_fund_spend_proxy(); + let program = Program::pda_spend_proxy(); let auth_transfer = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let npk = keys.npk(); @@ -784,6 +787,7 @@ mod tests { nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier, + seed: None, }, InputAccountIdentity::Public, ], @@ -819,6 +823,7 @@ mod tests { npk, ssk: shared_secret, identifier: 99, + seed: None, }], &program.into(), ); @@ -828,7 +833,7 @@ mod tests { #[test] fn private_pda_update_identifier_mismatch_fails() { - let program = Program::pda_fund_spend_proxy(); + let program = Program::pda_spend_proxy(); let auth_transfer = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let npk = keys.npk(); @@ -862,6 +867,7 @@ mod tests { nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier: 99, + seed: None, }, InputAccountIdentity::Public, ], diff --git a/nssa/src/program.rs b/nssa/src/program.rs index c3c92f1e..696c2086 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -350,12 +350,12 @@ mod tests { } #[must_use] - pub fn pda_fund_spend_proxy() -> Self { - use test_program_methods::{PDA_FUND_SPEND_PROXY_ELF, PDA_FUND_SPEND_PROXY_ID}; + pub fn pda_spend_proxy() -> Self { + use test_program_methods::{PDA_SPEND_PROXY_ELF, PDA_SPEND_PROXY_ID}; Self { - id: PDA_FUND_SPEND_PROXY_ID, - elf: PDA_FUND_SPEND_PROXY_ELF.to_vec(), + id: PDA_SPEND_PROXY_ID, + elf: PDA_SPEND_PROXY_ELF.to_vec(), } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index e9f2058f..0f38f9f3 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -2218,7 +2218,7 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// A mask-3 account that no program claims via `Claim::Pda` and no caller authorizes via + /// A private PDA account that no program claims via `Claim::Pda` and no caller authorizes via /// `ChainedCall.pda_seeds` has no binding between its 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. @@ -2249,6 +2249,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }, ], &program.into(), @@ -2257,7 +2258,7 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// Happy path: a program claims a new mask-3 account via `Claim::Pda(seed)`. The circuit + /// Happy path: a program claims a new private PDA 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 `AccountId::for_private_pda(program_id, seed, npk)`, and /// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim @@ -2280,11 +2281,12 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program.into(), ); - let (output, _proof) = result.expect("mask-3 private PDA claim should succeed"); + let (output, _proof) = result.expect("private PDA claim should succeed"); assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.ciphertexts.len(), 1); @@ -2319,6 +2321,7 @@ pub mod tests { npk: npk_b, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program.into(), ); @@ -2326,7 +2329,7 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// Happy path for the caller-seeds authorization of a mask-3 PDA. The delegator claims a + /// Happy path for the caller-seeds authorization of a private 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`'s authorization /// is established via the private derivation @@ -2354,12 +2357,13 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program_with_deps, ); let (output, _proof) = - result.expect("caller-seeds authorization of mask-3 private PDA should succeed"); + result.expect("caller-seeds authorization of private PDA should succeed"); assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.new_nullifiers.len(), 1); } @@ -2392,6 +2396,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program_with_deps, ); @@ -2401,8 +2406,8 @@ pub mod tests { /// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of /// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide - /// family-binding check, a program could claim `PDA_alice` (mask-3, `alice_npk`) and - /// `PDA_bob` (mask-3, `bob_npk`) under the same seed in one transaction, and once reuse + /// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and + /// `PDA_bob` (`bob_npk`) under the same seed in one transaction, and once reuse /// is supported a later chained call could delegate both to a callee via /// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup /// here: after the first claim records `(program, seed) → PDA_alice`, the second claim @@ -2430,11 +2435,13 @@ pub mod tests { npk: keys_a.npk(), ssk: shared_a, identifier: u128::MAX, + seed: None, }, InputAccountIdentity::PrivatePdaInit { npk: keys_b.npk(), ssk: shared_b, identifier: u128::MAX, + seed: None, }, ], &program.into(), @@ -2443,17 +2450,11 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } - /// 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. This PR only binds 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 reject site is the - /// post-loop `private_pda_bound_positions` assertion in - /// `privacy_preserving_circuit.rs`: `noop` emits no `Claim::Pda` and there is no caller + /// A private PDA that is reused at top level without an external seed in the identity still + /// fails binding. The noop program 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 `AccountId::for_private_pda(owner, seed, npk) == pre.account_id` without a - // claim. + /// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is + /// the correct path for top-level reuse; this test pins the failure when no seed is provided. #[test] fn private_pda_top_level_reuse_rejected_by_binding_check() { let program = Program::noop(); @@ -2481,6 +2482,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + seed: None, }], &program.into(), ); @@ -4372,15 +4374,15 @@ pub mod tests { let alice_keys = test_private_account_keys_1(); let alice_npk = alice_keys.npk(); - let proxy = Program::pda_fund_spend_proxy(); + let proxy = Program::pda_spend_proxy(); let auth_transfer = Program::authenticated_transfer_program(); let proxy_id = proxy.id(); let auth_transfer_id = auth_transfer.id(); let seed = PdaSeed::new([42; 32]); let amount: u128 = 100; - let program_with_deps = - ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); + let spend_with_deps = + ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer.clone())].into()); let funder_id = funder_keys.account_id(); let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); @@ -4406,7 +4408,7 @@ pub mod tests { let alice_shared_0 = SharedSecretKey::new([10; 32], &alice_keys.vpk()); let alice_shared_1 = SharedSecretKey::new([11; 32], &alice_keys.vpk()); - // Fund alice_pda_0 + // Fund alice_pda_0 via authenticated_transfer directly. { let funder_account = state.get_account_by_id(funder_id); let funder_nonce = funder_account.nonce; @@ -4415,16 +4417,18 @@ pub mod tests { AccountWithMetadata::new(funder_account, true, funder_id), AccountWithMetadata::new(Account::default(), false, alice_pda_0_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { npk: alice_npk, ssk: alice_shared_0, identifier: 0, + seed: Some((seed, proxy_id)), }, ], - &program_with_deps, + &auth_transfer.clone().into(), ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4448,7 +4452,7 @@ pub mod tests { .unwrap(); } - // Fund alice_pda_1 + // Fund alice_pda_1 the same way with identifier 1. { let funder_account = state.get_account_by_id(funder_id); let funder_nonce = funder_account.nonce; @@ -4457,16 +4461,18 @@ pub mod tests { AccountWithMetadata::new(funder_account, true, funder_id), AccountWithMetadata::new(Account::default(), false, alice_pda_1_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { npk: alice_npk, ssk: alice_shared_1, identifier: 1, + seed: Some((seed, proxy_id)), }, ], - &program_with_deps, + &auth_transfer.into(), ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4504,7 +4510,7 @@ pub mod tests { AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), AccountWithMetadata::new(recipient_account, true, recipient_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { ssk: alice_shared_0, @@ -4513,10 +4519,11 @@ pub mod tests { .get_proof_for_commitment(&commitment_pda_0) .expect("pda_0 must be in state"), identifier: 0, + seed: None, }, InputAccountIdentity::Public, ], - &program_with_deps, + &spend_with_deps, ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4545,10 +4552,10 @@ pub mod tests { let recipient_account = state.get_account_by_id(recipient_id); let (output, proof) = execute_and_prove( vec![ - AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id), + AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id), AccountWithMetadata::new(recipient_account, false, recipient_id), ], - Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { ssk: alice_shared_1, @@ -4557,10 +4564,11 @@ pub mod tests { .get_proof_for_commitment(&commitment_pda_1) .expect("pda_1 must be in state"), identifier: 1, + seed: None, }, InputAccountIdentity::Public, ], - &program_with_deps, + &spend_with_deps, ) .unwrap(); let message = Message::try_from_circuit_output( @@ -4585,5 +4593,70 @@ pub mod tests { } assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount); + + // Re-fund alice_pda_1 top-level via auth_transfer using PrivatePdaUpdate with an + // external seed. + let alice_pda_1_account_after_spend = Account { + program_owner: auth_transfer_id, + balance: 0, + nonce: alice_pda_1_account + .nonce + .private_account_nonce_increment(&alice_keys.nsk), + ..Account::default() + }; + let commitment_pda_1_after_spend = + Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend); + let alice_shared_1_refund = SharedSecretKey::new([12; 32], &alice_keys.vpk()); + { + let recipient_account = state.get_account_by_id(recipient_id); + let recipient_nonce = recipient_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(recipient_account, true, recipient_id), + AccountWithMetadata::new( + alice_pda_1_account_after_spend, + false, + alice_pda_1_id, + ), + ], + Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) + .unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaUpdate { + nsk: alice_keys.nsk, + ssk: alice_shared_1_refund, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1_after_spend) + .expect("pda_1 after spend must be in state"), + identifier: 1, + seed: Some((seed, proxy_id)), + }, + ], + &Program::authenticated_transfer_program().into(), + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![recipient_id], + vec![recipient_nonce], + vec![( + alice_npk, + alice_keys.vpk(), + EphemeralPublicKey::from_scalar([12; 32]), + )], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 5, + 0, + ) + .unwrap(); + } + + assert_eq!(state.get_account_by_id(recipient_id).balance, amount); } } diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs b/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs index c65b1d29..c06698d6 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs @@ -305,6 +305,68 @@ impl ExecutionState { } Entry::Vacant(_) => { // Pre state for the initial call + let pre_state_position = self.pre_states.len(); + let external_seed = match account_identities.get(pre_state_position) { + Some(InputAccountIdentity::PrivatePdaInit { + npk, + identifier, + seed: Some((seed, authority_program_id)), + .. + }) => { + let expected = AccountId::for_private_pda( + authority_program_id, + seed, + npk, + *identifier, + ); + assert_eq!( + pre_account_id, expected, + "External seed mismatch for PrivatePdaInit at position {pre_state_position}" + ); + Some((*seed, *authority_program_id)) + } + Some(InputAccountIdentity::PrivatePdaUpdate { + nsk, + identifier, + seed: Some((seed, authority_program_id)), + .. + }) => { + let npk = NullifierPublicKey::from(nsk); + let expected = AccountId::for_private_pda( + authority_program_id, + seed, + &npk, + *identifier, + ); + assert_eq!( + pre_account_id, expected, + "External seed mismatch for PrivatePdaUpdate at position {pre_state_position}" + ); + Some((*seed, *authority_program_id)) + } + _ => None, + }; + // External seed is only consulted the first time the account is seen. + // Subsequent calls need no re-check because the entry is already recorded on + // private_pda_bound_positions. + if let Some((seed, authority_program_id)) = external_seed { + assert!( + !pre.is_authorized, + "Private PDA with externally-provided seed must not be authorized at position {pre_state_position}" + ); + bind_private_pda_position( + &mut self.private_pda_bound_positions, + pre_state_position, + authority_program_id, + seed, + ); + assert_family_binding( + &mut self.pda_family_binding, + authority_program_id, + seed, + pre_account_id, + ); + } self.pre_states.push(pre); } } @@ -348,14 +410,11 @@ impl ExecutionState { ); } } - } else if account_identity.is_private_pda() { + } else { + // Private accounts: don't enforce the claim semantics. Unauthorized private + // claiming is intentionally allowed match claim { - Claim::Authorized => { - assert!( - pre_is_authorized, - "Cannot claim unauthorized private PDA {pre_account_id}" - ); - } + Claim::Authorized => {} Claim::Pda(seed) => { let (npk, identifier) = self .private_pda_npk_by_position @@ -383,10 +442,6 @@ impl ExecutionState { ); } } - } else { - // Standalone private accounts: don't enforce the claim semantics. - // Unauthorized private claiming is intentionally allowed since operating - // these accounts requires the npk/nsk keypair anyway. } post.account_mut().program_owner = program_id; diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs index f5a6d1f9..6e302401 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs @@ -148,6 +148,7 @@ pub fn compute_circuit_output( npk: _, ssk, identifier, + seed: _, } => { // The npk-to-account_id binding is established upstream in // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` @@ -172,7 +173,7 @@ pub fn compute_circuit_output( let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); let account_id = pre_state.account_id; - let (pda_program_id, seed) = pda_seed_by_position + let (authority_program_id, seed) = pda_seed_by_position .get(&pos) .expect("PrivatePdaInit position must be in pda_seed_by_position"); emit_private_output( @@ -181,7 +182,7 @@ pub fn compute_circuit_output( post_state, &account_id, &PrivateAccountKind::Pda { - program_id: *pda_program_id, + program_id: *authority_program_id, seed: *seed, identifier: *identifier, }, @@ -195,14 +196,16 @@ pub fn compute_circuit_output( nsk, membership_proof, identifier, + seed: external_seed, } => { - // The npk binding is established upstream. Authorization must already be set; - // an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an - // unbound PDA, which the upstream binding check would have rejected anyway, - // but we assert here to fail fast and document the precondition. + // With an external seed the binding comes from the circuit input and the + // pre_state is intentionally unauthorized; without one the binding comes from + // a Claim or caller pda_seeds, so the pre_state must already be authorized. + // When `external_seed` is `Some`, execution_state already asserted + // `!pre_state.is_authorized`. assert!( - pre_state.is_authorized, - "PrivatePdaUpdate requires authorized pre_state" + pre_state.is_authorized ^ external_seed.is_some(), + "PrivatePdaUpdate requires authorized pre_state or external seed" ); let new_nullifier = compute_update_nullifier_and_set_digest( @@ -214,7 +217,7 @@ pub fn compute_circuit_output( let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); let account_id = pre_state.account_id; - let (pda_program_id, seed) = pda_seed_by_position + let (authority_program_id, seed) = pda_seed_by_position .get(&pos) .expect("PrivatePdaUpdate position must be in pda_seed_by_position"); emit_private_output( @@ -223,7 +226,7 @@ pub fn compute_circuit_output( post_state, &account_id, &PrivateAccountKind::Pda { - program_id: *pda_program_id, + program_id: *authority_program_id, seed: *seed, identifier: *identifier, }, diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs index 2c9dfb3a..ed05954c 100644 --- a/test_fixtures/src/lib.rs +++ b/test_fixtures/src/lib.rs @@ -34,7 +34,7 @@ pub mod setup; pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin"; -pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin"; +pub const NSSA_PROGRAM_FOR_TEST_PDA_SPEND_PROXY: &str = "pda_spend_proxy.bin"; pub(crate) const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0"; pub(crate) const BEDROCK_SERVICE_PORT: u16 = 18080; diff --git a/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs b/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs deleted file mode 100644 index 567f9af1..00000000 --- a/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs +++ /dev/null @@ -1,71 +0,0 @@ -use nssa_core::{ - account::AccountWithMetadata, - program::{ - AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, - read_nssa_inputs, - }, -}; -use risc0_zkvm::serde::to_vec; - -/// Proxy for interacting with private PDAs via `auth_transfer`. -/// -/// The `is_fund` flag selects the operating mode: -/// -/// - `false` (Spend): `pre_states = [pda (authorized), recipient]`. Debits the PDA. The PDA-to-npk -/// binding is established via `pda_seeds` in the chained call to `auth_transfer`. -/// -/// - `true` (Fund): `pre_states = [sender (authorized), pda (foreign/uninitialized)]`. Credits the -/// PDA. A direct call to `auth_transfer` cannot bind the PDA because `auth_transfer` uses -/// `Claim::Authorized`, not `Claim::Pda`. Routing through this proxy establishes the binding via -/// `pda_seeds` in the chained call. -type Instruction = (PdaSeed, u128, ProgramId, bool); - -fn main() { - let ( - ProgramInput { - self_program_id, - caller_program_id, - pre_states, - instruction: (seed, amount, auth_transfer_id, is_fund), - }, - instruction_words, - ) = read_nssa_inputs::(); - - let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else { - return; - }; - - assert!(first.is_authorized, "first pre_state must be authorized"); - - let chained_pre_states = if is_fund { - let pda_authorized = AccountWithMetadata { - account: second.account.clone(), - account_id: second.account_id, - is_authorized: true, - }; - vec![first.clone(), pda_authorized] - } else { - vec![first.clone(), second.clone()] - }; - - let first_post = AccountPostState::new(first.account.clone()); - let second_post = AccountPostState::new(second.account.clone()); - - let chained_call = ChainedCall { - program_id: auth_transfer_id, - instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount }) - .unwrap(), - pre_states: chained_pre_states, - pda_seeds: vec![seed], - }; - - ProgramOutput::new( - self_program_id, - caller_program_id, - instruction_words, - vec![first, second], - vec![first_post, second_post], - ) - .with_chained_calls(vec![chained_call]) - .write(); -} diff --git a/test_program_methods/guest/src/bin/pda_spend_proxy.rs b/test_program_methods/guest/src/bin/pda_spend_proxy.rs new file mode 100644 index 00000000..4094e101 --- /dev/null +++ b/test_program_methods/guest/src/bin/pda_spend_proxy.rs @@ -0,0 +1,50 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; +use risc0_zkvm::serde::to_vec; + +/// Proxy for spending from a private PDA via `auth_transfer`. +/// +/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient. +/// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `auth_transfer`. +type Instruction = (PdaSeed, u128, ProgramId); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (seed, amount, auth_transfer_id), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else { + return; + }; + + assert!(first.is_authorized, "first pre_state must be authorized"); + + let first_post = AccountPostState::new(first.account.clone()); + let second_post = AccountPostState::new(second.account.clone()); + + let chained_call = ChainedCall { + program_id: auth_transfer_id, + instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount }) + .unwrap(), + pre_states: vec![first.clone(), second.clone()], + pda_seeds: vec![seed], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![first, second], + vec![first_post, second_post], + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/tools/integration_bench/src/harness.rs b/tools/integration_bench/src/harness.rs index fb9d4d5c..813bbbab 100644 --- a/tools/integration_bench/src/harness.rs +++ b/tools/integration_bench/src/harness.rs @@ -15,7 +15,7 @@ use test_fixtures::{DiskSizes, TestContext}; use wallet::cli::SubcommandReturnValue; const TX_INCLUSION_POLL_INTERVAL: Duration = Duration::from_millis(250); -const TX_INCLUSION_TIMEOUT: Duration = Duration::from_secs(120); +const TX_INCLUSION_TIMEOUT: Duration = Duration::from_mins(2); /// Borsh-serialized sizes for one zone block fetched after a step. `block_bytes` /// is the full Block (header + body + bedrock metadata) and is the closest diff --git a/tools/integration_bench/src/main.rs b/tools/integration_bench/src/main.rs index ccf7058e..4d14f6d1 100644 --- a/tools/integration_bench/src/main.rs +++ b/tools/integration_bench/src/main.rs @@ -181,7 +181,7 @@ async fn measure_bedrock_finality(ctx: &TestContext) -> Result { .context("connect indexer WS")?; let sequencer_tip = ctx.sequencer_client().get_last_block_id().await?; - let timeout = Duration::from_secs(60); + let timeout = Duration::from_mins(1); let started = std::time::Instant::now(); let poll = async { loop { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index bfbe7145..30880445 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -297,7 +297,7 @@ impl WalletCore { .key_chain() .group_key_holder(&entry.group_label)?; - if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.pda_program_id) { + if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.authority_program_id) { let keys = holder.derive_keys_for_pda(&program_id, &pda_seed); Some(PrivacyPreservingAccount::PrivatePdaShared { account_id, @@ -340,7 +340,7 @@ impl WalletCore { group_label: Label, identifier: nssa_core::Identifier, pda_seed: Option, - pda_program_id: Option, + authority_program_id: Option, ) { self.storage.key_chain_mut().insert_shared_private_account( account_id, @@ -348,7 +348,7 @@ impl WalletCore { group_label, identifier, pda_seed, - pda_program_id, + authority_program_id, account: Account::default(), }, ); @@ -729,7 +729,7 @@ impl WalletCore { .key_chain() .group_key_holder(&entry.group_label)?; - let keys = match (&entry.pda_seed, &entry.pda_program_id) { + let keys = match (&entry.pda_seed, &entry.authority_program_id) { (Some(pda_seed), Some(program_id)) => { holder.derive_keys_for_pda(program_id, pda_seed) } diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 005eaf75..865fcdce 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -252,11 +252,13 @@ impl AccountManager { nsk, membership_proof, identifier: pre.identifier, + seed: None, }, _ => InputAccountIdentity::PrivatePdaInit { npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, + seed: None, }, }, State::Private(pre) => match (pre.nsk, pre.proof.clone()) { diff --git a/wallet/src/storage/key_chain.rs b/wallet/src/storage/key_chain.rs index e00dee8d..637884f1 100644 --- a/wallet/src/storage/key_chain.rs +++ b/wallet/src/storage/key_chain.rs @@ -55,7 +55,7 @@ pub struct SharedAccountEntry { /// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`. /// `None` for regular shared accounts (keys derived from identifier via derivation seed). pub pda_seed: Option, - pub pda_program_id: Option, + pub authority_program_id: Option, pub account: Account, } @@ -858,7 +858,7 @@ mod tests { group_label: Label::new("test-group"), identifier: 42, pda_seed: None, - pda_program_id: None, + authority_program_id: None, account: nssa_core::account::Account::default(), }; let encoded = bincode::serialize(&entry).expect("serialize"); @@ -871,7 +871,7 @@ mod tests { group_label: Label::new("pda-group"), identifier: u128::MAX, pda_seed: Some(PdaSeed::new([7_u8; 32])), - pda_program_id: Some([9; 8]), + authority_program_id: Some([9; 8]), account: nssa_core::account::Account::default(), }; let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda"); @@ -890,7 +890,7 @@ mod tests { group_label: Label::new("old"), identifier: 1, pda_seed: None, - pda_program_id: None, + authority_program_id: None, account: nssa_core::account::Account::default(), }; let encoded = bincode::serialize(&entry).expect("serialize");