From 526f9ccb32f68874c1ba908ee2f6e4eb182cb28f Mon Sep 17 00:00:00 2001 From: agureev Date: Fri, 19 Jun 2026 22:35:41 +0400 Subject: [PATCH] feat(ppc)!: introduce protocol-level changes for vpk binding BREAKING: Before: The epk and the vpk of the receiver were not bound to the ss that was directly fed to the circuit. After: The ss, epk, tag fields are removed as explicit arguments per-account and instead replaced by supplying a vpk, esk per account. The ss, epk, tag all constructed in-circuit. Account ID generation now uses vpk as additional argument. Mitigation: Change Account ID generation to include the vpk, change proving inputs. --- lee/state_machine/core/src/circuit_io.rs | 63 +++++++------- lee/state_machine/core/src/nullifier.rs | 29 ++++--- lee/state_machine/core/src/program.rs | 55 +++++++----- .../execution_state.rs | 52 +++++++----- .../bin/privacy_preserving_circuit/output.rs | 83 ++++++++++--------- 5 files changed, 159 insertions(+), 123 deletions(-) diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 78bfa24f..62b36481 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize}; use crate::{ Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey, - NullifierSecretKey, SharedSecretKey, + NullifierSecretKey, account::{Account, AccountWithMetadata}, - encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag}, + encryption::{EncryptedAccountData, EphemeralSecretKey, ViewingPublicKey}, program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow}, }; @@ -14,15 +14,13 @@ pub struct PrivacyPreservingCircuitInput { pub program_outputs: Vec, /// One entry per `pre_state`, in the same order as the program's `pre_states`. /// Length must equal the number of `pre_states` derived from `program_outputs`. - /// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions` + /// The guest's `private_pda_by_position` and `private_pda_bound_positions` /// rely on this position alignment. pub account_identities: Vec, /// Program ID. pub program_id: ProgramId, } -/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields -/// the guest needs for that account's code path. #[derive(Serialize, Deserialize, Clone)] pub enum InputAccountIdentity { /// Public account. The guest reads pre/post state from `program_outputs` and emits no @@ -30,21 +28,19 @@ pub enum InputAccountIdentity { Public, /// Init of an authorized standalone private account: no membership proof. The `pre_state` /// must be `Account::default()`. The `account_id` is derived as - /// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and - /// matched against `pre_state.account_id`. + /// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), vpk, identifier)` + /// and matched against `pre_state.account_id`. PrivateAuthorizedInit { - epk: EphemeralPublicKey, - view_tag: ViewTag, - ssk: SharedSecretKey, + vpk: ViewingPublicKey, + esk: EphemeralSecretKey, nsk: NullifierSecretKey, identifier: Identifier, }, /// Update of an authorized standalone private account: existing on-chain commitment, with /// membership proof. PrivateAuthorizedUpdate { - epk: EphemeralPublicKey, - view_tag: ViewTag, - ssk: SharedSecretKey, + vpk: ViewingPublicKey, + esk: EphemeralSecretKey, nsk: NullifierSecretKey, membership_proof: MembershipProof, identifier: Identifier, @@ -52,10 +48,9 @@ pub enum InputAccountIdentity { /// Init of a standalone private account the caller does not own (e.g. a recipient who /// doesn't yet exist on chain). No `nsk`, no membership proof. PrivateUnauthorized { - epk: EphemeralPublicKey, - view_tag: ViewTag, + vpk: ViewingPublicKey, + esk: EphemeralSecretKey, npk: NullifierPublicKey, - ssk: SharedSecretKey, identifier: Identifier, }, /// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream @@ -63,14 +58,13 @@ pub enum InputAccountIdentity { /// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it /// as the 4th input. PrivatePdaInit { - epk: EphemeralPublicKey, - view_tag: ViewTag, + vpk: ViewingPublicKey, + esk: EphemeralSecretKey, npk: NullifierPublicKey, - ssk: SharedSecretKey, identifier: Identifier, /// 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) == + /// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) == /// pre_state.account_id` rather than requiring a `Claim::Pda` or caller /// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized /// == false`. @@ -80,15 +74,14 @@ pub enum InputAccountIdentity { /// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a /// previously-seen authorization in a chained call. PrivatePdaUpdate { - epk: EphemeralPublicKey, - view_tag: ViewTag, - ssk: SharedSecretKey, + vpk: ViewingPublicKey, + esk: EphemeralSecretKey, nsk: NullifierSecretKey, membership_proof: MembershipProof, identifier: Identifier, /// 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) == + /// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) == /// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish /// the binding. The `pre_state` must have `is_authorized == false`. seed: Option<(PdaSeed, ProgramId)>, @@ -109,17 +102,23 @@ impl InputAccountIdentity { ) } - /// For private PDA variants, return the `(npk, identifier)` pair. `Init` carries both - /// directly; `Update` derives `npk` from `nsk`. For non-PDA variants returns `None`. #[must_use] - pub fn npk_if_private_pda(&self) -> Option<(NullifierPublicKey, Identifier)> { + pub fn npk_vpk_if_private_pda( + &self, + ) -> Option<(NullifierPublicKey, ViewingPublicKey, Identifier)> { match self { Self::PrivatePdaInit { - npk, identifier, .. - } => Some((*npk, *identifier)), + npk, + vpk, + identifier, + .. + } => Some((*npk, vpk.clone(), *identifier)), Self::PrivatePdaUpdate { - nsk, identifier, .. - } => Some((NullifierPublicKey::from(nsk), *identifier)), + nsk, + vpk, + identifier, + .. + } => Some((NullifierPublicKey::from(nsk), vpk.clone(), *identifier)), Self::Public | Self::PrivateAuthorizedInit { .. } | Self::PrivateAuthorizedUpdate { .. } @@ -158,7 +157,7 @@ mod tests { use crate::{ Commitment, Nullifier, account::{Account, AccountId, AccountWithMetadata, Nonce}, - encryption::Ciphertext, + encryption::{Ciphertext, EphemeralPublicKey}, }; #[test] diff --git a/lee/state_machine/core/src/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index 0490ac00..18b776ca 100644 --- a/lee/state_machine/core/src/nullifier.rs +++ b/lee/state_machine/core/src/nullifier.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; -use crate::{Commitment, account::AccountId}; +use crate::{Commitment, account::AccountId, encryption::ViewingPublicKey}; const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00"; @@ -16,12 +16,16 @@ impl AccountId { /// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public /// key and identifier. #[must_use] - pub fn for_regular_private_account(npk: &NullifierPublicKey, identifier: Identifier) -> Self { - // 32 bytes prefix || 32 bytes npk || 16 bytes identifier - let mut bytes = [0; 80]; + pub fn for_regular_private_account( + npk: &NullifierPublicKey, + vpk: &ViewingPublicKey, + identifier: Identifier, + ) -> Self { + let mut bytes = [0_u8; 32 + 32 + ViewingPublicKey::LEN + 16]; bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX); bytes[32..64].copy_from_slice(&npk.0); - bytes[64..80].copy_from_slice(&identifier.to_le_bytes()); + bytes[64..64 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes()); + bytes[64 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes()); Self::new( Impl::hash_bytes(&bytes) @@ -32,9 +36,9 @@ impl AccountId { } } -impl From<(&NullifierPublicKey, Identifier)> for AccountId { - fn from((npk, identifier): (&NullifierPublicKey, Identifier)) -> Self { - Self::for_regular_private_account(npk, identifier) +impl From<(&NullifierPublicKey, &ViewingPublicKey, Identifier)> for AccountId { + fn from((npk, vpk, identifier): (&NullifierPublicKey, &ViewingPublicKey, Identifier)) -> Self { + Self::for_regular_private_account(npk, vpk, identifier) } } @@ -158,12 +162,13 @@ mod tests { 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, ]; let npk = NullifierPublicKey::from(&nsk); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let expected_account_id = AccountId::new([ 165, 52, 40, 32, 231, 171, 113, 10, 65, 241, 156, 72, 154, 207, 122, 192, 15, 46, 50, 253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142, ]); - let account_id = AccountId::for_regular_private_account(&npk, 0); + let account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); assert_eq!(account_id, expected_account_id); } @@ -175,12 +180,13 @@ mod tests { 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, ]; let npk = NullifierPublicKey::from(&nsk); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let expected_account_id = AccountId::new([ 203, 201, 109, 245, 40, 54, 195, 12, 55, 33, 0, 86, 245, 65, 70, 156, 24, 249, 26, 95, 56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72, ]); - let account_id = AccountId::for_regular_private_account(&npk, 1); + let account_id = AccountId::for_regular_private_account(&npk, &vpk, 1); assert_eq!(account_id, expected_account_id); } @@ -193,12 +199,13 @@ mod tests { 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, ]; let npk = NullifierPublicKey::from(&nsk); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let expected_account_id = AccountId::new([ 178, 16, 226, 206, 217, 38, 38, 45, 155, 240, 226, 253, 168, 87, 146, 70, 72, 32, 174, 19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196, ]); - let account_id = AccountId::for_regular_private_account(&npk, identifier); + let account_id = AccountId::for_regular_private_account(&npk, &vpk, identifier); assert_eq!(account_id, expected_account_id); } diff --git a/lee/state_machine/core/src/program.rs b/lee/state_machine/core/src/program.rs index c5949dcf..6a5c6b0f 100644 --- a/lee/state_machine/core/src/program.rs +++ b/lee/state_machine/core/src/program.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::{ BlockId, Identifier, NullifierPublicKey, Timestamp, account::{Account, AccountId, AccountWithMetadata}, + encryption::ViewingPublicKey, }; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; @@ -154,19 +155,21 @@ impl AccountId { program_id: &ProgramId, seed: &PdaSeed, npk: &NullifierPublicKey, + vpk: &ViewingPublicKey, identifier: Identifier, ) -> Self { use risc0_zkvm::sha::{Impl, Sha256 as _}; const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00"; - let mut bytes = [0_u8; 144]; + let mut bytes = [0_u8; 32 + 32 + 32 + 32 + ViewingPublicKey::LEN + 16]; bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX); let program_id_bytes: &[u8] = bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]"); bytes[32..64].copy_from_slice(program_id_bytes); bytes[64..96].copy_from_slice(&seed.0); bytes[96..128].copy_from_slice(&npk.to_byte_array()); - bytes[128..144].copy_from_slice(&identifier.to_le_bytes()); + bytes[128..128 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes()); + bytes[128 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes()); Self::new( Impl::hash_bytes(&bytes) .as_bytes() @@ -177,16 +180,20 @@ impl AccountId { /// Derives the [`AccountId`] for a private account from the nullifier public key and kind. #[must_use] - pub fn for_private_account(npk: &NullifierPublicKey, kind: &PrivateAccountKind) -> Self { + pub fn for_private_account( + npk: &NullifierPublicKey, + vpk: &ViewingPublicKey, + kind: &PrivateAccountKind, + ) -> Self { match kind { PrivateAccountKind::Regular(identifier) => { - Self::for_regular_private_account(npk, *identifier) + Self::for_regular_private_account(npk, vpk, *identifier) } PrivateAccountKind::Pda { program_id, seed, identifier, - } => Self::for_private_pda(program_id, seed, npk, *identifier), + } => Self::for_private_pda(program_id, seed, npk, vpk, *identifier), } } } @@ -953,13 +960,14 @@ mod tests { let program_id: ProgramId = [1; 8]; let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let identifier: Identifier = u128::MAX; let expected = AccountId::new([ 59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54, 124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17, ]); assert_eq!( - AccountId::for_private_pda(&program_id, &seed, &npk, identifier), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier), expected ); } @@ -971,9 +979,10 @@ mod tests { let seed = PdaSeed::new([2; 32]); let npk_a = NullifierPublicKey([3; 32]); let npk_b = NullifierPublicKey([4; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); assert_ne!( - AccountId::for_private_pda(&program_id, &seed, &npk_a, u128::MAX), - AccountId::for_private_pda(&program_id, &seed, &npk_b, u128::MAX), + AccountId::for_private_pda(&program_id, &seed, &npk_a, &vpk, u128::MAX), + AccountId::for_private_pda(&program_id, &seed, &npk_b, &vpk, u128::MAX), ); } @@ -984,9 +993,10 @@ mod tests { let seed_a = PdaSeed::new([2; 32]); let seed_b = PdaSeed::new([5; 32]); let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); assert_ne!( - AccountId::for_private_pda(&program_id, &seed_a, &npk, u128::MAX), - AccountId::for_private_pda(&program_id, &seed_b, &npk, u128::MAX), + AccountId::for_private_pda(&program_id, &seed_a, &npk, &vpk, u128::MAX), + AccountId::for_private_pda(&program_id, &seed_b, &npk, &vpk, u128::MAX), ); } @@ -997,9 +1007,10 @@ mod tests { let program_id_b: ProgramId = [9; 8]; let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); assert_ne!( - AccountId::for_private_pda(&program_id_a, &seed, &npk, u128::MAX), - AccountId::for_private_pda(&program_id_b, &seed, &npk, u128::MAX), + AccountId::for_private_pda(&program_id_a, &seed, &npk, &vpk, u128::MAX), + AccountId::for_private_pda(&program_id_b, &seed, &npk, &vpk, u128::MAX), ); } @@ -1010,13 +1021,14 @@ mod tests { let program_id: ProgramId = [1; 8]; let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); assert_ne!( - AccountId::for_private_pda(&program_id, &seed, &npk, 0), - AccountId::for_private_pda(&program_id, &seed, &npk, 1), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 1), ); assert_ne!( - AccountId::for_private_pda(&program_id, &seed, &npk, 0), - AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX), ); } @@ -1027,7 +1039,8 @@ mod tests { let program_id: ProgramId = [1; 8]; let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); - let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX); let public_id = AccountId::for_public_pda(&program_id, &seed); assert_ne!(private_id, public_id); } @@ -1064,22 +1077,24 @@ mod tests { let program_id: ProgramId = [1; 8]; let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let identifier: Identifier = 77; assert_eq!( - AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)), - AccountId::for_regular_private_account(&npk, identifier), + AccountId::for_private_account(&npk, &vpk, &PrivateAccountKind::Regular(identifier)), + AccountId::for_regular_private_account(&npk, &vpk, identifier), ); assert_eq!( AccountId::for_private_account( &npk, + &vpk, &PrivateAccountKind::Pda { program_id, seed, identifier } ), - AccountId::for_private_pda(&program_id, &seed, &npk, identifier), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier), ); } diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs b/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs index 8d920068..09ad30af 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs @@ -6,6 +6,7 @@ use std::{ use lee_core::{ Identifier, InputAccountIdentity, NullifierPublicKey, account::{Account, AccountId, AccountWithMetadata}, + encryption::ViewingPublicKey, program::{ AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow, @@ -21,7 +22,7 @@ pub struct ExecutionState { block_validity_window: BlockValidityWindow, timestamp_validity_window: TimestampValidityWindow, /// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound to - /// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk, + /// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk, vpk, /// identifier)` check. /// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on /// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state` @@ -43,12 +44,13 @@ pub struct ExecutionState { /// `AccountId` entry or as an equality check against the existing one, making the rule: one /// `(program, seed)` → one account per tx. pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>, - /// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk, + /// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk, vpk, /// identifier) supplied for that position. Built once in `derive_from_outputs` by walking - /// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and + /// `account_identities` and consulting `npk_vpk_if_private_pda`. Used later by the claim and /// caller-seeds authorization paths to verify - /// `AccountId::for_private_pda(program_id, seed, npk, identifier) == pre_state.account_id`. - private_pda_npk_by_position: HashMap, + /// `AccountId::for_private_pda(program_id, seed, npk, vpk, identifier) == + /// pre_state.account_id`. + private_pda_by_position: HashMap, authorized_accounts: HashSet, } @@ -63,11 +65,13 @@ impl ExecutionState { // 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(); + let mut private_pda_by_position: HashMap< + usize, + (NullifierPublicKey, ViewingPublicKey, Identifier), + > = 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)); + if let Some((npk, vpk, identifier)) = account_identity.npk_vpk_if_private_pda() { + private_pda_by_position.insert(pos, (npk, vpk, identifier)); } } @@ -107,7 +111,7 @@ impl ExecutionState { timestamp_validity_window, private_pda_bound_positions: HashMap::new(), pda_family_binding: HashMap::new(), - private_pda_npk_by_position, + private_pda_by_position, authorized_accounts: HashSet::new(), }; @@ -289,7 +293,7 @@ impl ExecutionState { 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, + &self.private_pda_by_position, &mut self.authorized_accounts, pre_account_id, pre_state_position, @@ -309,6 +313,7 @@ impl ExecutionState { let external_seed = match account_identities.get(pre_state_position) { Some(InputAccountIdentity::PrivatePdaInit { npk, + vpk, identifier, seed: Some((seed, authority_program_id)), .. @@ -317,6 +322,7 @@ impl ExecutionState { authority_program_id, seed, npk, + vpk, *identifier, ); assert_eq!( @@ -327,6 +333,7 @@ impl ExecutionState { } Some(InputAccountIdentity::PrivatePdaUpdate { nsk, + vpk, identifier, seed: Some((seed, authority_program_id)), .. @@ -336,6 +343,7 @@ impl ExecutionState { authority_program_id, seed, &npk, + vpk, *identifier, ); assert_eq!( @@ -416,14 +424,19 @@ impl ExecutionState { match claim { Claim::Authorized => {} Claim::Pda(seed) => { - let (npk, identifier) = self - .private_pda_npk_by_position + let (npk, vpk, identifier) = self + .private_pda_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); + let pda = AccountId::for_private_pda( + &program_id, + &seed, + npk, + vpk, + *identifier, + ); assert_eq!( pre_account_id, pda, "Invalid private PDA claim for account {pre_account_id}" @@ -548,7 +561,7 @@ fn bind_private_pda_position( fn resolve_authorization_and_record_bindings( pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>, private_pda_bound_positions: &mut HashMap, - private_pda_npk_by_position: &HashMap, + private_pda_by_position: &HashMap, authorized_accounts: &mut HashSet, pre_account_id: AccountId, pre_state_position: usize, @@ -562,9 +575,10 @@ fn resolve_authorization_and_record_bindings( if AccountId::for_public_pda(&caller, seed) == pre_account_id { return Some((*seed, false, caller)); } - if let Some((npk, identifier)) = - private_pda_npk_by_position.get(&pre_state_position) - && AccountId::for_private_pda(&caller, seed, npk, *identifier) == pre_account_id + if let Some((npk, vpk, identifier)) = + private_pda_by_position.get(&pre_state_position) + && AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier) + == pre_account_id { return Some((*seed, true, caller)); } diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs index 8c8ec2a4..64c70c1b 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs @@ -1,9 +1,10 @@ use lee_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, - EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, + EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, compute_digest_for_path, + encryption::ViewingPublicKey, }; use crate::execution_state::ExecutionState; @@ -40,14 +41,13 @@ pub fn compute_circuit_output( output.public_post_states.push(post_state); } InputAccountIdentity::PrivateAuthorizedInit { - epk, - view_tag, - ssk, + vpk, + esk, nsk, identifier, } => { let npk = NullifierPublicKey::from(nsk); - let account_id = AccountId::for_regular_private_account(&npk, *identifier); + let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier); assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); assert!( @@ -72,23 +72,22 @@ pub fn compute_circuit_output( post_state, &account_id, &PrivateAccountKind::Regular(*identifier), - ssk, - epk, - *view_tag, + &npk, + vpk, + esk, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivateAuthorizedUpdate { - epk, - view_tag, - ssk, + vpk, + esk, nsk, membership_proof, identifier, } => { let npk = NullifierPublicKey::from(nsk); - let account_id = AccountId::for_regular_private_account(&npk, *identifier); + let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier); assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); assert!( @@ -110,21 +109,20 @@ pub fn compute_circuit_output( post_state, &account_id, &PrivateAccountKind::Regular(*identifier), - ssk, - epk, - *view_tag, + &npk, + vpk, + esk, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivateUnauthorized { - epk, - view_tag, + vpk, + esk, npk, - ssk, identifier, } => { - let account_id = AccountId::for_regular_private_account(npk, *identifier); + let account_id = AccountId::for_regular_private_account(npk, vpk, *identifier); assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); assert_eq!( @@ -149,25 +147,24 @@ pub fn compute_circuit_output( post_state, &account_id, &PrivateAccountKind::Regular(*identifier), - ssk, - epk, - *view_tag, + npk, + vpk, + esk, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivatePdaInit { - epk, - view_tag, - npk: _, - ssk, + vpk, + esk, + npk, identifier, 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 + // the variant has been recorded into `private_pda_by_position` and used // for the binding check; we use `pre_state.account_id` directly for nullifier // and commitment derivation. assert!( @@ -200,17 +197,16 @@ pub fn compute_circuit_output( seed: *seed, identifier: *identifier, }, - ssk, - epk, - *view_tag, + npk, + vpk, + esk, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivatePdaUpdate { - epk, - view_tag, - ssk, + vpk, + esk, nsk, membership_proof, identifier, @@ -235,6 +231,7 @@ pub fn compute_circuit_output( let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); let account_id = pre_state.account_id; + let npk = NullifierPublicKey::from(nsk); let (authority_program_id, seed) = pda_seed_by_position .get(&pos) .expect("PrivatePdaUpdate position must be in pda_seed_by_position"); @@ -248,9 +245,9 @@ pub fn compute_circuit_output( seed: *seed, identifier: *identifier, }, - ssk, - epk, - *view_tag, + &npk, + vpk, + esk, new_nullifier, new_nonce, ); @@ -271,9 +268,9 @@ fn emit_private_output( post_state: Account, account_id: &AccountId, kind: &PrivateAccountKind, - shared_secret: &SharedSecretKey, - epk: &EphemeralPublicKey, - view_tag: u8, + npk: &NullifierPublicKey, + vpk: &ViewingPublicKey, + esk: &EphemeralSecretKey, new_nullifier: (Nullifier, CommitmentSetDigest), new_nonce: Nonce, ) { @@ -283,10 +280,14 @@ fn emit_private_output( post_with_updated_nonce.nonce = new_nonce; let commitment_post = Commitment::new(account_id, &post_with_updated_nonce); + + let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(vpk, esk, *output_index); + let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk); + let encrypted_account = EncryptionScheme::encrypt( &post_with_updated_nonce, kind, - shared_secret, + &shared_secret, &commitment_post, *output_index, ); @@ -296,7 +297,7 @@ fn emit_private_output( .encrypted_private_post_states .push(EncryptedAccountData { ciphertext: encrypted_account, - epk: epk.clone(), + epk, view_tag, }); *output_index = output_index