From 301273dd448053d7e8b939186d4000e31502a518 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Tue, 7 Jul 2026 11:52:58 +0000 Subject: [PATCH] feat(wallet): pad private transactions to a fixed maximum --- .../tests/auth_transfer/private.rs | 3 - integration_tests/tests/keys.rs | 1 - lez/wallet/src/account_manager.rs | 81 +++++++++++++++++++ lez/wallet/src/lib.rs | 13 +-- .../native_token_transfer/deshielded.rs | 1 - .../native_token_transfer/private.rs | 2 - .../native_token_transfer/shielded.rs | 2 - 7 files changed, 85 insertions(+), 18 deletions(-) diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 43444538..03546730 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -83,7 +83,6 @@ async fn private_transfer_to_foreign_account() -> Result<()> { let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; assert_eq!(tx.message.new_commitments[0], new_commitment1); - assert_eq!(tx.message.new_commitments.len(), 2); for commitment in tx.message.new_commitments { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } @@ -172,7 +171,6 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { .context("Failed to get private account commitment for sender")?; assert_eq!(tx.message.new_commitments[0], sender_commitment); - assert_eq!(tx.message.new_commitments.len(), 2); for commitment in tx.message.new_commitments { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } @@ -307,7 +305,6 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Verify commitments are in state - assert_eq!(tx.message.new_commitments.len(), 2); for commitment in tx.message.new_commitments { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } diff --git a/integration_tests/tests/keys.rs b/integration_tests/tests/keys.rs index 7fc73c62..1b6a0f70 100644 --- a/integration_tests/tests/keys.rs +++ b/integration_tests/tests/keys.rs @@ -73,7 +73,6 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { .context("Failed to get private account commitment for sender")?; assert_eq!(tx.message.new_commitments[0], new_commitment1); - assert_eq!(tx.message.new_commitments.len(), 2); for commitment in tx.message.new_commitments { assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); } diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index 98d19a88..5374e07b 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -193,6 +193,14 @@ pub struct AccountManager { } impl AccountManager { + /// The private-account count that every privacy-preserving transaction is padded up to with + /// dummy inputs via the default interface. + /// + /// The value is selected based on the largest account number per-tx currently supported + /// (it is 7 for AMM). It is recommended to reassess this value per new actively supported + /// application and that all users share the value for a larger anonymity set. + const MAX_PRIVATE_ACCOUNTS: usize = 7; + pub async fn new( wallet: &WalletCore, accounts: Vec, @@ -410,6 +418,17 @@ impl AccountManager { .collect() } + /// Generate the dummy inputs that pad this transaction's private-account count up to + /// `MAX_PRIVATE_ACCOUNTS`. + pub fn dummy_inputs_default(&self) -> Vec { + let private_count = self + .states + .iter() + .filter(|state| matches!(state, State::Private(_))) + .count(); + self.dummy_inputs(Self::MAX_PRIVATE_ACCOUNTS.saturating_sub(private_count)) + } + /// Build the per-account input vec for the privacy-preserving circuit. Each variant carries /// exactly the fields the circuit's code path for that account needs, with the ephemeral /// keys (`ssk`) drawn from the cached values that `private_account_keys` and the message @@ -684,4 +703,66 @@ mod tests { assert!(acc.is_private()); assert!(!acc.is_public()); } + + fn private_state() -> State { + let npk = NullifierPublicKey([0; 32]); + let vpk = ViewingPublicKey::from_seed(&[0; 32], &[0; 32]); + let pre_state = AccountWithMetadata::new(Account::default(), false, (&npk, &vpk, 0)); + State::Private(AccountPreparedData { + nsk: None, + npk, + identifier: 0, + vpk, + pre_state, + proof: None, + random_seed: [0; 32], + is_pda: false, + }) + } + + fn public_state() -> State { + let npk = NullifierPublicKey([0; 32]); + let vpk = ViewingPublicKey::from_seed(&[0; 32], &[0; 32]); + let account = AccountWithMetadata::new(Account::default(), false, (&npk, &vpk, 0)); + State::Public { account, sk: None } + } + + fn manager(states: Vec) -> AccountManager { + AccountManager { + states, + pin: None, + dummy_commitment_root: [0; 32], + } + } + + #[test] + fn dummy_inputs_default_pads_private_count_to_max() { + let max = AccountManager::MAX_PRIVATE_ACCOUNTS; + + // Empty txs get padded to the max. + assert_eq!(manager(vec![]).dummy_inputs_default().len(), max); + // In a padded transaction, the padding amount depends on + // the amount of private accounts used. + assert_eq!( + manager(vec![private_state(), private_state()]) + .dummy_inputs_default() + .len(), + max - 2 + ); + assert_eq!( + manager(vec![private_state(), public_state(), private_state()]) + .dummy_inputs_default() + .len(), + max - 2 + ); + + // If the private accounts in the transaction exceed the max, no padding + // is done. + let full: Vec = std::iter::repeat_with(private_state).take(max).collect(); + assert_eq!(manager(full).dummy_inputs_default().len(), 0); + let over: Vec = std::iter::repeat_with(private_state) + .take(max + 2) + .collect(); + assert_eq!(manager(over).dummy_inputs_default().len(), 0); + } } diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index cfc3fe18..27839eb4 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -557,13 +557,9 @@ impl WalletCore { instruction_data: InstructionData, program: &ProgramWithDependencies, ) -> Result<(HashType, Vec), ExecutionFailureKind> { - self.send_privacy_preserving_tx_with_pre_check( - accounts, - instruction_data, - program, - |_| Ok(()), - 0, - ) + self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| { + Ok(()) + }) .await } @@ -573,7 +569,6 @@ impl WalletCore { instruction_data: InstructionData, program: &ProgramWithDependencies, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, - dummy_count: usize, ) -> Result<(HashType, Vec), ExecutionFailureKind> { let acc_manager = account_manager::AccountManager::new(self, accounts).await?; @@ -592,7 +587,7 @@ impl WalletCore { pre_states, instruction_data, acc_manager.account_identities(), - acc_manager.dummy_inputs(dummy_count), + acc_manager.dummy_inputs_default(), &program.to_owned(), )?; diff --git a/lez/wallet/src/program_facades/native_token_transfer/deshielded.rs b/lez/wallet/src/program_facades/native_token_transfer/deshielded.rs index be70fe1d..f060e0fa 100644 --- a/lez/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/lez/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -24,7 +24,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - 0, ) .await .map(|(resp, secrets)| { diff --git a/lez/wallet/src/program_facades/native_token_transfer/private.rs b/lez/wallet/src/program_facades/native_token_transfer/private.rs index 52503ced..712d6774 100644 --- a/lez/wallet/src/program_facades/native_token_transfer/private.rs +++ b/lez/wallet/src/program_facades/native_token_transfer/private.rs @@ -58,7 +58,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - 0, ) .await .map(|(resp, secrets)| { @@ -92,7 +91,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - 0, ) .await .map(|(resp, secrets)| { diff --git a/lez/wallet/src/program_facades/native_token_transfer/shielded.rs b/lez/wallet/src/program_facades/native_token_transfer/shielded.rs index fb935ab0..002b1176 100644 --- a/lez/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/lez/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -24,7 +24,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - 0, ) .await .map(|(resp, secrets)| { @@ -58,7 +57,6 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - 0, ) .await .map(|(resp, secrets)| {