diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index 7cbc7f94..f89524c7 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 2677bb68..e67b8a69 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 8b2bf53f..58522a1e 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 52509536..cd1a9b2a 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 f75381af..9c18a4e2 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/bridge_lock.bin b/artifacts/lez/programs/bridge_lock.bin index 6dda67f0..34864afc 100644 Binary files a/artifacts/lez/programs/bridge_lock.bin and b/artifacts/lez/programs/bridge_lock.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index f5fdb554..4d216551 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/cross_zone_inbox.bin b/artifacts/lez/programs/cross_zone_inbox.bin index 532173a0..f8ba9029 100644 Binary files a/artifacts/lez/programs/cross_zone_inbox.bin and b/artifacts/lez/programs/cross_zone_inbox.bin differ diff --git a/artifacts/lez/programs/cross_zone_outbox.bin b/artifacts/lez/programs/cross_zone_outbox.bin index ad36bdba..c463dd5c 100644 Binary files a/artifacts/lez/programs/cross_zone_outbox.bin and b/artifacts/lez/programs/cross_zone_outbox.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index 06d032b0..f9abdd3a 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 67e746ec..ba555a20 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 fd945598..23e71794 100644 Binary files a/artifacts/lez/programs/pinata_token.bin and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/ping_receiver.bin b/artifacts/lez/programs/ping_receiver.bin index a300a4e6..33316416 100644 Binary files a/artifacts/lez/programs/ping_receiver.bin and b/artifacts/lez/programs/ping_receiver.bin differ diff --git a/artifacts/lez/programs/ping_sender.bin b/artifacts/lez/programs/ping_sender.bin index ffb096c9..7ba0ba41 100644 Binary files a/artifacts/lez/programs/ping_sender.bin and b/artifacts/lez/programs/ping_sender.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 865d618e..40a96ac2 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 368a7235..fa90bdba 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/artifacts/lez/programs/wrapped_token.bin b/artifacts/lez/programs/wrapped_token.bin index dbe74cf7..52a61dfe 100644 Binary files a/artifacts/lez/programs/wrapped_token.bin and b/artifacts/lez/programs/wrapped_token.bin differ diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 825a089f..b62fe892 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -12,7 +12,8 @@ use lee::{ program::Program, }; use lee_core::{ - DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, NullifierPublicKey, + DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, NullifierPublicKey, NullifierWitness, + PrivateWitness, WitnessKind, account::{Account, AccountWithMetadata}, encryption::ViewingPublicKey, }; @@ -629,14 +630,16 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { instruction, vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { + InputAccountIdentity::Private(PrivateWitness { vpk, random_seed: [0; 32], - npk, identifier: 1337, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program_with_deps, ); @@ -671,13 +674,16 @@ async fn prove_init_with_commitment_root( })?, vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivateForeignInit { + InputAccountIdentity::Private(PrivateWitness { vpk, random_seed: [0; 32], - npk, identifier: 0, - commitment_root, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk, + commitment_root, + }, + }), ], &program.into(), )?; diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index d7ca565d..b73e943a 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -21,7 +21,8 @@ use lee::{ program::Program, }; use lee_core::{ - DUMMY_COMMITMENT_HASH, InputAccountIdentity, NullifierPublicKey, + DUMMY_COMMITMENT_HASH, InputAccountIdentity, NullifierPublicKey, NullifierWitness, + PrivateWitness, WitnessKind, account::{Account, AccountWithMetadata}, encryption::ViewingPublicKey, program::PdaSeed, @@ -65,14 +66,18 @@ async fn fund_private_pda( let account_identities = vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { + InputAccountIdentity::Private(PrivateWitness { vpk, random_seed: [0; 32], - npk, identifier, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: Some((seed, authority_program_id)), - }, + kind: WitnessKind::Pda { + binding: Some((authority_program_id, seed)), + }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ]; let (output, proof) = execute_and_prove( diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 55d88d3f..5977dfc1 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -23,6 +23,7 @@ use lee::{ }; use lee_core::{ DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey, + NullifierWitness, PrivateWitness, WitnessKind, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -291,21 +292,27 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { }) .unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_vpk, random_seed: [0; 32], - view_tag: 0, - nsk: sender_nsk, - membership_proof: proof, identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_nsk, + membership_proof: proof, + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_vpk, random_seed: [0; 32], - npk: recipient_npk, identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_npk, + 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 09ad30af..334cad7c 100644 --- a/lee/privacy_preserving_circuit/src/execution_state.rs +++ b/lee/privacy_preserving_circuit/src/execution_state.rs @@ -4,7 +4,7 @@ use std::{ }; use lee_core::{ - Identifier, InputAccountIdentity, NullifierPublicKey, + Identifier, InputAccountIdentity, NullifierPublicKey, PrivateWitness, WitnessKind, account::{Account, AccountId, AccountWithMetadata}, encryption::ViewingPublicKey, program::{ @@ -311,53 +311,35 @@ impl ExecutionState { // 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, + Some(InputAccountIdentity::Private(PrivateWitness { vpk, identifier, - seed: Some((seed, authority_program_id)), + kind: + WitnessKind::Pda { + binding: Some((authority_program_id, seed)), + }, + nullifier, .. - }) => { + })) => { let expected = AccountId::for_private_pda( authority_program_id, seed, - npk, + &nullifier.npk(), vpk, *identifier, ); assert_eq!( pre_account_id, expected, - "External seed mismatch for PrivatePdaInit at position {pre_state_position}" + "External seed mismatch for private PDA at position {pre_state_position}" ); - Some((*seed, *authority_program_id)) - } - Some(InputAccountIdentity::PrivatePdaUpdate { - nsk, - vpk, - identifier, - seed: Some((seed, authority_program_id)), - .. - }) => { - let npk = NullifierPublicKey::from(nsk); - let expected = AccountId::for_private_pda( - authority_program_id, - seed, - &npk, - vpk, - *identifier, - ); - assert_eq!( - pre_account_id, expected, - "External seed mismatch for PrivatePdaUpdate at position {pre_state_position}" - ); - Some((*seed, *authority_program_id)) + Some((*authority_program_id, *seed)) } _ => 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 { + if let Some((authority_program_id, seed)) = external_seed { assert!( !pre.is_authorized, "Private PDA with externally-provided seed must not be authorized at position {pre_state_position}" diff --git a/lee/privacy_preserving_circuit/src/output.rs b/lee/privacy_preserving_circuit/src/output.rs index d4b9f1af..fb9f8662 100644 --- a/lee/privacy_preserving_circuit/src/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -1,8 +1,8 @@ use lee_core::{ Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme, - EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, - NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, PrivateAction, - PublicAction, SharedSecretKey, + EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierSecretKey, + NullifierWitness, PrivacyPreservingCircuitOutput, PrivateAccountKind, PrivateAction, + PrivateWitness, PublicAction, SharedSecretKey, WitnessKind, account::{Account, AccountId, Nonce}, compute_digest_for_path, encryption::{ViewTag, ViewingPublicKey}, @@ -40,39 +40,107 @@ pub fn compute_circuit_output( post: post_state, }); } - InputAccountIdentity::PrivateAuthorizedInit { + InputAccountIdentity::Private(PrivateWitness { vpk, random_seed, - nsk, identifier, - commitment_root, - } => { - let npk = NullifierPublicKey::from(nsk); - let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier); + kind, + nullifier, + }) => { + let account_id = match kind { + WitnessKind::Regular => { + let derived = AccountId::for_regular_private_account( + &nullifier.npk(), + vpk, + *identifier, + ); + assert_eq!(derived, pre_state.account_id, "AccountId mismatch"); + derived + } + // 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 lifecycle pre-conditions. The supplied npk + // on the witness 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. + WitnessKind::Pda { .. } => pre_state.account_id, + }; - 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" - ); + match (kind, nullifier) { + (WitnessKind::Regular, _) => assert!( + pre_state.is_authorized, + "Regular private account pre-state must be authorized" + ), + (WitnessKind::Pda { .. }, NullifierWitness::Init { .. }) => assert!( + !pre_state.is_authorized, + "Private PDA init requires unauthorized pre_state" + ), + // 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 `binding` is `Some`, execution_state already asserted + // `!pre_state.is_authorized`. + (WitnessKind::Pda { binding }, NullifierWitness::Update { .. }) => assert!( + pre_state.is_authorized ^ binding.is_some(), + "Private PDA update requires authorized pre_state or external seed" + ), + } - let new_nullifier = ( - Nullifier::for_account_initialization(&account_id), - *commitment_root, - ); - let new_nonce = Nonce::private_account_nonce_init(&account_id); - let view_tag = EncryptedAccountData::compute_view_tag(&npk, vpk); + let (new_nullifier, new_nonce, view_tag) = match nullifier { + NullifierWitness::Init { + npk, + commitment_root, + } => { + assert_eq!( + pre_state.account, + Account::default(), + "Private account init requires a default pre-state" + ); + + ( + ( + Nullifier::for_account_initialization(&account_id), + *commitment_root, + ), + Nonce::private_account_nonce_init(&account_id), + EncryptedAccountData::compute_view_tag(npk, vpk), + ) + } + NullifierWitness::Update { + view_tag, + nsk, + membership_proof, + } => ( + compute_update_nullifier_and_set_digest( + membership_proof, + &pre_state.account, + &account_id, + nsk, + ), + pre_state.account.nonce.private_account_nonce_increment(nsk), + *view_tag, + ), + }; + + let account_kind = match kind { + WitnessKind::Regular => PrivateAccountKind::Regular(*identifier), + WitnessKind::Pda { .. } => { + let (authority_program_id, seed) = pda_seed_by_position + .get(&pos) + .expect("private PDA position must be in pda_seed_by_position"); + PrivateAccountKind::Pda { + program_id: *authority_program_id, + seed: *seed, + identifier: *identifier, + } + } + }; emit_private_output( &mut output, post_state, &account_id, - &PrivateAccountKind::Regular(*identifier), + &account_kind, view_tag, vpk, random_seed, @@ -80,180 +148,6 @@ pub fn compute_circuit_output( new_nonce, ); } - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk, - random_seed, - view_tag, - nsk, - membership_proof, - identifier, - } => { - let npk = NullifierPublicKey::from(nsk); - let account_id = AccountId::for_regular_private_account(&npk, vpk, *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, - post_state, - &account_id, - &PrivateAccountKind::Regular(*identifier), - *view_tag, - vpk, - random_seed, - new_nullifier, - new_nonce, - ); - } - InputAccountIdentity::PrivateForeignInit { - vpk, - random_seed, - npk, - identifier, - commitment_root, - } => { - let account_id = AccountId::for_regular_private_account(npk, vpk, *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 unauthorized." - ); - - let new_nullifier = ( - Nullifier::for_account_initialization(&account_id), - *commitment_root, - ); - let new_nonce = Nonce::private_account_nonce_init(&account_id); - let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk); - - emit_private_output( - &mut output, - post_state, - &account_id, - &PrivateAccountKind::Regular(*identifier), - view_tag, - vpk, - random_seed, - new_nullifier, - new_nonce, - ); - } - InputAccountIdentity::PrivatePdaInit { - vpk, - random_seed, - npk, - 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_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), - *commitment_root, - ); - 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"); - let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk); - emit_private_output( - &mut output, - post_state, - &account_id, - &PrivateAccountKind::Pda { - program_id: *authority_program_id, - seed: *seed, - identifier: *identifier, - }, - view_tag, - vpk, - random_seed, - new_nullifier, - new_nonce, - ); - } - InputAccountIdentity::PrivatePdaUpdate { - vpk, - random_seed, - view_tag, - nsk, - 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, - post_state, - &account_id, - &PrivateAccountKind::Pda { - program_id: *authority_program_id, - seed: *seed, - identifier: *identifier, - }, - *view_tag, - vpk, - random_seed, - new_nullifier, - new_nonce, - ); - } } } diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 0044f025..1e3b3515 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -24,74 +24,63 @@ pub struct PrivacyPreservingCircuitInput { } #[derive(Serialize, Deserialize, Clone)] +#[expect( + clippy::large_enum_variant, + reason = "Private carries the ML-KEM viewing key and dominates; boxing it would add a guest heap allocation per witness, and the footprint matches the pre-refactor enum" +)] pub enum InputAccountIdentity { /// Public account. The guest reads pre/post state from `program_outputs` and emits no /// commitment, ciphertext, or nullifier. 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), vpk, identifier)` - /// and matched against `pre_state.account_id`. - PrivateAuthorizedInit { - vpk: ViewingPublicKey, - random_seed: [u8; 32], - nsk: NullifierSecretKey, - identifier: Identifier, - commitment_root: CommitmentSetDigest, - }, - /// Update of an authorized standalone private account: existing on-chain commitment, with - /// membership proof. - PrivateAuthorizedUpdate { - vpk: ViewingPublicKey, - random_seed: [u8; 32], - view_tag: ViewTag, - nsk: NullifierSecretKey, - membership_proof: MembershipProof, - identifier: Identifier, - }, - /// 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. - PrivateForeignInit { - vpk: ViewingPublicKey, - random_seed: [u8; 32], - npk: NullifierPublicKey, - 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 - /// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it - /// as the 4th input. - PrivatePdaInit { - vpk: ViewingPublicKey, - random_seed: [u8; 32], - npk: NullifierPublicKey, - identifier: Identifier, - commitment_root: CommitmentSetDigest, - /// When `Some((seed, authority_program_id))`, the circuit binds this position via the + Private(PrivateWitness), +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PrivateWitness { + pub vpk: ViewingPublicKey, + pub random_seed: [u8; 32], + pub identifier: Identifier, + pub kind: WitnessKind, + pub nullifier: NullifierWitness, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum WitnessKind { + /// Standalone private account. The `account_id` is derived as + /// `AccountId::for_regular_private_account(&npk, vpk, identifier)` and matched against + /// `pre_state.account_id`. + Regular, + /// Private PDA. The npk-to-account_id binding is proven upstream via `Claim::Pda(seed)` or a + /// caller's `pda_seeds` match. The identifier diversifies the PDA within the + /// `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it as the 4th input. + /// An init is unauthorized; on an update, authorization may be established upstream by a + /// caller `pda_seeds` match or a previously-seen authorization in a chained call. + Pda { + /// When `Some((authority_program_id, seed))`, the circuit binds this position via the /// external derivation check /// `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`. - seed: Option<(PdaSeed, ProgramId)>, + binding: Option<(ProgramId, PdaSeed)>, }, - /// Update of an existing private PDA, with membership proof. `npk` is derived - /// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a - /// previously-seen authorization in a chained call. - PrivatePdaUpdate { - vpk: ViewingPublicKey, - random_seed: [u8; 32], +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum NullifierWitness { + /// Init of a private account: no membership proof. The `pre_state` must be + /// `Account::default()`. `npk` is supplied directly, so the caller need not own the account + /// (e.g. a recipient who doesn't yet exist on chain). + Init { + npk: NullifierPublicKey, + commitment_root: CommitmentSetDigest, + }, + /// Update of a private account: existing on-chain commitment, with membership proof. `npk` + /// is derived from `nsk`. + Update { view_tag: ViewTag, 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, 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)>, }, } @@ -119,7 +108,10 @@ impl InputAccountIdentity { pub const fn is_private_pda(&self) -> bool { matches!( self, - Self::PrivatePdaInit { .. } | Self::PrivatePdaUpdate { .. } + Self::Private(PrivateWitness { + kind: WitnessKind::Pda { .. }, + .. + }) ) } @@ -128,22 +120,24 @@ impl InputAccountIdentity { &self, ) -> Option<(NullifierPublicKey, ViewingPublicKey, Identifier)> { match self { - Self::PrivatePdaInit { - npk, + Self::Private(PrivateWitness { vpk, identifier, + kind: WitnessKind::Pda { .. }, + nullifier, .. - } => Some((*npk, vpk.clone(), *identifier)), - Self::PrivatePdaUpdate { - nsk, - vpk, - identifier, - .. - } => Some((NullifierPublicKey::from(nsk), vpk.clone(), *identifier)), - Self::Public - | Self::PrivateAuthorizedInit { .. } - | Self::PrivateAuthorizedUpdate { .. } - | Self::PrivateForeignInit { .. } => None, + }) => Some((nullifier.npk(), vpk.clone(), *identifier)), + Self::Public | Self::Private(_) => None, + } + } +} + +impl NullifierWitness { + #[must_use] + pub fn npk(&self) -> NullifierPublicKey { + match self { + Self::Init { npk, .. } => *npk, + Self::Update { nsk, .. } => NullifierPublicKey::from(nsk), } } } diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index df247532..f6944ec8 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -4,8 +4,8 @@ )] pub use circuit_io::{ - DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, - PrivacyPreservingCircuitOutput, PrivateAction, PublicAction, + DummyInput, InputAccountIdentity, NullifierWitness, PrivacyPreservingCircuitInput, + PrivacyPreservingCircuitOutput, PrivateAction, PrivateWitness, PublicAction, WitnessKind, }; pub use commitment::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs index 033984ce..9a0bb94f 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs @@ -2,7 +2,8 @@ use lee_core::{ Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey, - Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey, + Nullifier, NullifierPublicKey, NullifierWitness, PrivacyPreservingCircuitOutput, + PrivateWitness, SharedSecretKey, WitnessKind, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, program::{PdaSeed, PrivateAccountKind}, }; @@ -88,13 +89,16 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() Program::serialize_instruction(balance_to_move).unwrap(), vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivateForeignInit { + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &crate::test_methods::simple_balance_transfer().into(), ) @@ -191,23 +195,29 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { vec![sender_pre, recipient], Program::serialize_instruction(balance_to_move).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: commitment_set - .get_proof_for(&commitment_sender) - .expect("sender's commitment must be in the set"), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: commitment_set + .get_proof_for(&commitment_sender) + .expect("sender's commitment must be in the set"), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ) @@ -270,13 +280,16 @@ fn init_note_view_tag_is_derived_from_account_keys() { let (output, proof) = execute_and_prove( vec![account], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateForeignInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk: keys.npk(), identifier, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ) .unwrap(); @@ -312,14 +325,17 @@ fn update_note_view_tag_is_the_supplied_value() { let (output, proof) = execute_and_prove( vec![sender], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedUpdate { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - view_tag: fed_tag, - nsk: keys.nsk, - membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), identifier, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: fed_tag, + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), + }, + })], &program.into(), ) .unwrap(); @@ -361,13 +377,16 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { let result = execute_and_prove( vec![pre], instruction, - vec![InputAccountIdentity::PrivateForeignInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: account_keys.vpk(), random_seed: [0; 32], - npk: account_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: account_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program_with_deps, ); @@ -394,14 +413,16 @@ fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() { let (output, _proof) = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.clone().into(), ) .unwrap(); @@ -440,14 +461,16 @@ fn private_pda_init() { let result = execute_and_prove( vec![pda_pre], instruction, - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program_with_deps, ); @@ -492,14 +515,16 @@ fn private_pda_withdraw() { vec![pda_pre, recipient_pre], instruction, vec![ - InputAccountIdentity::PrivatePdaInit { + InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), InputAccountIdentity::Public, ], &program_with_deps, @@ -545,13 +570,16 @@ fn shared_account_receives_via_simple_transfer() { instruction, vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivateForeignInit { + InputAccountIdentity::Private(PrivateWitness { vpk: shared_keys.vpk(), random_seed: [0; 32], - npk: shared_npk, identifier: shared_identifier, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: shared_npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -561,8 +589,9 @@ fn shared_account_receives_via_simple_transfer() { assert_eq!(output.private_actions.len(), 1); } -/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts -/// to `PrivateAccountKind::Regular` carrying the correct identifier. +/// A regular init with an npk derived from the held `nsk` and a non-default identifier +/// produces a ciphertext that decrypts to `PrivateAccountKind::Regular` carrying the correct +/// identifier. #[test] fn private_authorized_init_encrypts_regular_kind_with_identifier() { let program = crate::test_methods::claimer(); @@ -580,13 +609,16 @@ fn private_authorized_init_encrypts_regular_kind_with_identifier() { let (output, _) = execute_and_prove( vec![pre], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - nsk: keys.nsk, identifier, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: NullifierPublicKey::from(&keys.nsk), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ) .unwrap(); @@ -597,8 +629,9 @@ fn private_authorized_init_encrypts_regular_kind_with_identifier() { ); } -/// `PrivateForeignInit` with a non-default identifier produces a ciphertext that decrypts -/// to `PrivateAccountKind::Regular` carrying the correct identifier. +/// A regular init with a directly-supplied npk (the caller does not own the account) and a +/// non-default identifier produces a ciphertext that decrypts to `PrivateAccountKind::Regular` +/// carrying the correct identifier. #[test] fn private_foreign_init_encrypts_regular_kind_with_identifier() { let program = crate::test_methods::claimer(); @@ -616,13 +649,16 @@ fn private_foreign_init_encrypts_regular_kind_with_identifier() { let (output, _) = execute_and_prove( vec![recipient], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateForeignInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk: keys.npk(), identifier, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ) .unwrap(); @@ -633,7 +669,7 @@ fn private_foreign_init_encrypts_regular_kind_with_identifier() { ); } -/// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts +/// A regular update with a non-default identifier produces a ciphertext that decrypts /// to `PrivateAccountKind::Regular` carrying the correct identifier. #[test] fn private_authorized_update_encrypts_regular_kind_with_identifier() { @@ -661,14 +697,17 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() { let (output, _) = execute_and_prove( vec![sender], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedUpdate { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: keys.nsk, - membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), identifier, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), + }, + })], &program.into(), ) .unwrap(); @@ -679,7 +718,7 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() { ); } -/// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts +/// A private-PDA update with a non-default identifier produces a ciphertext that decrypts /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. #[test] fn private_pda_update_encrypts_pda_kind_with_identifier() { @@ -718,15 +757,17 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() { vec![pda_pre, recipient_pre], Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(), vec![ - InputAccountIdentity::PrivatePdaUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: keys.nsk, - membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + }, + }), InputAccountIdentity::Public, ], &program_with_deps, @@ -755,14 +796,16 @@ fn private_pda_init_identifier_mismatch_fails() { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: 99, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ); @@ -797,15 +840,17 @@ fn private_pda_update_identifier_mismatch_fails() { vec![pda_pre, recipient_pre], Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(), vec![ - InputAccountIdentity::PrivatePdaUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: keys.nsk, - membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier: 99, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + }, + }), InputAccountIdentity::Public, ], &program_with_deps, diff --git a/lee/state_machine/src/state/tests/changer_claimer.rs b/lee/state_machine/src/state/tests/changer_claimer.rs index f6bb4932..16b3872e 100644 --- a/lee/state_machine/src/state/tests/changer_claimer.rs +++ b/lee/state_machine/src/state/tests/changer_claimer.rs @@ -71,14 +71,17 @@ fn private_changer_claimer_no_data_change_no_claim_succeeds() { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedUpdate { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + })], &program.into(), ); @@ -102,14 +105,17 @@ fn private_changer_claimer_data_change_no_claim_fails() { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedUpdate { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + })], &program.into(), ); diff --git a/lee/state_machine/src/state/tests/circuit.rs b/lee/state_machine/src/state/tests/circuit.rs index 591235b0..f71f9e09 100644 --- a/lee/state_machine/src/state/tests/circuit.rs +++ b/lee/state_machine/src/state/tests/circuit.rs @@ -55,27 +55,33 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() { // Setting the recipient nsk to authorize the sender. // This should be set to the sender private account in a normal circumstance. - // `PrivateAuthorizedUpdate` derives npk from nsk and asserts equality with + // A regular update derives npk from nsk and asserts equality with // `pre_state.account_id`, so a mismatched nsk fails that check. let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: recipient_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: recipient_keys.nsk, + membership_proof: (0, vec![]), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -111,21 +117,27 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -161,21 +173,27 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_ vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -211,21 +229,27 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -261,21 +285,27 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -309,21 +339,27 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -357,14 +393,16 @@ fn private_pda_without_binding_fails() { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { + InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -390,14 +428,16 @@ fn private_pda_claim_succeeds() { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ); @@ -429,14 +469,16 @@ fn private_pda_npk_mismatch_fails() { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys_b.vpk(), random_seed: [0; 32], - npk: npk_b, identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk: npk_b, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ); @@ -466,14 +508,16 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((seed, seed, callee_id)).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program_with_deps, ); @@ -505,14 +549,16 @@ fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program_with_deps, ); @@ -556,22 +602,26 @@ fn two_private_pda_claims_under_same_seed_are_rejected() { vec![pre_a, pre_b], Program::serialize_instruction(seed).unwrap(), vec![ - InputAccountIdentity::PrivatePdaInit { + InputAccountIdentity::Private(PrivateWitness { vpk: keys_a.vpk(), random_seed: [0; 32], - npk: keys_a.npk(), identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }, - InputAccountIdentity::PrivatePdaInit { + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk: keys_a.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: keys_b.vpk(), random_seed: [0; 32], - npk: keys_b.npk(), identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk: keys_b.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ); @@ -582,7 +632,7 @@ fn two_private_pda_claims_under_same_seed_are_rejected() { /// A private PDA that is reused at top level without an external seed in the identity still /// fails binding. The noop program emits no `Claim::Pda` and there is no caller /// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires. -/// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is +/// Supplying `binding: Some((owner_program_id, seed))` in the witness's `WitnessKind::Pda` is /// the correct path for top-level reuse; this test pins the failure when no seed is provided. #[test] fn private_pda_top_level_reuse_rejected_by_binding_check() { @@ -606,14 +656,16 @@ fn private_pda_top_level_reuse_rejected_by_binding_check() { let result = execute_and_prove( vec![owned_pre_state], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: keys.vpk(), random_seed: [0; 32], - npk, identifier: u128::MAX, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: None, - }], + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Init { + npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ); @@ -693,22 +745,28 @@ fn circuit_should_fail_if_there_are_repeated_ids() { vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (1, vec![]), identifier: 0, - }, - InputAccountIdentity::PrivateAuthorizedUpdate { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: (1, vec![]), identifier: 0, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + }, + }), ], &program.into(), ); @@ -740,13 +798,16 @@ fn private_authorized_uninitialized_account() { let (output, proof) = execute_and_prove( vec![authorized_account], Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: private_keys.vpk(), random_seed: [0; 32], - nsk: private_keys.nsk, identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: NullifierPublicKey::from(&private_keys.nsk), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ) .unwrap(); @@ -786,13 +847,16 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() { let (output, proof) = execute_and_prove( vec![unauthorized_account], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateForeignInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: private_keys.vpk(), random_seed: [0; 32], - npk: private_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: private_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &program.into(), ) .unwrap(); @@ -836,13 +900,16 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { let (output, proof) = execute_and_prove( vec![authorized_account.clone()], Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: private_keys.vpk(), random_seed: [0; 32], - nsk: private_keys.nsk, identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: NullifierPublicKey::from(&private_keys.nsk), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &claimer_program.into(), ) .unwrap(); @@ -878,13 +945,16 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { let res = execute_and_prove( vec![account_metadata], Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: private_keys.vpk(), random_seed: [0; 32], - nsk: private_keys.nsk, identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: NullifierPublicKey::from(&private_keys.nsk), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &noop_program.into(), ); @@ -945,14 +1015,18 @@ fn two_private_pda_family_members_receive_and_spend() { Program::serialize_instruction(amount).unwrap(), vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { + InputAccountIdentity::Private(PrivateWitness { vpk: alice_keys.vpk(), random_seed: [0; 32], - npk: alice_npk, identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: Some((seed, proxy_id)), - }, + kind: WitnessKind::Pda { + binding: Some((proxy_id, seed)), + }, + nullifier: NullifierWitness::Init { + npk: alice_npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &simple_transfer.clone().into(), ) @@ -980,14 +1054,18 @@ fn two_private_pda_family_members_receive_and_spend() { Program::serialize_instruction(amount).unwrap(), vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { + InputAccountIdentity::Private(PrivateWitness { vpk: alice_keys.vpk(), random_seed: [0; 32], - npk: alice_npk, identifier: 1, - commitment_root: DUMMY_COMMITMENT_HASH, - seed: Some((seed, proxy_id)), - }, + kind: WitnessKind::Pda { + binding: Some((proxy_id, seed)), + }, + nullifier: NullifierWitness::Init { + npk: alice_npk, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &simple_transfer.into(), ) @@ -1019,17 +1097,19 @@ fn two_private_pda_family_members_receive_and_spend() { ], Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), vec![ - InputAccountIdentity::PrivatePdaUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: alice_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: alice_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&commitment_pda_0) - .expect("pda_0 must be in state"), identifier: 0, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_0) + .expect("pda_0 must be in state"), + }, + }), InputAccountIdentity::Public, ], &spend_with_deps, @@ -1056,17 +1136,19 @@ fn two_private_pda_family_members_receive_and_spend() { ], Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), vec![ - InputAccountIdentity::PrivatePdaUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: alice_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: alice_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&commitment_pda_1) - .expect("pda_1 must be in state"), identifier: 1, - seed: None, - }, + kind: WitnessKind::Pda { binding: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1) + .expect("pda_1 must be in state"), + }, + }), InputAccountIdentity::Public, ], &spend_with_deps, @@ -1085,7 +1167,7 @@ fn two_private_pda_family_members_receive_and_spend() { assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount); - // Re-fund alice_pda_1 top-level via simple_transfer using PrivatePdaUpdate with an + // Re-fund alice_pda_1 top-level via simple_transfer using a private-PDA update with an // external seed. let alice_pda_1_account_after_spend = Account { program_owner: simple_transfer_id, @@ -1108,17 +1190,21 @@ fn two_private_pda_family_members_receive_and_spend() { Program::serialize_instruction(amount).unwrap(), vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: alice_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: alice_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&commitment_pda_1_after_spend) - .expect("pda_1 after spend must be in state"), identifier: 1, - seed: Some((seed, proxy_id)), - }, + kind: WitnessKind::Pda { + binding: Some((proxy_id, seed)), + }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1_after_spend) + .expect("pda_1 after spend must be in state"), + }, + }), ], &crate::test_methods::simple_balance_transfer().into(), ) diff --git a/lee/state_machine/src/state/tests/claiming.rs b/lee/state_machine/src/state/tests/claiming.rs index a5e1c3b2..68c3cf5e 100644 --- a/lee/state_machine/src/state/tests/claiming.rs +++ b/lee/state_machine/src/state/tests/claiming.rs @@ -325,16 +325,19 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() { vec![sender_pre, recipient_pre], Program::serialize_instruction(balance).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender's commitment must be in state"), identifier: 0, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + }, + }), InputAccountIdentity::Public, ], &program.into(), @@ -439,26 +442,32 @@ fn private_chained_call(number_of_calls: u32) { vec![to_account, from_account], Program::serialize_instruction(instruction).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: from_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: from_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&from_commitment) - .expect("from's commitment must be in state"), identifier: 0, - }, - InputAccountIdentity::PrivateAuthorizedUpdate { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: from_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&from_commitment) + .expect("from's commitment must be in state"), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: to_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: to_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&to_commitment) - .expect("to's commitment must be in state"), identifier: 0, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: to_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&to_commitment) + .expect("to's commitment must be in state"), + }, + }), ], &program_with_deps, ) diff --git a/lee/state_machine/src/state/tests/mod.rs b/lee/state_machine/src/state/tests/mod.rs index 06a05000..878f722c 100644 --- a/lee/state_machine/src/state/tests/mod.rs +++ b/lee/state_machine/src/state/tests/mod.rs @@ -8,7 +8,8 @@ use std::collections::HashMap; use lee_core::{ BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, - NullifierPublicKey, NullifierSecretKey, Timestamp, + NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, Timestamp, + WitnessKind, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, program::{ @@ -279,13 +280,16 @@ fn shielded_balance_transfer_for_tests( Program::serialize_instruction(balance_to_move).unwrap(), vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivateForeignInit { + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &crate::test_methods::simple_balance_transfer().into(), ) @@ -323,23 +327,29 @@ fn private_balance_transfer_for_tests( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender's commitment must be in state"), identifier: 0, - }, - InputAccountIdentity::PrivateForeignInit { + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + }, + }), + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - npk: recipient_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: recipient_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + }), ], &program.into(), ) @@ -378,16 +388,19 @@ fn deshielded_balance_transfer_for_tests( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: sender_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: sender_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender's commitment must be in state"), identifier: 0, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + }, + }), InputAccountIdentity::Public, ], &program.into(), diff --git a/lee/state_machine/src/state/tests/privacy_preserving.rs b/lee/state_machine/src/state/tests/privacy_preserving.rs index e01cacbd..afc9d88a 100644 --- a/lee/state_machine/src/state/tests/privacy_preserving.rs +++ b/lee/state_machine/src/state/tests/privacy_preserving.rs @@ -521,16 +521,19 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { Program::serialize_instruction(instruction).unwrap(), vec![ InputAccountIdentity::Public, - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: recipient_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: recipient_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&recipient_commitment) - .expect("recipient's commitment must be in state"), identifier: 0, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: recipient_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&recipient_commitment) + .expect("recipient's commitment must be in state"), + }, + }), ], &program_with_deps, ); diff --git a/lee/state_machine/src/state/tests/validity_window.rs b/lee/state_machine/src/state/tests/validity_window.rs index be51b1f6..c39571ea 100644 --- a/lee/state_machine/src/state/tests/validity_window.rs +++ b/lee/state_machine/src/state/tests/validity_window.rs @@ -138,13 +138,16 @@ fn validity_window_works_in_privacy_preserving_transactions( let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateForeignInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: account_keys.vpk(), random_seed: [0; 32], - npk: account_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: account_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &validity_window_program.into(), ) .unwrap(); @@ -203,13 +206,16 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions( let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateForeignInit { + vec![InputAccountIdentity::Private(PrivateWitness { vpk: account_keys.vpk(), random_seed: [0; 32], - npk: account_keys.npk(), identifier: 0, - commitment_root: DUMMY_COMMITMENT_HASH, - }], + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Init { + npk: account_keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], &validity_window_program.into(), ) .unwrap(); diff --git a/lee/state_machine/src/validated_state_diff/tests.rs b/lee/state_machine/src/validated_state_diff/tests.rs index 19b00939..b3db107a 100644 --- a/lee/state_machine/src/validated_state_diff/tests.rs +++ b/lee/state_machine/src/validated_state_diff/tests.rs @@ -78,7 +78,7 @@ fn public_diff_reflects_a_successful_transfer() { #[test] fn privacy_malicious_programs_cannot_drain_public_victim() { use lee_core::{ - Commitment, InputAccountIdentity, + Commitment, InputAccountIdentity, NullifierWitness, PrivateWitness, WitnessKind, account::{Account, AccountWithMetadata}, }; @@ -164,14 +164,17 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { // [1] victim — first seen in simple_balance_transfer's program_output.pre_states // [2] recipient — first seen in simple_balance_transfer's program_output.pre_states let account_identities = vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: attacker_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: attacker_keys.nsk, - membership_proof, identifier: 0, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: attacker_keys.nsk, + membership_proof, + }, + }), InputAccountIdentity::Public, // victim InputAccountIdentity::Public, // recipient ]; @@ -213,7 +216,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { /// verbatim, the attacker must choose how to declare the victim in `account_identities`. /// There are two routes, both closed: /// -/// - **mask=1 (`PrivateAuthorizedUpdate`)**: the circuit derives `account_id = +/// - **mask=1 (regular update)**: the circuit derives `account_id = /// AccountId::for_regular_private_account(&npk_from(nsk), identifier)` and asserts it matches /// `pre_state.account_id`. Passing this check requires the victim's `nsk`, which the attacker /// does not have. `execute_and_prove` panics inside the ZKVM and no proof is produced. @@ -227,7 +230,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { #[test] fn privacy_malicious_programs_cannot_drain_private_victim() { use lee_core::{ - Commitment, InputAccountIdentity, + Commitment, InputAccountIdentity, NullifierWitness, PrivateWitness, WitnessKind, account::{Account, AccountWithMetadata}, }; @@ -321,16 +324,19 @@ fn privacy_malicious_programs_cannot_drain_private_victim() { // [2] recipient — first seen in simple_balance_transfer's program_output.pre_states // // Victim is marked Public: the attacker has no nsk for the victim's private account, - // so PrivateAuthorizedUpdate is not an option. + // so a regular update is not an option. let account_identities = vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { + InputAccountIdentity::Private(PrivateWitness { vpk: attacker_keys.vpk(), random_seed: [0; 32], - view_tag: 0, - nsk: attacker_keys.nsk, - membership_proof, identifier: 0, - }, + kind: WitnessKind::Regular, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: attacker_keys.nsk, + membership_proof, + }, + }), InputAccountIdentity::Public, // victim — attacker lacks victim's nsk InputAccountIdentity::Public, // recipient ]; diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index 3f7c43e0..bad16212 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -5,7 +5,8 @@ use keycard_wallet::KeycardWallet; use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ Commitment, CommitmentSetDigest, DummyInput, Identifier, InputAccountIdentity, MembershipProof, - NullifierPublicKey, NullifierSecretKey, PrivateAccountKind, SharedSecretKey, + NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateAccountKind, PrivateWitness, + SharedSecretKey, WitnessKind, account::{Account, AccountWithMetadata, Nonce}, compute_digest_for_path, encryption::{ @@ -430,60 +431,42 @@ impl AccountManager { self.dummy_inputs(Self::MAX_PRIVATE_ACCOUNTS.saturating_sub(private_count)) } - /// Build the per-account input vec for the privacy-preserving circuit. Each variant carries - /// exactly the fields the circuit's code path for that account needs, with the ephemeral - /// keys (`ssk`) drawn from the cached values that `private_account_keys` and the message - /// construction also use, so all three views agree on the same ephemeral key. + /// Build the per-account input vec for the privacy-preserving circuit. The `kind` and + /// `nullifier` axes select exactly the fields the circuit's code path for that account + /// needs, with the ephemeral keys (`ssk`) drawn from the cached values that + /// `private_account_keys` and the message construction also use, so all three views agree + /// on the same ephemeral key. pub fn account_identities(&self) -> Vec { self.states .iter() .map(|state| match state { State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public, - State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) { - (Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate { - vpk: pre.vpk.clone(), - random_seed: pre.random_seed, - view_tag: random_view_tag(), - nsk, - membership_proof, - identifier: pre.identifier, - seed: None, + State::Private(pre) => InputAccountIdentity::Private(PrivateWitness { + vpk: pre.vpk.clone(), + random_seed: pre.random_seed, + identifier: pre.identifier, + kind: if pre.is_pda { + WitnessKind::Pda { binding: None } + } else { + WitnessKind::Regular }, - _ => InputAccountIdentity::PrivatePdaInit { - vpk: pre.vpk.clone(), - random_seed: pre.random_seed, - npk: pre.npk, - identifier: pre.identifier, - commitment_root: self.dummy_commitment_root, - seed: None, - }, - }, - State::Private(pre) => match (pre.nsk, pre.proof.clone()) { - (Some(nsk), Some(membership_proof)) => { - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: pre.vpk.clone(), - random_seed: pre.random_seed, + nullifier: match (pre.nsk, pre.proof.clone()) { + (Some(nsk), Some(membership_proof)) => NullifierWitness::Update { view_tag: random_view_tag(), nsk, membership_proof, - identifier: pre.identifier, - } - } - (Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit { - vpk: pre.vpk.clone(), - random_seed: pre.random_seed, - nsk, - identifier: pre.identifier, - commitment_root: self.dummy_commitment_root, + }, + (nsk, _) => NullifierWitness::Init { + // A regular init recomputes the npk from the key the wallet holds; + // a PDA's stored npk is the owner's, so it is passed through. + npk: match nsk { + Some(nsk) if !pre.is_pda => NullifierPublicKey::from(&nsk), + _ => pre.npk, + }, + commitment_root: self.dummy_commitment_root, + }, }, - (None, _) => InputAccountIdentity::PrivateForeignInit { - vpk: pre.vpk.clone(), - random_seed: pre.random_seed, - npk: pre.npk, - identifier: pre.identifier, - commitment_root: self.dummy_commitment_root, - }, - }, + }), }) .collect() } @@ -552,7 +535,7 @@ struct AccountPreparedData { proof: Option, random_seed: [u8; 32], /// True when this account is a private PDA (owned or foreign). Used by `account_identities()` - /// to select `PrivatePdaInit`/`PrivatePdaUpdate` rather than the standalone private variants. + /// to select `WitnessKind::Pda` rather than `WitnessKind::Regular`. is_pda: bool, } diff --git a/test_fixtures/fixtures/prebuilt_sequencer_db.dump b/test_fixtures/fixtures/prebuilt_sequencer_db.dump index d56c32dc..21d07e78 100644 Binary files a/test_fixtures/fixtures/prebuilt_sequencer_db.dump and b/test_fixtures/fixtures/prebuilt_sequencer_db.dump differ