diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index 398db4f9..ebc8d365 100644 Binary files a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin and b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin differ diff --git a/artifacts/lez/programs/amm.bin b/artifacts/lez/programs/amm.bin index 18152451..3940bbd2 100644 Binary files a/artifacts/lez/programs/amm.bin and b/artifacts/lez/programs/amm.bin differ diff --git a/artifacts/lez/programs/associated_token_account.bin b/artifacts/lez/programs/associated_token_account.bin index cc0689a3..5313badf 100644 Binary files a/artifacts/lez/programs/associated_token_account.bin and b/artifacts/lez/programs/associated_token_account.bin differ diff --git a/artifacts/lez/programs/authenticated_transfer.bin b/artifacts/lez/programs/authenticated_transfer.bin index 94b93d26..5b22fd1a 100644 Binary files a/artifacts/lez/programs/authenticated_transfer.bin and b/artifacts/lez/programs/authenticated_transfer.bin differ diff --git a/artifacts/lez/programs/bridge.bin b/artifacts/lez/programs/bridge.bin index 9424499c..061cb7c2 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/bridge_lock.bin b/artifacts/lez/programs/bridge_lock.bin index 2c143d37..fcec36f0 100644 Binary files a/artifacts/lez/programs/bridge_lock.bin and b/artifacts/lez/programs/bridge_lock.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index e39955c8..b402bc20 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/cross_zone_inbox.bin b/artifacts/lez/programs/cross_zone_inbox.bin index 610b15f3..3c7de003 100644 Binary files a/artifacts/lez/programs/cross_zone_inbox.bin and b/artifacts/lez/programs/cross_zone_inbox.bin differ diff --git a/artifacts/lez/programs/cross_zone_outbox.bin b/artifacts/lez/programs/cross_zone_outbox.bin index d10ce8c5..8df676a7 100644 Binary files a/artifacts/lez/programs/cross_zone_outbox.bin and b/artifacts/lez/programs/cross_zone_outbox.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index 9b58cb09..ded3feb0 100644 Binary files a/artifacts/lez/programs/faucet.bin and b/artifacts/lez/programs/faucet.bin differ diff --git a/artifacts/lez/programs/pinata.bin b/artifacts/lez/programs/pinata.bin index 9cb14837..24383422 100644 Binary files a/artifacts/lez/programs/pinata.bin and b/artifacts/lez/programs/pinata.bin differ diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin index 6a0d9341..aa88de51 100644 Binary files a/artifacts/lez/programs/pinata_token.bin and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/ping_receiver.bin b/artifacts/lez/programs/ping_receiver.bin index 8b6e83ff..85de2df9 100644 Binary files a/artifacts/lez/programs/ping_receiver.bin and b/artifacts/lez/programs/ping_receiver.bin differ diff --git a/artifacts/lez/programs/ping_sender.bin b/artifacts/lez/programs/ping_sender.bin index 52195fad..fd93cd74 100644 Binary files a/artifacts/lez/programs/ping_sender.bin and b/artifacts/lez/programs/ping_sender.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 3bc7e839..fa57f5f6 100644 Binary files a/artifacts/lez/programs/token.bin and b/artifacts/lez/programs/token.bin differ diff --git a/artifacts/lez/programs/vault.bin b/artifacts/lez/programs/vault.bin index bc821716..1e8bf6b7 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/artifacts/lez/programs/wrapped_token.bin b/artifacts/lez/programs/wrapped_token.bin index b0f37414..d0362994 100644 Binary files a/artifacts/lez/programs/wrapped_token.bin and b/artifacts/lez/programs/wrapped_token.bin differ diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index ddea3ab8..8136f9df 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 10b5710e..f0a66e0e 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/integration_tests/tests/private_transaction_padding.rs b/integration_tests/tests/private_transaction_padding.rs new file mode 100644 index 00000000..d9fceefe --- /dev/null +++ b/integration_tests/tests/private_transaction_padding.rs @@ -0,0 +1,34 @@ +#![expect( + clippy::tests_outside_test_module, + reason = "We don't care about these in tests" +)] + +use anyhow::Result; +use integration_tests::{TestContext, fetch_privacy_preserving_tx, new_account, private_mention}; +use tokio::test; +use wallet::cli::{ + Command, SubcommandReturnValue, programs::native_token_transfer::AuthTransferSubcommand, +}; + +#[test] +async fn private_transaction_pads_notes_to_max() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let account_id = new_account(&mut ctx, true, None).await?; + + let command = Command::AuthTransfer(AuthTransferSubcommand::Init { + account_id: private_mention(account_id), + }); + let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else { + anyhow::bail!("Expected TransactionExecuted return value"); + }; + + let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; + + assert_eq!(tx.message.new_commitments.len(), 7); + assert_eq!(tx.message.new_nullifiers.len(), 7); + assert_eq!(tx.message.encrypted_private_post_states.len(), 7); + + Ok(()) +} diff --git a/lee/privacy_preserving_circuit/src/main.rs b/lee/privacy_preserving_circuit/src/main.rs index a342665d..1fc06157 100644 --- a/lee/privacy_preserving_circuit/src/main.rs +++ b/lee/privacy_preserving_circuit/src/main.rs @@ -9,6 +9,7 @@ fn main() { program_outputs, account_identities, program_id, + dummy_inputs, } = env::read(); let execution_state = execution_state::ExecutionState::derive_from_outputs( @@ -17,7 +18,7 @@ fn main() { program_outputs, ); - let output = output::compute_circuit_output(execution_state, &account_identities); + let output = output::compute_circuit_output(execution_state, &account_identities, dummy_inputs); env::commit(&output); } diff --git a/lee/privacy_preserving_circuit/src/output.rs b/lee/privacy_preserving_circuit/src/output.rs index c5d962d6..9ae530b2 100644 --- a/lee/privacy_preserving_circuit/src/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -1,7 +1,7 @@ use lee_core::{ - Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey, - InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, - PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, + Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme, + EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, + NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, compute_digest_for_path, encryption::{ViewTag, ViewingPublicKey}, @@ -12,6 +12,7 @@ use crate::execution_state::ExecutionState; pub fn compute_circuit_output( execution_state: ExecutionState, account_identities: &[InputAccountIdentity], + dummy_inputs: Vec, ) -> PrivacyPreservingCircuitOutput { let (block_validity_window, timestamp_validity_window, pda_seed_by_position, states_iter) = execution_state.into_parts(); @@ -262,9 +263,31 @@ pub fn compute_circuit_output( } } + for dummy in dummy_inputs { + emit_dummy_output(&mut output, dummy); + } + output } +fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyInput) { + // Note: the nullifiers and commitments are generated from seeds. + // The prover is responsible for their randomness. + let nullifier = Nullifier::for_dummy(&dummy.nullifier_seed); + let commitment = Commitment::for_dummy(&nullifier, &dummy.commitment_seed); + output + .new_nullifiers + .push((nullifier, dummy.commitment_root)); + output.new_commitments.push(commitment); + // Note: the encrypted post states are pushed as fed into the circuit. + // That means that the prover is responsible for managing the randomness + // so as to not reveal the padding. + // + // In particular, it is recommended to generate the ML KEM ciphertext + // explicitly as these are not uniformly random. + output.encrypted_private_post_states.push(dummy.note); +} + #[expect( clippy::too_many_arguments, reason = "Inputs are distinct concerns from the variant arms; bundling would be artificial" diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 8db29342..9b283eae 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -19,6 +19,7 @@ pub struct PrivacyPreservingCircuitInput { pub account_identities: Vec, /// Program ID. pub program_id: ProgramId, + pub dummy_inputs: Vec, } #[derive(Serialize, Deserialize, Clone)] @@ -93,6 +94,20 @@ pub enum InputAccountIdentity { }, } +/// A struct containing necessary data for dummy nullifier and +/// commitment generation. +#[derive(Serialize, Deserialize)] +pub struct DummyInput { + /// The seed used for generating the dummy nullifier. + pub nullifier_seed: [u8; 32], + /// The seed used for generating the dummy commitment. + pub commitment_seed: [u8; 32], + /// The dummy ciphertext, epk, and view tag. + pub note: EncryptedAccountData, + /// The dummy root. + pub commitment_root: CommitmentSetDigest, +} + impl InputAccountIdentity { #[must_use] pub const fn is_public(&self) -> bool { diff --git a/lee/state_machine/core/src/commitment.rs b/lee/state_machine/core/src/commitment.rs index 92085d7d..46b475e4 100644 --- a/lee/state_machine/core/src/commitment.rs +++ b/lee/state_machine/core/src/commitment.rs @@ -2,7 +2,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; -use crate::account::{Account, AccountId}; +use crate::{ + Nullifier, + account::{Account, AccountId}, +}; /// A commitment to all zero data. /// ```python @@ -78,6 +81,15 @@ impl Commitment { bytes.extend_from_slice(&account_bytes_with_hashed_data); Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) } + + #[must_use] + pub fn for_dummy(nullifier: &Nullifier, commitment_seed: &[u8; 32]) -> Self { + const DUMMY_PREFIX: &[u8; 32] = b"/LEE/v0.3/Commitment/Dummy/\x00\x00\x00\x00\x00"; + let mut bytes = DUMMY_PREFIX.to_vec(); + bytes.extend_from_slice(&nullifier.0); + bytes.extend_from_slice(commitment_seed); + Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) + } } pub type CommitmentSetDigest = [u8; 32]; @@ -117,7 +129,7 @@ mod tests { use risc0_zkvm::sha::{Impl, Sha256 as _}; use crate::{ - Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, + Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, Nullifier, account::{Account, AccountId}, }; @@ -138,4 +150,18 @@ mod tests { .unwrap(); assert_eq!(DUMMY_COMMITMENT_HASH, expected_dummy_commitment_hash); } + + #[test] + fn for_dummy_matches_pinned_value() { + let nullifier = Nullifier::for_dummy(&[0; 32]); + let commitment_seed = [1; 32]; + let expected_commitment = Commitment([ + 106, 88, 233, 248, 28, 251, 254, 48, 62, 53, 61, 248, 25, 148, 223, 133, 108, 213, 184, + 83, 73, 145, 122, 104, 89, 220, 111, 132, 40, 87, 12, 105, + ]); + assert_eq!( + Commitment::for_dummy(&nullifier, &commitment_seed), + expected_commitment + ); + } } diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index 4bcb23a4..62dbd3cc 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -4,7 +4,7 @@ )] pub use circuit_io::{ - InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, }; pub use commitment::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, diff --git a/lee/state_machine/core/src/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index 0374a31a..336f275f 100644 --- a/lee/state_machine/core/src/nullifier.rs +++ b/lee/state_machine/core/src/nullifier.rs @@ -109,6 +109,14 @@ impl Nullifier { bytes.extend_from_slice(account_id.value()); Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) } + + #[must_use] + pub fn for_dummy(nullifier_seed: &[u8; 32]) -> Self { + const DUMMY_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Dummy/\x00\x00\x00\x00\x00\x00"; + let mut bytes = DUMMY_PREFIX.to_vec(); + bytes.extend_from_slice(nullifier_seed); + Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()) + } } #[cfg(test)] @@ -209,4 +217,14 @@ mod tests { assert_eq!(account_id, expected_account_id); } + + #[test] + fn for_dummy_matches_pinned_value() { + let nullifier_seed = [0; 32]; + let expected_nullifier = Nullifier([ + 244, 220, 48, 137, 204, 138, 180, 41, 108, 86, 40, 46, 187, 7, 232, 57, 57, 167, 143, + 157, 125, 171, 137, 46, 64, 206, 191, 211, 231, 0, 11, 86, + ]); + assert_eq!(Nullifier::for_dummy(&nullifier_seed), expected_nullifier); + } } diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit/mod.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit/mod.rs index da8b2be4..5127531a 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit/mod.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/mod.rs @@ -2,7 +2,8 @@ use std::collections::{HashMap, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use lee_core::{ - InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, + PrivacyPreservingCircuitOutput, account::AccountWithMetadata, program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, }; @@ -69,6 +70,22 @@ pub fn execute_and_prove( instruction_data: InstructionData, account_identities: Vec, program_with_dependencies: &ProgramWithDependencies, +) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> { + execute_and_prove_with_dummy_inputs( + pre_states, + instruction_data, + account_identities, + vec![], + program_with_dependencies, + ) +} + +pub fn execute_and_prove_with_dummy_inputs( + pre_states: Vec, + instruction_data: InstructionData, + account_identities: Vec, + dummy_inputs: Vec, + program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> { let ProgramWithDependencies { program: initial_program, @@ -127,6 +144,7 @@ pub fn execute_and_prove( program_outputs, account_identities, program_id: program_with_dependencies.program.id(), + dummy_inputs, }; env_builder.write(&circuit_input).unwrap(); diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index 03bf3fa3..30627a3e 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -4,11 +4,13 @@ use anyhow::Result; use keycard_wallet::{KeycardWallet, python_path}; use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ - Commitment, CommitmentSetDigest, Identifier, InputAccountIdentity, MembershipProof, - NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - account::{AccountWithMetadata, Nonce}, + Commitment, CommitmentSetDigest, DummyInput, Identifier, InputAccountIdentity, MembershipProof, + NullifierPublicKey, NullifierSecretKey, PrivateAccountKind, SharedSecretKey, + account::{Account, AccountWithMetadata, Nonce}, compute_digest_for_path, - encryption::{ViewTag, ViewingPublicKey}, + encryption::{ + Ciphertext, EncryptedAccountData, MlKem768EncapsulationKey, ViewTag, ViewingPublicKey, + }, }; use rand::{RngCore as _, rngs::OsRng}; @@ -192,6 +194,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, @@ -264,8 +274,7 @@ impl AccountManager { } => { let acc = lee_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, (&npk, &vpk, identifier)); - let mut random_seed: [u8; 32] = [0; 32]; - OsRng.fill_bytes(&mut random_seed); + let random_seed = random_bytes(); let pre = AccountPreparedData { nsk: None, npk, @@ -291,8 +300,7 @@ impl AccountManager { } => { let acc = lee_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, account_id); - let mut random_seed: [u8; 32] = [0; 32]; - OsRng.fill_bytes(&mut random_seed); + let random_seed = random_bytes(); let pre = AccountPreparedData { nsk: None, npk, @@ -398,6 +406,30 @@ impl AccountManager { .collect() } + /// Given a count, generate that many dummy inputs with randomized seeds and notes. + /// Uses the given commitment root from the account. + pub fn dummy_inputs(&self, count: usize) -> Vec { + std::iter::repeat_with(|| DummyInput { + nullifier_seed: random_bytes(), + commitment_seed: random_bytes(), + note: random_dummy_note(), + commitment_root: self.dummy_commitment_root, + }) + .take(count) + .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 @@ -549,8 +581,7 @@ fn private_key_tree_acc_preparation( // support from that in the wallet. let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), true, account_id); - let mut random_seed: [u8; 32] = [0; 32]; - OsRng.fill_bytes(&mut random_seed); + let random_seed = random_bytes(); Ok(AccountPreparedData { nsk: Some(nsk), @@ -582,8 +613,7 @@ fn private_shared_acc_preparation( let pre_state = AccountWithMetadata::new(acc, true, account_id); - let mut random_seed: [u8; 32] = [0; 32]; - OsRng.fill_bytes(&mut random_seed); + let random_seed = random_bytes(); AccountPreparedData { nsk: Some(nsk), @@ -659,6 +689,34 @@ fn random_view_tag() -> ViewTag { byte[0] } +fn random_bytes() -> [u8; 32] { + let mut bytes = [0; 32]; + OsRng.fill_bytes(&mut bytes); + bytes +} + +fn random_vec(len: usize) -> Vec { + let mut bytes = vec![0; len]; + OsRng.fill_bytes(&mut bytes); + bytes +} + +/// Generates a dummy note: random bytes sized to a default-account ciphertext, a real +/// ML-KEM ciphertext epk toward a throwaway key, and a random view tag. +fn random_dummy_note() -> EncryptedAccountData { + // Sized to a default-account ciphertext; matching real data sizes is a separate issue. + let ciphertext_len = PrivateAccountKind::HEADER_LEN + .checked_add(Account::default().to_bytes().len()) + .expect("dummy ciphertext length fits in usize"); + let throwaway_ek = MlKem768EncapsulationKey::from_seed(&random_bytes(), &random_bytes()); + let (_, epk) = SharedSecretKey::encapsulate(&throwaway_ek); + EncryptedAccountData { + ciphertext: Ciphertext::from_inner(random_vec(ciphertext_len)), + epk, + view_tag: random_view_tag(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -674,4 +732,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 16aac115..595d1a15 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -649,12 +649,14 @@ impl WalletCore { )?; let private_account_keys = acc_manager.private_account_keys(); - let (output, proof) = lee::privacy_preserving_transaction::circuit::execute_and_prove( - pre_states, - instruction_data, - acc_manager.account_identities(), - &program.to_owned(), - )?; + let (output, proof) = + lee::privacy_preserving_transaction::circuit::execute_and_prove_with_dummy_inputs( + pre_states, + instruction_data, + acc_manager.account_identities(), + acc_manager.dummy_inputs_default(), + &program.to_owned(), + )?; let message = lee::privacy_preserving_transaction::message::Message::try_from_circuit_output( diff --git a/test_fixtures/fixtures/prebuilt_sequencer_db.dump b/test_fixtures/fixtures/prebuilt_sequencer_db.dump index 6d4117a7..26c582a1 100644 Binary files a/test_fixtures/fixtures/prebuilt_sequencer_db.dump and b/test_fixtures/fixtures/prebuilt_sequencer_db.dump differ