feat(wallet): pad private transactions to a fixed maximum

This commit is contained in:
Artem Gureev 2026-07-07 11:52:58 +00:00 committed by agureev
parent a7511eecfc
commit 301273dd44
7 changed files with 85 additions and 18 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<AccountIdentity>,
@ -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<DummyInput> {
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<State>) -> 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<State> = std::iter::repeat_with(private_state).take(max).collect();
assert_eq!(manager(full).dummy_inputs_default().len(), 0);
let over: Vec<State> = std::iter::repeat_with(private_state)
.take(max + 2)
.collect();
assert_eq!(manager(over).dummy_inputs_default().len(), 0);
}
}

View File

@ -557,13 +557,9 @@ impl WalletCore {
instruction_data: InstructionData,
program: &ProgramWithDependencies,
) -> Result<(HashType, Vec<SharedSecretKey>), 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<SharedSecretKey>), 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(),
)?;

View File

@ -24,7 +24,6 @@ impl NativeTokenTransfer<'_> {
instruction_data,
&program.into(),
tx_pre_check,
0,
)
.await
.map(|(resp, secrets)| {

View File

@ -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)| {

View File

@ -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)| {