diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 30f0cfdd..ab6007e0 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -11,8 +11,9 @@ use lee::{ privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; use lee_core::{ - EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, - account::AccountWithMetadata, + Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, EncryptedAccountData, + InputAccountIdentity, Nullifier, NullifierPublicKey, compute_digest_for_path, + account::{Account, AccountWithMetadata}, encryption::{EphemeralPublicKey, ViewingPublicKey}, }; use log::info; @@ -710,6 +711,7 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { npk, ssk, identifier: 1337, + membership_proof: None, seed: None, }, ], @@ -720,3 +722,115 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { Ok(()) } + +#[test] +async fn init_with_dummy_commitment_membership_proof_produces_valid_root() -> Result<()> { + let ctx = TestContext::new().await?; + + let program = Program::authenticated_transfer_program(); + let sender_id = ctx.existing_public_accounts()[0]; + let sender_pre = AccountWithMetadata::new( + ctx.sequencer_client().get_account(sender_id).await?, + true, + sender_id, + ); + + let nsk: lee_core::NullifierSecretKey = [7; 32]; + let npk = NullifierPublicKey::from(&nsk); + let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let ssk = SharedSecretKey([55_u8; 32]); + let recipient_account_id = AccountId::for_regular_private_account(&npk, 0); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); + + let dummy_proof = ctx + .sequencer_client() + .get_proof_for_commitment(DUMMY_COMMITMENT) + .await? + .expect("DUMMY_COMMITMENT must be in genesis commitment set"); + + let expected_digest = compute_digest_for_path(&DUMMY_COMMITMENT, &dummy_proof); + + let (output, proof) = execute_and_prove( + vec![sender_pre, recipient], + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 1, + })?, + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk), + npk, + ssk, + identifier: 0, + membership_proof: Some(dummy_proof), + }, + ], + &program.into(), + )?; + + assert!(proof.is_valid_for(&output)); + assert_eq!(output.new_nullifiers.len(), 1); + let (nullifier, digest) = &output.new_nullifiers[0]; + assert_eq!( + *nullifier, + Nullifier::for_account_initialization(&recipient_account_id) + ); + assert_eq!(*digest, expected_digest); + assert_ne!(*digest, DUMMY_COMMITMENT_HASH); + + Ok(()) +} + +#[test] +async fn init_proof_is_invalid_when_nullifier_digest_is_swapped() -> Result<()> { + let ctx = TestContext::new().await?; + + let program = Program::authenticated_transfer_program(); + let sender_id = ctx.existing_public_accounts()[0]; + let sender_pre = AccountWithMetadata::new( + ctx.sequencer_client().get_account(sender_id).await?, + true, + sender_id, + ); + + let nsk: lee_core::NullifierSecretKey = [7; 32]; + let npk = NullifierPublicKey::from(&nsk); + let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let ssk = SharedSecretKey([55_u8; 32]); + let recipient_account_id = AccountId::for_regular_private_account(&npk, 0); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); + + let dummy_proof = ctx + .sequencer_client() + .get_proof_for_commitment(DUMMY_COMMITMENT) + .await? + .expect("DUMMY_COMMITMENT must be in genesis commitment set"); + + let (output, proof) = execute_and_prove( + vec![sender_pre, recipient], + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 1, + })?, + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk), + npk, + ssk, + identifier: 0, + membership_proof: Some(dummy_proof), + }, + ], + &program.into(), + )?; + + assert!(proof.is_valid_for(&output)); + + let mut tampered_output = output; + tampered_output.new_nullifiers[0].1 = DUMMY_COMMITMENT_HASH; + assert!(!proof.is_valid_for(&tampered_output)); + + Ok(()) +} diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index f3136717..150efb31 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -78,6 +78,7 @@ async fn fund_private_pda( npk, ssk, identifier, + membership_proof: None, seed: Some((seed, authority_program_id)), }, ]; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index a11668a8..f6640581 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -314,6 +314,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { npk: recipient_npk, ssk: recipient_ss, identifier: 0, + membership_proof: None, }, ], &program.into(), diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 78bfa24f..752764b4 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -38,6 +38,7 @@ pub enum InputAccountIdentity { ssk: SharedSecretKey, nsk: NullifierSecretKey, identifier: Identifier, + membership_proof: Option, }, /// Update of an authorized standalone private account: existing on-chain commitment, with /// membership proof. @@ -57,6 +58,7 @@ pub enum InputAccountIdentity { npk: NullifierPublicKey, ssk: SharedSecretKey, identifier: Identifier, + membership_proof: Option, }, /// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream /// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. The identifier diversifies the @@ -68,6 +70,7 @@ pub enum InputAccountIdentity { npk: NullifierPublicKey, ssk: SharedSecretKey, identifier: Identifier, + membership_proof: Option, /// 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) == diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs index 489ee373..4ce58c42 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs @@ -273,6 +273,7 @@ mod tests { npk: recipient_keys.npk(), ssk: shared_secret, identifier: 0, + membership_proof: None, }, ], &crate::test_methods::simple_balance_transfer().into(), @@ -387,6 +388,7 @@ mod tests { npk: recipient_keys.npk(), ssk: shared_secret_2, identifier: 0, + membership_proof: None, }, ], &program.into(), @@ -460,6 +462,7 @@ mod tests { npk: account_keys.npk(), ssk: shared_secret, identifier: 0, + membership_proof: None, }], &program_with_deps, ); @@ -491,6 +494,7 @@ mod tests { npk, ssk: shared_secret, identifier, + membership_proof: None, seed: None, }], &program.clone().into(), @@ -540,6 +544,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + membership_proof: None, seed: None, }], &program_with_deps, @@ -595,6 +600,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + membership_proof: None, seed: None, }, InputAccountIdentity::Public, @@ -653,6 +659,7 @@ mod tests { npk: shared_npk, ssk: shared_secret, identifier: shared_identifier, + membership_proof: None, }, ], &program.into(), @@ -683,6 +690,7 @@ mod tests { ssk, nsk: keys.nsk, identifier, + membership_proof: None, }], &program.into(), ) @@ -698,23 +706,40 @@ mod tests { /// to `PrivateAccountKind::Regular` carrying the correct identifier. #[test] fn private_unauthorized_init_encrypts_regular_kind_with_identifier() { - let program = crate::test_methods::claimer(); + let program = Program::authenticated_transfer_program(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; + + let sender = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 1, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier); let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id); let (output, _) = execute_and_prove( - vec![recipient], - Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), - npk: keys.npk(), - ssk, - identifier, - }], + vec![sender, recipient], + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 1, + }) + .unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), + npk: keys.npk(), + ssk, + identifier, + membership_proof: None, + }, + ], &program.into(), ) .unwrap(); @@ -848,6 +873,7 @@ mod tests { npk, ssk: shared_secret, identifier: 99, + membership_proof: None, seed: None, }], &program.into(), @@ -903,4 +929,5 @@ mod tests { assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); } + } diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs index c399cea1..33da957d 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -1192,6 +1192,7 @@ pub mod tests { npk: recipient_keys.npk(), ssk: shared_secret, identifier: 0, + membership_proof: None, }, ], &crate::test_methods::simple_balance_transfer().into(), @@ -1259,6 +1260,7 @@ pub mod tests { npk: recipient_keys.npk(), ssk: shared_secret_2, identifier: 0, + membership_proof: None, }, ], &program.into(), @@ -1908,6 +1910,7 @@ pub mod tests { 0, ) .0, + membership_proof: None, identifier: 0, }, ], @@ -1974,6 +1977,7 @@ pub mod tests { 0, ) .0, + membership_proof: None, identifier: 0, }, ], @@ -2040,6 +2044,7 @@ pub mod tests { 0, ) .0, + membership_proof: None, identifier: 0, }, ], @@ -2106,6 +2111,7 @@ pub mod tests { 0, ) .0, + membership_proof: None, identifier: 0, }, ], @@ -2172,6 +2178,7 @@ pub mod tests { 0, ) .0, + membership_proof: None, identifier: 0, }, ], @@ -2236,6 +2243,7 @@ pub mod tests { 0, ) .0, + membership_proof: None, identifier: 0, }, ], @@ -2279,6 +2287,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + membership_proof: None, seed: None, }, ], @@ -2314,6 +2323,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + membership_proof: None, seed: None, }], &program.into(), @@ -2357,6 +2367,7 @@ pub mod tests { npk: npk_b, ssk: shared_secret, identifier: u128::MAX, + membership_proof: None, seed: None, }], &program.into(), @@ -2396,6 +2407,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + membership_proof: None, seed: None, }], &program_with_deps, @@ -2438,6 +2450,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + membership_proof: None, seed: None, }], &program_with_deps, @@ -2479,6 +2492,7 @@ pub mod tests { npk: keys_a.npk(), ssk: shared_a, identifier: u128::MAX, + membership_proof: None, seed: None, }, InputAccountIdentity::PrivatePdaInit { @@ -2487,6 +2501,7 @@ pub mod tests { npk: keys_b.npk(), ssk: shared_b, identifier: u128::MAX, + membership_proof: None, seed: None, }, ], @@ -2531,6 +2546,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + membership_proof: None, seed: None, }], &program.into(), @@ -3302,6 +3318,7 @@ pub mod tests { ssk: shared_secret, nsk: private_keys.nsk, identifier: 0, + membership_proof: None, }], &program.into(), ) @@ -3349,6 +3366,7 @@ pub mod tests { npk: private_keys.npk(), ssk: shared_secret, identifier: 0, + membership_proof: None, }], &program.into(), ) @@ -3400,6 +3418,7 @@ pub mod tests { ssk: shared_secret, nsk: private_keys.nsk, identifier: 0, + membership_proof: None, }], &claimer_program.into(), ) @@ -3446,6 +3465,7 @@ pub mod tests { ssk: shared_secret2, nsk: private_keys.nsk, identifier: 0, + membership_proof: None, }], &noop_program.into(), ); @@ -3788,6 +3808,7 @@ pub mod tests { npk: account_keys.npk(), ssk: shared_secret, identifier: 0, + membership_proof: None, }], &validity_window_program.into(), ) @@ -3856,6 +3877,7 @@ pub mod tests { npk: account_keys.npk(), ssk: shared_secret, identifier: 0, + membership_proof: None, }], &validity_window_program.into(), ) @@ -4200,6 +4222,7 @@ pub mod tests { npk: alice_npk, ssk: alice_shared_0, identifier: 0, + membership_proof: None, seed: Some((seed, proxy_id)), }, ], @@ -4240,6 +4263,7 @@ pub mod tests { npk: alice_npk, ssk: alice_shared_1, identifier: 1, + membership_proof: None, seed: Some((seed, proxy_id)), }, ], diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index ce9d1833..49db589f 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -187,6 +187,7 @@ enum State { pub struct AccountManager { states: Vec, pin: Option, + dummy_commitment_proof: Option, } impl AccountManager { @@ -340,7 +341,21 @@ impl AccountManager { states.push(state); } - Ok(Self { states, pin }) + let has_init_account = states.iter().any(|s| matches!(s, State::Private(pre) if pre.proof.is_none())); + let dummy_commitment_proof = if has_init_account { + wallet + .get_dummy_commitment_proof() + .await + .map_err(ExecutionFailureKind::SequencerError)? + } else { + None + }; + + Ok(Self { + states, + pin, + dummy_commitment_proof, + }) } pub fn pre_states(&self) -> Vec { @@ -404,6 +419,7 @@ impl AccountManager { npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, + membership_proof: self.dummy_commitment_proof.clone(), seed: None, }, }, @@ -424,6 +440,7 @@ impl AccountManager { ssk: pre.ssk, nsk, identifier: pre.identifier, + membership_proof: self.dummy_commitment_proof.clone(), }, (None, _) => InputAccountIdentity::PrivateUnauthorized { epk: pre.epk.clone(), @@ -431,6 +448,7 @@ impl AccountManager { npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, + membership_proof: self.dummy_commitment_proof.clone(), }, }, }) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 80b42e17..db68883c 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -22,7 +22,8 @@ use lee::{ }, }; use lee_core::{ - Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData, + Commitment, DUMMY_COMMITMENT, MembershipProof, SharedSecretKey, account::Nonce, + program::InstructionData, }; use log::info; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; @@ -508,6 +509,13 @@ impl WalletCore { } } + pub async fn get_dummy_commitment_proof(&self) -> Result> { + self.sequencer_client + .get_proof_for_commitment(DUMMY_COMMITMENT) + .await + .map_err(Into::into) + } + pub fn decode_insert_privacy_preserving_transaction_results( &mut self, tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction,