diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index 8db9385b..bb4060e0 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 00f5343d..bbecfb28 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 8f4b95ed..fb0e04f2 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 4f56b0f7..7986077a 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 e4e4ec5e..16170125 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index 663cc59b..ae8db775 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index b26cfc6f..f28425e0 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 f93e2c37..7b32974e 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 1ffa430d..1b4a4d02 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/token.bin b/artifacts/lez/programs/token.bin index 210c7c3a..a0f25405 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 e6766101..ec74232d 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 9094460e..d8aa0ad3 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -11,8 +11,10 @@ use lee::{ privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; use lee_core::{ - EncryptedAccountData, InputAccountIdentity, ML_KEM_768_CIPHERTEXT_LEN, NullifierPublicKey, - account::AccountWithMetadata, + DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, Nullifier, + NullifierPublicKey, + account::{Account, AccountWithMetadata}, + compute_digest_for_path, encryption::{EphemeralPublicKey, ViewingPublicKey}, }; use log::info; @@ -710,6 +712,7 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { npk, ssk, identifier: 1337, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }, ], @@ -720,3 +723,100 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { Ok(()) } + +async fn prove_init_with_commitment_root( + ctx: &TestContext, + commitment_root: lee_core::CommitmentSetDigest, +) -> Result { + let program = programs::authenticated_transfer(); + 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 (output, _) = 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, + commitment_root, + }, + ], + &program.into(), + )?; + + Ok(output) +} + +#[test] +async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> { + let ctx = TestContext::new().await?; + + 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 nsk: lee_core::NullifierSecretKey = [7; 32]; + let npk = NullifierPublicKey::from(&nsk); + let recipient_account_id = AccountId::for_regular_private_account(&npk, 0); + + let output = prove_init_with_commitment_root(&ctx, expected_digest).await?; + + 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_nullifier_digest_is_bound_to_commitment_root() -> Result<()> { + let ctx = TestContext::new().await?; + + 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_with_root = prove_init_with_commitment_root(&ctx, expected_digest).await?; + let output_without_root = prove_init_with_commitment_root(&ctx, DUMMY_COMMITMENT_HASH).await?; + + assert_eq!(output_with_root.new_nullifiers[0].1, expected_digest); + assert_eq!( + output_without_root.new_nullifiers[0].1, + DUMMY_COMMITMENT_HASH + ); + assert_ne!( + output_with_root.new_nullifiers[0].1, + output_without_root.new_nullifiers[0].1, + ); + + Ok(()) +} diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index f3136717..81ff8467 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -22,7 +22,7 @@ use lee::{ program::Program, }; use lee_core::{ - EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, + DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, account::{Account, AccountWithMetadata}, encryption::ViewingPublicKey, program::PdaSeed, @@ -78,6 +78,7 @@ async fn fund_private_pda( npk, ssk, identifier, + commitment_root: DUMMY_COMMITMENT_HASH, seed: Some((seed, authority_program_id)), }, ]; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index a11668a8..0b9a1002 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -23,7 +23,8 @@ use lee::{ public_transaction as putx, }; use lee_core::{ - EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey, + DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, MembershipProof, + NullifierPublicKey, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -314,6 +315,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { npk: recipient_npk, ssk: recipient_ss, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), diff --git a/lee/privacy_preserving_circuit/src/execution_state.rs b/lee/privacy_preserving_circuit/src/execution_state.rs index 8d920068..4e81f9ce 100644 --- a/lee/privacy_preserving_circuit/src/execution_state.rs +++ b/lee/privacy_preserving_circuit/src/execution_state.rs @@ -59,46 +59,9 @@ impl ExecutionState { program_id: ProgramId, program_outputs: Vec, ) -> Self { - // Build position → (npk, identifier) map for private-PDA pre_states, indexed by position - // in `account_identities`. The vec is documented as 1:1 with the program's pre_state - // order, so position here matches `pre_state_position` used downstream in - // `validate_and_sync_states`. - let mut private_pda_npk_by_position: HashMap = - HashMap::new(); - for (pos, account_identity) in account_identities.iter().enumerate() { - if let Some((npk, identifier)) = account_identity.npk_if_private_pda() { - private_pda_npk_by_position.insert(pos, (npk, identifier)); - } - } - - let block_valid_from = program_outputs - .iter() - .filter_map(|output| output.block_validity_window.start()) - .max(); - let block_valid_until = program_outputs - .iter() - .filter_map(|output| output.block_validity_window.end()) - .min(); - let ts_valid_from = program_outputs - .iter() - .filter_map(|output| output.timestamp_validity_window.start()) - .max(); - let ts_valid_until = program_outputs - .iter() - .filter_map(|output| output.timestamp_validity_window.end()) - .min(); - - let block_validity_window: BlockValidityWindow = (block_valid_from, block_valid_until) - .try_into() - .expect( - "There should be non empty intersection in the program output block validity windows", - ); - let timestamp_validity_window: TimestampValidityWindow = - (ts_valid_from, ts_valid_until) - .try_into() - .expect( - "There should be non empty intersection in the program output timestamp validity windows", - ); + let private_pda_npk_by_position = build_private_pda_npk_map(account_identities); + let (block_validity_window, timestamp_validity_window) = + intersect_validity_windows(&program_outputs); let mut execution_state = Self { pre_states: Vec::new(), @@ -136,49 +99,7 @@ impl ExecutionState { panic!("Insufficient program outputs for chained calls"); }; - // Check that instruction data in chained call is the instruction data in program output - assert_eq!( - chained_call.instruction_data, program_output.instruction_data, - "Mismatched instruction data between chained call and program output" - ); - - // Check that `program_output` is consistent with the execution of the corresponding - // program. - let program_output_words = - &to_vec(&program_output).expect("program_output must be serializable"); - env::verify(chained_call.program_id, program_output_words).unwrap_or_else( - |_: Infallible| unreachable!("Infallible error is never constructed"), - ); - - // Verify that the program output's self_program_id matches the expected program ID. - // This ensures the proof commits to which program produced the output. - assert_eq!( - program_output.self_program_id, chained_call.program_id, - "Program output self_program_id does not match chained call program_id" - ); - - // Verify that the program output's caller_program_id matches the actual caller. - // This prevents a malicious user from privately executing an internal function - // by spoofing caller_program_id (e.g. passing caller_program_id = self_program_id - // to bypass access control checks). - assert_eq!( - program_output.caller_program_id, caller_program_id, - "Program output caller_program_id does not match actual caller" - ); - - // Check that the program is well behaved. - // See the # Programs section for the definition of the `validate_execution` method. - let validated_execution = validate_execution( - &program_output.pre_states, - &program_output.post_states, - chained_call.program_id, - ); - if let Err(err) = validated_execution { - panic!( - "Invalid program behavior in program {:?}: {err}", - chained_call.program_id - ); - } + verify_program_output(&chained_call, caller_program_id, &program_output); for next_call in program_output.chained_calls.iter().rev() { chained_calls.push_front((next_call.clone(), Some(chained_call.program_id))); @@ -202,41 +123,8 @@ impl ExecutionState { "Inner call without a chained call found", ); - // Every private-PDA pre_state must have had its npk bound to its account_id, either via - // a `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` - // matching the private derivation. An unbound private-PDA pre_state has no - // cryptographic link between the supplied npk and the account_id, and must be rejected. - for (pos, account_identity) in account_identities.iter().enumerate() { - if account_identity.is_private_pda() { - assert!( - execution_state - .private_pda_bound_positions - .contains_key(&pos), - "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" - ); - } - } - - // Check that all modified uninitialized accounts were claimed - for (account_id, post) in execution_state - .pre_states - .iter() - .filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID) - .map(|a| { - let post = execution_state - .post_states - .get(&a.account_id) - .expect("Post state must exist for pre state"); - (a, post) - }) - .filter(|(pre_default, post)| pre_default.account != **post) - .map(|(pre, post)| (pre.account_id, post)) - { - assert_ne!( - post.program_owner, DEFAULT_PROGRAM_ID, - "Account {account_id} was modified but not claimed" - ); - } + execution_state.assert_all_pda_positions_bound(account_identities); + execution_state.assert_modified_accounts_claimed(); execution_state } @@ -254,200 +142,185 @@ impl ExecutionState { for (pre, mut post) in output_pre_states.into_iter().zip(output_post_states) { let pre_account_id = pre.account_id; let pre_is_authorized = pre.is_authorized; - let post_states_entry = self.post_states.entry(pre.account_id); - match &post_states_entry { - Entry::Occupied(occupied) => { - #[expect( - clippy::shadow_unrelated, - reason = "Shadowing is intentional to use all fields" - )] - let AccountWithMetadata { - account: pre_account, - account_id: pre_account_id, - is_authorized: pre_is_authorized, - } = pre; - // Ensure that new pre state is the same as known post state - assert_eq!( - occupied.get(), - &pre_account, - "Inconsistent pre state for account {pre_account_id}", + if let Some(existing) = self.post_states.get(&pre.account_id) { + #[expect( + clippy::shadow_unrelated, + reason = "Shadowing is intentional to use all fields" + )] + let AccountWithMetadata { + account: pre_account, + account_id: pre_account_id, + is_authorized: pre_is_authorized, + } = pre; + + assert_eq!( + existing, &pre_account, + "Inconsistent pre state for account {pre_account_id}", + ); + + let (previous_is_authorized, pre_state_position) = self + .pre_states + .iter() + .enumerate() + .find(|(_, acc)| acc.account_id == pre_account_id) + .map_or_else( + || panic!( + "Pre state must exist in execution state for account {pre_account_id}", + ), + |(pos, acc)| (acc.is_authorized, pos), ); - let (previous_is_authorized, pre_state_position) = self - .pre_states - .iter() - .enumerate() - .find(|(_, acc)| acc.account_id == pre_account_id) - .map_or_else( - || panic!( - "Pre state must exist in execution state for account {pre_account_id}", - ), - |(pos, acc)| (acc.is_authorized, pos) - ); + let is_authorized = resolve_authorization_and_record_bindings( + &mut self.pda_family_binding, + &mut self.private_pda_bound_positions, + &self.private_pda_npk_by_position, + &mut self.authorized_accounts, + pre_account_id, + pre_state_position, + caller_program_id, + caller_pda_seeds, + previous_is_authorized, + ); - let is_authorized = resolve_authorization_and_record_bindings( - &mut self.pda_family_binding, - &mut self.private_pda_bound_positions, - &self.private_pda_npk_by_position, - &mut self.authorized_accounts, - pre_account_id, - pre_state_position, - caller_program_id, - caller_pda_seeds, - previous_is_authorized, - ); - - assert_eq!( - pre_is_authorized, is_authorized, - "Inconsistent authorization for account {pre_account_id}", - ); - } - 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); - } + assert_eq!( + pre_is_authorized, is_authorized, + "Inconsistent authorization for account {pre_account_id}", + ); + } else { + let pre_state_position = self.pre_states.len(); + resolve_external_seed( + account_identities, + pre_state_position, + pre_account_id, + pre.is_authorized, + &mut self.private_pda_bound_positions, + &mut self.pda_family_binding, + ); + self.pre_states.push(pre); } if let Some(claim) = post.required_claim() { - // The invoked program can only claim accounts with default program id. - assert_eq!( - post.account().program_owner, - DEFAULT_PROGRAM_ID, - "Cannot claim an initialized account {pre_account_id}" + self.process_claim( + account_identities, + &mut post, + pre_account_id, + pre_is_authorized, + program_id, + claim, ); - - let pre_state_position = self - .pre_states - .iter() - .position(|acc| acc.account_id == pre_account_id) - .expect("Pre state must exist at this point"); - - let account_identity = &account_identities[pre_state_position]; - if account_identity.is_public() { - match claim { - Claim::Authorized => { - // Note: no need to check authorized pdas because we have already - // checked consistency of authorization above. - assert!( - pre_is_authorized, - "Cannot claim unauthorized account {pre_account_id}" - ); - } - Claim::Pda(seed) => { - let pda = AccountId::for_public_pda(&program_id, &seed); - assert_eq!( - pre_account_id, pda, - "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" - ); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } - } - } else { - // Private accounts: don't enforce the claim semantics. Unauthorized private - // claiming is intentionally allowed - match claim { - Claim::Authorized => {} - Claim::Pda(seed) => { - let (npk, identifier) = self - .private_pda_npk_by_position - .get(&pre_state_position) - .expect( - "private PDA pre_state must have an npk in the position map", - ); - let pda = - AccountId::for_private_pda(&program_id, &seed, npk, *identifier); - assert_eq!( - pre_account_id, pda, - "Invalid private PDA claim for account {pre_account_id}" - ); - bind_private_pda_position( - &mut self.private_pda_bound_positions, - pre_state_position, - program_id, - seed, - ); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } - } - } - - post.account_mut().program_owner = program_id; } - post_states_entry.insert_entry(post.into_account()); + self.post_states.insert(pre_account_id, post.into_account()); + } + } + + fn process_claim( + &mut self, + account_identities: &[InputAccountIdentity], + post: &mut AccountPostState, + pre_account_id: AccountId, + pre_is_authorized: bool, + program_id: ProgramId, + claim: Claim, + ) { + assert_eq!( + post.account().program_owner, + DEFAULT_PROGRAM_ID, + "Cannot claim an initialized account {pre_account_id}" + ); + + let pre_state_position = self + .pre_states + .iter() + .position(|acc| acc.account_id == pre_account_id) + .expect("Pre state must exist at this point"); + + let account_identity = &account_identities[pre_state_position]; + if account_identity.is_public() { + match claim { + Claim::Authorized => { + assert!( + pre_is_authorized, + "Cannot claim unauthorized account {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let pda = AccountId::for_public_pda(&program_id, &seed); + assert_eq!( + pre_account_id, pda, + "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" + ); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); + } + } + } else { + match claim { + Claim::Authorized => {} + Claim::Pda(seed) => { + let (npk, identifier) = self + .private_pda_npk_by_position + .get(&pre_state_position) + .expect("private PDA pre_state must have an npk in the position map"); + let pda = AccountId::for_private_pda(&program_id, &seed, npk, *identifier); + assert_eq!( + pre_account_id, pda, + "Invalid private PDA claim for account {pre_account_id}" + ); + bind_private_pda_position( + &mut self.private_pda_bound_positions, + pre_state_position, + program_id, + seed, + ); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); + } + } + } + + post.account_mut().program_owner = program_id; + } + + fn assert_all_pda_positions_bound(&self, account_identities: &[InputAccountIdentity]) { + for (pos, account_identity) in account_identities.iter().enumerate() { + if account_identity.is_private_pda() { + assert!( + self.private_pda_bound_positions.contains_key(&pos), + "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" + ); + } + } + } + + fn assert_modified_accounts_claimed(&self) { + for (account_id, post) in self + .pre_states + .iter() + .filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID) + .map(|a| { + let post = self + .post_states + .get(&a.account_id) + .expect("Post state must exist for pre state"); + (a, post) + }) + .filter(|(pre_default, post)| pre_default.account != **post) + .map(|(pre, post)| (pre.account_id, post)) + { + assert_ne!( + post.program_owner, DEFAULT_PROGRAM_ID, + "Account {account_id} was modified but not claimed" + ); } } @@ -486,6 +359,150 @@ impl ExecutionState { } } +fn verify_program_output( + chained_call: &ChainedCall, + caller_program_id: Option, + program_output: &ProgramOutput, +) { + assert_eq!( + chained_call.instruction_data, program_output.instruction_data, + "Mismatched instruction data between chained call and program output" + ); + + let program_output_words = + &to_vec(program_output).expect("program_output must be serializable"); + env::verify(chained_call.program_id, program_output_words) + .unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed")); + + assert_eq!( + program_output.self_program_id, chained_call.program_id, + "Program output self_program_id does not match chained call program_id" + ); + + assert_eq!( + program_output.caller_program_id, caller_program_id, + "Program output caller_program_id does not match actual caller" + ); + + if let Err(err) = validate_execution( + &program_output.pre_states, + &program_output.post_states, + chained_call.program_id, + ) { + panic!( + "Invalid program behavior in program {:?}: {err}", + chained_call.program_id + ); + } +} + +fn build_private_pda_npk_map( + account_identities: &[InputAccountIdentity], +) -> HashMap { + account_identities + .iter() + .enumerate() + .filter_map(|(pos, identity)| { + identity + .npk_if_private_pda() + .map(|(npk, identifier)| (pos, (npk, identifier))) + }) + .collect() +} + +fn intersect_validity_windows( + program_outputs: &[ProgramOutput], +) -> (BlockValidityWindow, TimestampValidityWindow) { + let block_valid_from = program_outputs + .iter() + .filter_map(|output| output.block_validity_window.start()) + .max(); + let block_valid_until = program_outputs + .iter() + .filter_map(|output| output.block_validity_window.end()) + .min(); + let ts_valid_from = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.start()) + .max(); + let ts_valid_until = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.end()) + .min(); + + let block_validity_window: BlockValidityWindow = + (block_valid_from, block_valid_until).try_into().expect( + "There should be non empty intersection in the program output block validity windows", + ); + let timestamp_validity_window: TimestampValidityWindow = (ts_valid_from, ts_valid_until) + .try_into() + .expect( + "There should be non empty intersection in the program output timestamp validity windows", + ); + + (block_validity_window, timestamp_validity_window) +} + +fn resolve_external_seed( + account_identities: &[InputAccountIdentity], + pre_state_position: usize, + pre_account_id: AccountId, + is_authorized: bool, + private_pda_bound_positions: &mut HashMap, + pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>, +) { + 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, + }; + + if let Some((seed, authority_program_id)) = external_seed { + assert!( + !is_authorized, + "Private PDA with externally-provided seed must not be authorized at position {pre_state_position}" + ); + bind_private_pda_position( + private_pda_bound_positions, + pre_state_position, + authority_program_id, + seed, + ); + assert_family_binding( + pda_family_binding, + authority_program_id, + seed, + pre_account_id, + ); + } +} + /// Record or re-verify the `(program_id, seed) → account_id` family binding for the /// transaction. Any claim or caller-seed authorization that resolves a `pre_state` under /// `(program_id, seed)` must agree with every prior resolution of the same pair; otherwise a diff --git a/lee/privacy_preserving_circuit/src/output.rs b/lee/privacy_preserving_circuit/src/output.rs index 8c8ec2a4..74c1cccc 100644 --- a/lee/privacy_preserving_circuit/src/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -1,13 +1,244 @@ use lee_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, - EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, - NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, + Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, + InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, + PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, compute_digest_for_path, }; use crate::execution_state::ExecutionState; +struct PrivateOutputHandler<'ctx> { + output: &'ctx mut PrivacyPreservingCircuitOutput, + output_index: &'ctx mut u32, + pre_state: &'ctx lee_core::account::AccountWithMetadata, + post_state: Account, + epk: &'ctx EphemeralPublicKey, + view_tag: u8, + ssk: &'ctx SharedSecretKey, + identifier: u128, +} + +impl PrivateOutputHandler<'_> { + fn authorized_init(self, nsk: &NullifierSecretKey, commitment_root: &CommitmentSetDigest) { + let npk = NullifierPublicKey::from(nsk); + let account_id = + derive_and_verify_account_id(&npk, self.identifier, self.pre_state.account_id); + + assert!( + self.pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + assert_eq!( + self.pre_state.account, + Account::default(), + "Found new private account with non default values" + ); + + let (new_nullifier, new_nonce) = init_nullifier_and_nonce(&account_id, commitment_root); + let kind = PrivateAccountKind::Regular(self.identifier); + + self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); + } + + fn authorized_update(self, nsk: &NullifierSecretKey, membership_proof: &MembershipProof) { + let npk = NullifierPublicKey::from(nsk); + let account_id = + derive_and_verify_account_id(&npk, self.identifier, self.pre_state.account_id); + + assert!( + self.pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + + let new_nullifier = compute_update_nullifier_and_set_digest( + membership_proof, + &self.pre_state.account, + &account_id, + nsk, + ); + let new_nonce = self + .pre_state + .account + .nonce + .private_account_nonce_increment(nsk); + let kind = PrivateAccountKind::Regular(self.identifier); + + self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); + } + + fn unauthorized(self, npk: &NullifierPublicKey, commitment_root: &CommitmentSetDigest) { + let account_id = + derive_and_verify_account_id(npk, self.identifier, self.pre_state.account_id); + + assert_eq!( + self.pre_state.account, + Account::default(), + "Found new private account with non default values", + ); + assert!( + !self.pre_state.is_authorized, + "Found new private account marked as authorized." + ); + + let (new_nullifier, new_nonce) = init_nullifier_and_nonce(&account_id, commitment_root); + let kind = PrivateAccountKind::Regular(self.identifier); + + self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); + } + + fn pda_init( + self, + commitment_root: &CommitmentSetDigest, + pos: usize, + pda_seed_by_position: &std::collections::HashMap< + usize, + (lee_core::program::ProgramId, lee_core::program::PdaSeed), + >, + ) { + // The npk-to-account_id binding is established upstream in + // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` + // match. Here we only enforce the init pre-conditions. + assert!( + !self.pre_state.is_authorized, + "PrivatePdaInit requires unauthorized pre_state" + ); + assert_eq!( + self.pre_state.account, + Account::default(), + "New private PDA must be default" + ); + + let (new_nullifier, new_nonce) = + init_nullifier_and_nonce(&self.pre_state.account_id, commitment_root); + + let account_id = self.pre_state.account_id; + let (authority_program_id, seed) = pda_seed_by_position + .get(&pos) + .expect("PrivatePdaInit position must be in pda_seed_by_position"); + let kind = PrivateAccountKind::Pda { + program_id: *authority_program_id, + seed: *seed, + identifier: self.identifier, + }; + + self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); + } + + fn pda_update( + self, + nsk: &NullifierSecretKey, + membership_proof: &MembershipProof, + external_seed: Option<&(lee_core::program::PdaSeed, lee_core::program::ProgramId)>, + pos: usize, + pda_seed_by_position: &std::collections::HashMap< + usize, + (lee_core::program::ProgramId, lee_core::program::PdaSeed), + >, + ) { + // 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. + assert!( + self.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( + membership_proof, + &self.pre_state.account, + &self.pre_state.account_id, + nsk, + ); + let new_nonce = self + .pre_state + .account + .nonce + .private_account_nonce_increment(nsk); + + let account_id = self.pre_state.account_id; + let (authority_program_id, seed) = pda_seed_by_position + .get(&pos) + .expect("PrivatePdaUpdate position must be in pda_seed_by_position"); + let kind = PrivateAccountKind::Pda { + program_id: *authority_program_id, + seed: *seed, + identifier: self.identifier, + }; + + self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); + } + + fn emit_private_output( + self, + account_id: &AccountId, + kind: &PrivateAccountKind, + new_nullifier: (Nullifier, CommitmentSetDigest), + new_nonce: Nonce, + ) { + self.output.new_nullifiers.push(new_nullifier); + + let mut post_with_updated_nonce = self.post_state; + post_with_updated_nonce.nonce = new_nonce; + + let commitment_post = Commitment::new(account_id, &post_with_updated_nonce); + let encrypted_account = EncryptionScheme::encrypt( + &post_with_updated_nonce, + kind, + self.ssk, + &commitment_post, + *self.output_index, + ); + + self.output.new_commitments.push(commitment_post); + self.output + .encrypted_private_post_states + .push(EncryptedAccountData { + ciphertext: encrypted_account, + epk: self.epk.clone(), + view_tag: self.view_tag, + }); + *self.output_index = self + .output_index + .checked_add(1) + .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); + } +} + +fn init_nullifier_and_nonce( + account_id: &AccountId, + commitment_root: &CommitmentSetDigest, +) -> ((Nullifier, CommitmentSetDigest), Nonce) { + let nullifier = ( + Nullifier::for_account_initialization(account_id), + *commitment_root, + ); + let nonce = Nonce::private_account_nonce_init(account_id); + (nullifier, nonce) +} + +fn derive_and_verify_account_id( + npk: &NullifierPublicKey, + identifier: u128, + pre_state_account_id: AccountId, +) -> AccountId { + let account_id = AccountId::for_regular_private_account(npk, identifier); + assert_eq!(account_id, pre_state_account_id, "AccountId mismatch"); + account_id +} + +fn compute_update_nullifier_and_set_digest( + membership_proof: &MembershipProof, + pre_account: &Account, + account_id: &AccountId, + nsk: &NullifierSecretKey, +) -> (Nullifier, CommitmentSetDigest) { + let commitment_pre = Commitment::new(account_id, pre_account); + let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + (nullifier, set_digest) +} + pub fn compute_circuit_output( execution_state: ExecutionState, account_identities: &[InputAccountIdentity], @@ -45,40 +276,18 @@ pub fn compute_circuit_output( ssk, nsk, identifier, - } => { - let npk = NullifierPublicKey::from(nsk); - let account_id = AccountId::for_regular_private_account(&npk, *identifier); - - assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); - assert!( - pre_state.is_authorized, - "Pre-state not authorized for authenticated private account" - ); - assert_eq!( - pre_state.account, - Account::default(), - "Found new private account with non default values" - ); - - let new_nullifier = ( - Nullifier::for_account_initialization(&account_id), - DUMMY_COMMITMENT_HASH, - ); - let new_nonce = Nonce::private_account_nonce_init(&account_id); - - emit_private_output( - &mut output, - &mut output_index, - post_state, - &account_id, - &PrivateAccountKind::Regular(*identifier), - ssk, - epk, - *view_tag, - new_nullifier, - new_nonce, - ); + commitment_root, + } => PrivateOutputHandler { + output: &mut output, + output_index: &mut output_index, + pre_state: &pre_state, + post_state, + epk, + view_tag: *view_tag, + ssk, + identifier: *identifier, } + .authorized_init(nsk, commitment_root), InputAccountIdentity::PrivateAuthorizedUpdate { epk, view_tag, @@ -86,127 +295,54 @@ pub fn compute_circuit_output( nsk, membership_proof, identifier, - } => { - let npk = NullifierPublicKey::from(nsk); - let account_id = AccountId::for_regular_private_account(&npk, *identifier); - - assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); - assert!( - pre_state.is_authorized, - "Pre-state not authorized for authenticated private account" - ); - - let new_nullifier = compute_update_nullifier_and_set_digest( - membership_proof, - &pre_state.account, - &account_id, - nsk, - ); - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - - emit_private_output( - &mut output, - &mut output_index, - post_state, - &account_id, - &PrivateAccountKind::Regular(*identifier), - ssk, - epk, - *view_tag, - new_nullifier, - new_nonce, - ); + } => PrivateOutputHandler { + output: &mut output, + output_index: &mut output_index, + pre_state: &pre_state, + post_state, + epk, + view_tag: *view_tag, + ssk, + identifier: *identifier, } + .authorized_update(nsk, membership_proof), InputAccountIdentity::PrivateUnauthorized { epk, view_tag, npk, ssk, identifier, - } => { - let account_id = AccountId::for_regular_private_account(npk, *identifier); - - assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); - assert_eq!( - pre_state.account, - Account::default(), - "Found new private account with non default values", - ); - assert!( - !pre_state.is_authorized, - "Found new private account marked as authorized." - ); - - let new_nullifier = ( - Nullifier::for_account_initialization(&account_id), - DUMMY_COMMITMENT_HASH, - ); - let new_nonce = Nonce::private_account_nonce_init(&account_id); - - emit_private_output( - &mut output, - &mut output_index, - post_state, - &account_id, - &PrivateAccountKind::Regular(*identifier), - ssk, - epk, - *view_tag, - new_nullifier, - new_nonce, - ); + commitment_root, + } => PrivateOutputHandler { + output: &mut output, + output_index: &mut output_index, + pre_state: &pre_state, + post_state, + epk, + view_tag: *view_tag, + ssk, + identifier: *identifier, } + .unauthorized(npk, commitment_root), InputAccountIdentity::PrivatePdaInit { epk, view_tag, npk: _, ssk, identifier, + commitment_root, seed: _, - } => { - // The npk-to-account_id binding is established upstream in - // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` - // match. Here we only enforce the init pre-conditions. The supplied npk on - // the variant has been recorded into `private_pda_npk_by_position` and used - // for the binding check; we use `pre_state.account_id` directly for nullifier - // and commitment derivation. - assert!( - !pre_state.is_authorized, - "PrivatePdaInit requires unauthorized pre_state" - ); - assert_eq!( - pre_state.account, - Account::default(), - "New private PDA must be default" - ); - - let new_nullifier = ( - Nullifier::for_account_initialization(&pre_state.account_id), - DUMMY_COMMITMENT_HASH, - ); - let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); - - let account_id = pre_state.account_id; - let (authority_program_id, seed) = pda_seed_by_position - .get(&pos) - .expect("PrivatePdaInit position must be in pda_seed_by_position"); - emit_private_output( - &mut output, - &mut output_index, - post_state, - &account_id, - &PrivateAccountKind::Pda { - program_id: *authority_program_id, - seed: *seed, - identifier: *identifier, - }, - ssk, - epk, - *view_tag, - new_nullifier, - new_nonce, - ); + } => PrivateOutputHandler { + output: &mut output, + output_index: &mut output_index, + pre_state: &pre_state, + post_state, + epk, + view_tag: *view_tag, + ssk, + identifier: *identifier, } + .pda_init(commitment_root, pos, &pda_seed_by_position), InputAccountIdentity::PrivatePdaUpdate { epk, view_tag, @@ -215,103 +351,25 @@ pub fn compute_circuit_output( membership_proof, identifier, seed: external_seed, - } => { - // 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 ^ external_seed.is_some(), - "PrivatePdaUpdate requires authorized pre_state or external seed" - ); - - let new_nullifier = compute_update_nullifier_and_set_digest( - membership_proof, - &pre_state.account, - &pre_state.account_id, - nsk, - ); - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - - let account_id = pre_state.account_id; - let (authority_program_id, seed) = pda_seed_by_position - .get(&pos) - .expect("PrivatePdaUpdate position must be in pda_seed_by_position"); - emit_private_output( - &mut output, - &mut output_index, - post_state, - &account_id, - &PrivateAccountKind::Pda { - program_id: *authority_program_id, - seed: *seed, - identifier: *identifier, - }, - ssk, - epk, - *view_tag, - new_nullifier, - new_nonce, - ); + } => PrivateOutputHandler { + output: &mut output, + output_index: &mut output_index, + pre_state: &pre_state, + post_state, + epk, + view_tag: *view_tag, + ssk, + identifier: *identifier, } + .pda_update( + nsk, + membership_proof, + external_seed.as_ref(), + pos, + &pda_seed_by_position, + ), } } output } - -#[expect( - clippy::too_many_arguments, - reason = "Inputs are distinct concerns from the variant arms; bundling would be artificial" -)] -fn emit_private_output( - output: &mut PrivacyPreservingCircuitOutput, - output_index: &mut u32, - post_state: Account, - account_id: &AccountId, - kind: &PrivateAccountKind, - shared_secret: &SharedSecretKey, - epk: &EphemeralPublicKey, - view_tag: u8, - new_nullifier: (Nullifier, CommitmentSetDigest), - new_nonce: Nonce, -) { - output.new_nullifiers.push(new_nullifier); - - let mut post_with_updated_nonce = post_state; - post_with_updated_nonce.nonce = new_nonce; - - let commitment_post = Commitment::new(account_id, &post_with_updated_nonce); - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - kind, - shared_secret, - &commitment_post, - *output_index, - ); - - output.new_commitments.push(commitment_post); - output - .encrypted_private_post_states - .push(EncryptedAccountData { - ciphertext: encrypted_account, - epk: epk.clone(), - view_tag, - }); - *output_index = output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); -} - -fn compute_update_nullifier_and_set_digest( - membership_proof: &MembershipProof, - pre_account: &Account, - account_id: &AccountId, - nsk: &NullifierSecretKey, -) -> (Nullifier, CommitmentSetDigest) { - let commitment_pre = Commitment::new(account_id, pre_account); - let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); - let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); - (nullifier, set_digest) -} diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 78bfa24f..88b214d4 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, + commitment_root: CommitmentSetDigest, }, /// 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, + commitment_root: CommitmentSetDigest, }, /// 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, + commitment_root: CommitmentSetDigest, /// 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..e7d8f8d4 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, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &crate::test_methods::simple_balance_transfer().into(), @@ -387,6 +388,7 @@ mod tests { npk: recipient_keys.npk(), ssk: shared_secret_2, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -460,6 +462,7 @@ mod tests { npk: account_keys.npk(), ssk: shared_secret, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }], &program_with_deps, ); @@ -491,6 +494,7 @@ mod tests { npk, ssk: shared_secret, identifier, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program.clone().into(), @@ -540,6 +544,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program_with_deps, @@ -595,6 +600,7 @@ mod tests { npk, ssk: shared_secret_pda, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }, InputAccountIdentity::Public, @@ -653,6 +659,7 @@ mod tests { npk: shared_npk, ssk: shared_secret, identifier: shared_identifier, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -683,6 +690,7 @@ mod tests { ssk, nsk: keys.nsk, identifier, + commitment_root: DUMMY_COMMITMENT_HASH, }], &program.into(), ) @@ -714,6 +722,7 @@ mod tests { npk: keys.npk(), ssk, identifier, + commitment_root: DUMMY_COMMITMENT_HASH, }], &program.into(), ) @@ -848,6 +857,7 @@ mod tests { npk, ssk: shared_secret, identifier: 99, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program.into(), diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs index 57cd0bf6..76013087 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -327,8 +327,8 @@ pub mod tests { use std::collections::HashMap; use lee_core::{ - BlockId, Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier, - NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp, + BlockId, Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, + Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey}, program::{ @@ -1192,6 +1192,7 @@ pub mod tests { npk: recipient_keys.npk(), ssk: shared_secret, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &crate::test_methods::simple_balance_transfer().into(), @@ -1259,6 +1260,7 @@ pub mod tests { npk: recipient_keys.npk(), ssk: shared_secret_2, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -1908,6 +1910,7 @@ pub mod tests { 0, ) .0, + commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, }, ], @@ -1974,6 +1977,7 @@ pub mod tests { 0, ) .0, + commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, }, ], @@ -2040,6 +2044,7 @@ pub mod tests { 0, ) .0, + commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, }, ], @@ -2106,6 +2111,7 @@ pub mod tests { 0, ) .0, + commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, }, ], @@ -2172,6 +2178,7 @@ pub mod tests { 0, ) .0, + commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, }, ], @@ -2236,6 +2243,7 @@ pub mod tests { 0, ) .0, + commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, }, ], @@ -2279,6 +2287,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }, ], @@ -2314,6 +2323,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program.into(), @@ -2357,6 +2367,7 @@ pub mod tests { npk: npk_b, ssk: shared_secret, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program.into(), @@ -2396,6 +2407,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program_with_deps, @@ -2438,6 +2450,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program_with_deps, @@ -2479,6 +2492,7 @@ pub mod tests { npk: keys_a.npk(), ssk: shared_a, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }, InputAccountIdentity::PrivatePdaInit { @@ -2487,6 +2501,7 @@ pub mod tests { npk: keys_b.npk(), ssk: shared_b, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }, ], @@ -2531,6 +2546,7 @@ pub mod tests { npk, ssk: shared_secret, identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }], &program.into(), @@ -3302,6 +3318,7 @@ pub mod tests { ssk: shared_secret, nsk: private_keys.nsk, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }], &program.into(), ) @@ -3349,6 +3366,7 @@ pub mod tests { npk: private_keys.npk(), ssk: shared_secret, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }], &program.into(), ) @@ -3400,6 +3418,7 @@ pub mod tests { ssk: shared_secret, nsk: private_keys.nsk, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }], &claimer_program.into(), ) @@ -3446,6 +3465,7 @@ pub mod tests { ssk: shared_secret2, nsk: private_keys.nsk, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }], &noop_program.into(), ); @@ -3788,6 +3808,7 @@ pub mod tests { npk: account_keys.npk(), ssk: shared_secret, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }], &validity_window_program.into(), ) @@ -3856,6 +3877,7 @@ pub mod tests { npk: account_keys.npk(), ssk: shared_secret, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }], &validity_window_program.into(), ) @@ -4200,6 +4222,7 @@ pub mod tests { npk: alice_npk, ssk: alice_shared_0, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, seed: Some((seed, proxy_id)), }, ], @@ -4240,6 +4263,7 @@ pub mod tests { npk: alice_npk, ssk: alice_shared_1, identifier: 1, + commitment_root: DUMMY_COMMITMENT_HASH, seed: Some((seed, proxy_id)), }, ], diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index ce9d1833..ae4e3945 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -5,8 +5,8 @@ use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use keycard_wallet::{KeycardWallet, python_path}; use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ - Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, - SharedSecretKey, + CommitmentSetDigest, DUMMY_COMMITMENT_HASH, Identifier, InputAccountIdentity, MembershipProof, + NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{AccountWithMetadata, Nonce}, encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey}, }; @@ -187,6 +187,7 @@ enum State { pub struct AccountManager { states: Vec, pin: Option, + dummy_commitment_root: CommitmentSetDigest, } impl AccountManager { @@ -340,7 +341,24 @@ 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_root = if has_init_account { + wallet + .get_commitment_root() + .await + .map_err(ExecutionFailureKind::SequencerError)? + .unwrap_or(DUMMY_COMMITMENT_HASH) + } else { + DUMMY_COMMITMENT_HASH + }; + + Ok(Self { + states, + pin, + dummy_commitment_root, + }) } pub fn pre_states(&self) -> Vec { @@ -404,6 +422,7 @@ impl AccountManager { npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, + commitment_root: self.dummy_commitment_root, seed: None, }, }, @@ -424,6 +443,7 @@ impl AccountManager { ssk: pre.ssk, nsk, identifier: pre.identifier, + commitment_root: self.dummy_commitment_root, }, (None, _) => InputAccountIdentity::PrivateUnauthorized { epk: pre.epk.clone(), @@ -431,6 +451,7 @@ impl AccountManager { npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, + commitment_root: self.dummy_commitment_root, }, }, }) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 80b42e17..27023a3e 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, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, SharedSecretKey, + account::Nonce, compute_digest_for_path, program::InstructionData, }; use log::info; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; @@ -508,6 +509,14 @@ impl WalletCore { } } + pub async fn get_commitment_root(&self) -> Result> { + let proof = self + .sequencer_client + .get_proof_for_commitment(DUMMY_COMMITMENT) + .await?; + Ok(proof.map(|p| compute_digest_for_path(&DUMMY_COMMITMENT, &p))) + } + pub fn decode_insert_privacy_preserving_transaction_results( &mut self, tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction,