diff --git a/.deny.toml b/.deny.toml index 4d69e70b..b02f2046 100644 --- a/.deny.toml +++ b/.deny.toml @@ -60,6 +60,7 @@ allow-git = [ "https://github.com/logos-blockchain/logos-blockchain.git", "https://github.com/logos-blockchain/logos-blockchain-circuits.git", "https://github.com/logos-blockchain/logos-blockchain-rust-rapidsnark.git", + "https://github.com/logos-blockchain/sponges", "https://github.com/arkworks-rs/spongefish.git", ] unknown-git = "deny" diff --git a/.github/workflows/bench-regression.yml b/.github/workflows/bench-regression.yml index d5d9568a..d82c0f11 100644 --- a/.github/workflows/bench-regression.yml +++ b/.github/workflows/bench-regression.yml @@ -28,10 +28,6 @@ jobs: - uses: ./.github/actions/install-risc0 - - uses: ./.github/actions/install-logos-blockchain-circuits - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install active toolchain run: rustup install diff --git a/Cargo.lock b/Cargo.lock index 84b5e1d6..fbe52a3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1914,7 +1914,6 @@ dependencies = [ "criterion", "key_protocol", "lee_core", - "rand 0.8.6", ] [[package]] @@ -3850,7 +3849,6 @@ dependencies = [ "anyhow", "arc-swap", "async-stream", - "authenticated_transfer_core", "borsh", "common", "futures", @@ -4441,11 +4439,11 @@ dependencies = [ [[package]] name = "keccak" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +source = "git+https://github.com/logos-blockchain/sponges?rev=3a56e99771beedf04946eab21a4a62adc2951377#3a56e99771beedf04946eab21a4a62adc2951377" dependencies = [ "cfg-if", "cpufeatures 0.3.0", + "risc0-zkvm", ] [[package]] @@ -4553,7 +4551,6 @@ dependencies = [ "anyhow", "borsh", "build_utils", - "env_logger", "hex", "hex-literal 1.1.0", "k256", @@ -4568,7 +4565,6 @@ dependencies = [ "test-case", "test_methods", "thiserror 2.0.18", - "token_core", ] [[package]] @@ -10890,7 +10886,6 @@ dependencies = [ "risc0-zkvm", "sequencer_service_rpc", "serde_json", - "tempfile", "tokio", "vault_core", "wallet", diff --git a/Cargo.toml b/Cargo.toml index 3e71f293..34333b5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,6 +194,10 @@ opt-level = 'z' lto = true codegen-units = 1 +# Keccak speedup for in-guest ML KEM +[patch.crates-io] +keccak = { git = "https://github.com/logos-blockchain/sponges", rev = "3a56e99771beedf04946eab21a4a62adc2951377" } + # Keep backtraces but drop full DWARF type info to avoid LLD OOM/SIGBUS when # linking large integration-test binaries on resource-constrained CI runners. [profile.dev] @@ -339,4 +343,4 @@ clippy.cargo = { level = "deny", priority = -1 } clippy.cargo-common-metadata = "allow" # Reason: hard to address right now and mostly comes from dependencies # so the fix would be just a long list of exceptions. -clippy.multiple-crate-versions = "allow" +clippy.multiple-crate-versions = "allow" \ No newline at end of file diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index fa70e8ee..bb1b66fc 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 7e8d0bb8..2bf13353 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 bed80a76..31604d5a 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 193d4ec2..25ff2172 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 3a22bd01..0063fff9 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index 8c8c1b4f..10cc941c 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index 914059cb..c389298b 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 47bc6158..82e7fb78 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 41b061ef..08900cda 100644 Binary files a/artifacts/lez/programs/pinata_token.bin and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 1882cfb6..568c6f40 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 7e053181..91482c04 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/integration_tests/tests/account.rs b/integration_tests/tests/account.rs index dd6ba4ed..2b69f7e0 100644 --- a/integration_tests/tests/account.rs +++ b/integration_tests/tests/account.rs @@ -141,7 +141,11 @@ async fn import_private_account() -> Result<()> { let mut ctx = TestContext::new().await?; let key_chain = KeyChain::new_os_random(); - let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0)); + let account_id = lee::AccountId::from(( + &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, + 0, + )); let account = lee::Account { program_owner: programs::authenticated_transfer().id(), balance: 777, @@ -198,7 +202,11 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<( let mut ctx = TestContext::new().await?; let key_chain = KeyChain::new_os_random(); - let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0)); + let account_id = lee::AccountId::from(( + &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, + 0, + )); let key_chain_json = serde_json::to_string(&key_chain).context("Failed to serialize key chain")?; diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index b543a2f6..205cbfed 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -8,15 +8,14 @@ use integration_tests::{ private_mention, public_mention, send, sync_private, verify_commitment_is_in_state, }; use lee::{ - AccountId, SharedSecretKey, execute_and_prove, - privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, + AccountId, execute_and_prove, privacy_preserving_transaction::circuit::ProgramWithDependencies, + program::Program, }; use lee_core::{ - DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, - ML_KEM_768_CIPHERTEXT_LEN, Nullifier, NullifierPublicKey, + DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, NullifierPublicKey, account::{Account, AccountWithMetadata}, compute_digest_for_path, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::ViewingPublicKey, }; use log::info; use sequencer_service_rpc::RpcClient as _; @@ -499,14 +498,14 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { sync_private(&mut ctx).await?; // Both accounts must be discovered with the correct balances. - let account_id_1 = AccountId::for_regular_private_account(&npk, identifier_1); + let account_id_1 = AccountId::for_regular_private_account(&npk, &vpk, identifier_1); let acc_1 = ctx .wallet() .get_account_private(account_id_1) .context("account for identifier 1 not found after sync")?; assert_eq!(acc_1.balance, 100); - let account_id_2 = AccountId::for_regular_private_account(&npk, identifier_2); + let account_id_2 = AccountId::for_regular_private_account(&npk, &vpk, identifier_2); let acc_2 = ctx .wallet() .get_account_private(account_id_2) @@ -562,11 +561,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { let nsk: lee_core::NullifierSecretKey = [3; 32]; let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); - let ssk = SharedSecretKey([55_u8; 32]); - let epk = EphemeralPublicKey(vec![55_u8; ML_KEM_768_CIPHERTEXT_LEN]); let attacker_vault_id = { let seed = vault_core::compute_vault_seed(attacker_id); - AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337) + AccountId::for_private_pda(&vault_program_id, &seed, &npk, &vpk, 1337) }; let amount: u128 = 1; @@ -600,10 +597,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { - epk, - view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk), + vpk, + random_seed: [0; 32], npk, - ssk, identifier: 1337, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -632,8 +628,7 @@ async fn prove_init_with_commitment_root( let nsk: lee_core::NullifierSecretKey = [7; 32]; let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); - let ssk = SharedSecretKey([55_u8; 32]); - let recipient_account_id = AccountId::for_regular_private_account(&npk, 0); + let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); let (output, _) = execute_and_prove( @@ -644,10 +639,9 @@ async fn prove_init_with_commitment_root( vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk), + vpk, + random_seed: [0; 32], npk, - ssk, identifier: 0, commitment_root, }, @@ -671,7 +665,8 @@ async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> { let nsk: lee_core::NullifierSecretKey = [7; 32]; let npk = NullifierPublicKey::from(&nsk); - let recipient_account_id = AccountId::for_regular_private_account(&npk, 0); + let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); let output = prove_init_with_commitment_root(&ctx, expected_digest).await?; diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index cea7a707..167d9b66 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -9,7 +9,7 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use integration_tests::{ TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, new_account, private_mention, - public_mention, sync_private, verify_commitment_is_in_state, + public_mention, sync_private, verify_commitment_is_in_state, wait_for_indexer_to_catch_up, }; use log::info; use tokio::test; @@ -115,6 +115,41 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> { Ok(()) } +#[test] +async fn claim_pinata_indexer_keeps_up() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { + to: public_mention(ctx.existing_public_accounts()[0]), + }); + + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + info!("Waiting for indexer to parse blocks"); + wait_for_indexer_to_catch_up(&ctx).await?; + + let winner_ind_state = indexer_service_rpc::RpcClient::get_account( + &**ctx.indexer_client(), + ctx.existing_public_accounts()[0].into(), + ) + .await + .unwrap(); + let winner_seq_state = sequencer_service_rpc::RpcClient::get_account( + ctx.sequencer_client(), + ctx.existing_public_accounts()[0], + ) + .await?; + + assert_eq!(winner_ind_state, winner_seq_state.into()); + + info!("Indexer correctly indexed the pinata claim"); + + Ok(()) +} + #[test] async fn claim_pinata_to_existing_private_account() -> Result<()> { let mut ctx = TestContext::new().await?; diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index 2ccdfad8..0c67049f 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -11,7 +11,6 @@ use common::transaction::LeeTransaction; use integration_tests::{ TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, sync_private, verify_commitment_is_in_state, }; -use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use lee::{ AccountId, PrivacyPreservingTransaction, ProgramId, privacy_preserving_transaction::{ @@ -22,7 +21,7 @@ use lee::{ program::Program, }; use lee_core::{ - DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, + DUMMY_COMMITMENT_HASH, InputAccountIdentity, NullifierPublicKey, account::{Account, AccountWithMetadata}, encryption::ViewingPublicKey, program::PdaSeed, @@ -48,7 +47,8 @@ async fn fund_private_pda( amount: u128, auth_transfer: &ProgramWithDependencies, ) -> Result<()> { - let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier); + let pda_account_id = + AccountId::for_private_pda(&authority_program_id, &seed, &npk, &vpk, identifier); let sender_account = wallet .get_account_public(sender) .await @@ -60,20 +60,15 @@ async fn fund_private_pda( let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); - let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) .context("failed to serialize auth_transfer instruction")?; let account_identities = vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { - epk, - view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk), + vpk, + random_seed: [0; 32], npk, - ssk, identifier, commitment_root: DUMMY_COMMITMENT_HASH, seed: Some((seed, authority_program_id)), @@ -173,8 +168,8 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { let spend_program = ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); - let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); - let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1); + let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 0); + let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 1); // Use two different public senders to avoid nonce conflicts between the back-to-back txs. let senders = ctx.existing_public_accounts(); diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 0b9a1002..71fb132b 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -15,7 +15,6 @@ use anyhow::{Context as _, Result}; use bytesize::ByteSize; use common::transaction::LeeTransaction; use integration_tests::{TestContext, config::SequencerPartialConfig}; -use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use lee::{ Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction, privacy_preserving_transaction::{self as pptx, circuit}, @@ -23,8 +22,7 @@ use lee::{ public_transaction as putx, }; use lee_core::{ - DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, MembershipProof, - NullifierPublicKey, + DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -267,7 +265,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { data: Data::default(), }, true, - AccountId::for_regular_private_account(&sender_npk, 0), + AccountId::for_regular_private_account(&sender_npk, &sender_vpk, 0), ); let recipient_nsk = [2; 32]; let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]); @@ -275,17 +273,9 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { let recipient_pre = AccountWithMetadata::new( Account::default(), false, - AccountId::for_regular_private_account(&recipient_npk, 0), + AccountId::for_regular_private_account(&recipient_npk, &recipient_vpk, 0), ); - let eph_holder_from = EphemeralKeyHolder::new(&sender_vpk); - let sender_ss = eph_holder_from.calculate_shared_secret_sender(); - let sender_epk = eph_holder_from.ephemeral_public_key().clone(); - - let eph_holder_to = EphemeralKeyHolder::new(&recipient_vpk); - let recipient_ss = eph_holder_to.calculate_shared_secret_sender(); - let recipient_epk = eph_holder_to.ephemeral_public_key().clone(); - let balance_to_move: u128 = 1; let proof: MembershipProof = ( 1, @@ -302,18 +292,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: sender_epk, - view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk), - ssk: sender_ss, + vpk: sender_vpk, + random_seed: [0; 32], nsk: sender_nsk, membership_proof: proof, identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: recipient_epk, - view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk), + vpk: recipient_vpk, + random_seed: [0; 32], npk: recipient_npk, - ssk: recipient_ss, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }, diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index 57f1afa4..321c874d 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -1012,7 +1012,11 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> { let (to, to_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); - let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128); + let account_id = lee::AccountId::for_regular_private_account( + &out_keys.npk(), + &out_keys.vpk().unwrap(), + 0_u128, + ); let to: FfiBytes32 = account_id.into(); (to, out_keys) }; @@ -1155,7 +1159,11 @@ fn test_wallet_ffi_transfer_private() -> Result<()> { let (to, to_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); - let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128); + let account_id = lee::AccountId::for_regular_private_account( + &out_keys.npk(), + &out_keys.vpk().unwrap(), + 0_u128, + ); let to: FfiBytes32 = account_id.into(); (to, out_keys) }; @@ -1236,7 +1244,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> { let (private_account_id_1, private_account_1_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); - let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128); + let account_id = lee::AccountId::for_regular_private_account( + &out_keys.npk(), + &out_keys.vpk().unwrap(), + 0_u128, + ); let to: FfiBytes32 = account_id.into(); (to, out_keys) }; @@ -1244,7 +1256,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> { let (private_account_id_2, private_account_2_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); - let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128); + let account_id = lee::AccountId::for_regular_private_account( + &out_keys.npk(), + &out_keys.vpk().unwrap(), + 0_u128, + ); let to: FfiBytes32 = account_id.into(); (to, out_keys) }; diff --git a/lee/key_protocol/src/key_management/group_key_holder.rs b/lee/key_protocol/src/key_management/group_key_holder.rs index 1efc49cb..1aef6c91 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -351,10 +351,10 @@ mod tests { let program_id: ProgramId = [9; 8]; let holder = GroupKeyHolder::from_gms(gms); - let npk = holder - .derive_keys_for_pda(&TEST_PROGRAM_ID, &seed) - .generate_nullifier_public_key(); - let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX); + let keys = holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed); + let npk = keys.generate_nullifier_public_key(); + let vpk = keys.generate_viewing_public_key(); + let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX); let expected_npk = NullifierPublicKey([ 136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79, @@ -363,7 +363,7 @@ mod tests { // AccountId is derived from (program_id, seed, npk), so it changes when npk changes. // We verify npk is pinned, and AccountId is deterministically derived from it. let expected_account_id = - AccountId::for_private_pda(&program_id, &seed, &expected_npk, u128::MAX); + AccountId::for_private_pda(&program_id, &seed, &expected_npk, &vpk, u128::MAX); assert_eq!(npk, expected_npk); assert_eq!(account_id, expected_account_id); @@ -560,13 +560,16 @@ mod tests { let bob_holder = GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS"); - let bob_npk = bob_holder - .derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed) - .generate_nullifier_public_key(); + let bob_group_keys = bob_holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed); + let bob_npk = bob_group_keys.generate_nullifier_public_key(); assert_eq!(alice_npk, bob_npk); - let alice_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, 0); - let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, 0); + let alice_vpk = alice_keys.generate_viewing_public_key(); + let bob_group_vpk = bob_group_keys.generate_viewing_public_key(); + let alice_account_id = + AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, &alice_vpk, 0); + let bob_account_id = + AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, &bob_group_vpk, 0); assert_eq!(alice_account_id, bob_account_id); } diff --git a/lee/key_protocol/src/key_management/key_tree/keys_private.rs b/lee/key_protocol/src/key_management/key_tree/keys_private.rs index 48b47975..8165e808 100644 --- a/lee/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/lee/key_protocol/src/key_management/key_tree/keys_private.rs @@ -94,10 +94,11 @@ impl KeyTreeNode for ChildKeysPrivate { fn account_ids(&self) -> impl Iterator { let npk = self.value.0.nullifier_public_key; + let vpk = self.value.0.viewing_public_key.clone(); self.value .1 .keys() - .map(move |kind| lee::AccountId::for_private_account(&npk, kind)) + .map(move |kind| lee::AccountId::for_private_account(&npk, &vpk, kind)) } } diff --git a/lee/key_protocol/src/key_management/key_tree/mod.rs b/lee/key_protocol/src/key_management/key_tree/mod.rs index 7e4b432c..463c757a 100644 --- a/lee/key_protocol/src/key_management/key_tree/mod.rs +++ b/lee/key_protocol/src/key_management/key_tree/mod.rs @@ -268,6 +268,7 @@ impl KeyTree { let node = self.key_map.get(cci)?; let account_id = lee::AccountId::for_regular_private_account( &node.value.0.nullifier_public_key, + &node.value.0.viewing_public_key, identifier, ); if self.account_id_map.contains_key(&account_id) { diff --git a/lee/privacy_preserving_circuit/src/execution_state.rs b/lee/privacy_preserving_circuit/src/execution_state.rs index 4e81f9ce..09ad30af 100644 --- a/lee/privacy_preserving_circuit/src/execution_state.rs +++ b/lee/privacy_preserving_circuit/src/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, } @@ -59,9 +61,48 @@ impl ExecutionState { program_id: ProgramId, program_outputs: Vec, ) -> Self { - let private_pda_npk_by_position = build_private_pda_npk_map(account_identities); - let (block_validity_window, timestamp_validity_window) = - intersect_validity_windows(&program_outputs); + // Build position → (npk, identifier) map for private-PDA pre_states, indexed by position + // in `account_identities`. The vec is documented as 1:1 with the program's pre_state + // order, so position here matches `pre_state_position` used downstream in + // `validate_and_sync_states`. + let mut private_pda_by_position: HashMap< + usize, + (NullifierPublicKey, ViewingPublicKey, Identifier), + > = HashMap::new(); + for (pos, account_identity) in account_identities.iter().enumerate() { + if let Some((npk, vpk, identifier)) = account_identity.npk_vpk_if_private_pda() { + private_pda_by_position.insert(pos, (npk, vpk, identifier)); + } + } + + let block_valid_from = program_outputs + .iter() + .filter_map(|output| output.block_validity_window.start()) + .max(); + let block_valid_until = program_outputs + .iter() + .filter_map(|output| output.block_validity_window.end()) + .min(); + let ts_valid_from = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.start()) + .max(); + let ts_valid_until = program_outputs + .iter() + .filter_map(|output| output.timestamp_validity_window.end()) + .min(); + + let block_validity_window: BlockValidityWindow = (block_valid_from, block_valid_until) + .try_into() + .expect( + "There should be non empty intersection in the program output block validity windows", + ); + let timestamp_validity_window: TimestampValidityWindow = + (ts_valid_from, ts_valid_until) + .try_into() + .expect( + "There should be non empty intersection in the program output timestamp validity windows", + ); let mut execution_state = Self { pre_states: Vec::new(), @@ -70,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(), }; @@ -99,7 +140,49 @@ impl ExecutionState { panic!("Insufficient program outputs for chained calls"); }; - verify_program_output(&chained_call, caller_program_id, &program_output); + // Check that instruction data in chained call is the instruction data in program output + assert_eq!( + chained_call.instruction_data, program_output.instruction_data, + "Mismatched instruction data between chained call and program output" + ); + + // Check that `program_output` is consistent with the execution of the corresponding + // program. + let program_output_words = + &to_vec(&program_output).expect("program_output must be serializable"); + env::verify(chained_call.program_id, program_output_words).unwrap_or_else( + |_: Infallible| unreachable!("Infallible error is never constructed"), + ); + + // Verify that the program output's self_program_id matches the expected program ID. + // This ensures the proof commits to which program produced the output. + assert_eq!( + program_output.self_program_id, chained_call.program_id, + "Program output self_program_id does not match chained call program_id" + ); + + // Verify that the program output's caller_program_id matches the actual caller. + // This prevents a malicious user from privately executing an internal function + // by spoofing caller_program_id (e.g. passing caller_program_id = self_program_id + // to bypass access control checks). + assert_eq!( + program_output.caller_program_id, caller_program_id, + "Program output caller_program_id does not match actual caller" + ); + + // Check that the program is well behaved. + // See the # Programs section for the definition of the `validate_execution` method. + let validated_execution = validate_execution( + &program_output.pre_states, + &program_output.post_states, + chained_call.program_id, + ); + if let Err(err) = validated_execution { + panic!( + "Invalid program behavior in program {:?}: {err}", + chained_call.program_id + ); + } for next_call in program_output.chained_calls.iter().rev() { chained_calls.push_front((next_call.clone(), Some(chained_call.program_id))); @@ -123,8 +206,41 @@ impl ExecutionState { "Inner call without a chained call found", ); - execution_state.assert_all_pda_positions_bound(account_identities); - execution_state.assert_modified_accounts_claimed(); + // Every private-PDA pre_state must have had its npk bound to its account_id, either via + // a `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` + // matching the private derivation. An unbound private-PDA pre_state has no + // cryptographic link between the supplied npk and the account_id, and must be rejected. + for (pos, account_identity) in account_identities.iter().enumerate() { + if account_identity.is_private_pda() { + assert!( + execution_state + .private_pda_bound_positions + .contains_key(&pos), + "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" + ); + } + } + + // Check that all modified uninitialized accounts were claimed + for (account_id, post) in execution_state + .pre_states + .iter() + .filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID) + .map(|a| { + let post = execution_state + .post_states + .get(&a.account_id) + .expect("Post state must exist for pre state"); + (a, post) + }) + .filter(|(pre_default, post)| pre_default.account != **post) + .map(|(pre, post)| (pre.account_id, post)) + { + assert_ne!( + post.program_owner, DEFAULT_PROGRAM_ID, + "Account {account_id} was modified but not claimed" + ); + } execution_state } @@ -142,185 +258,209 @@ impl ExecutionState { for (pre, mut post) in output_pre_states.into_iter().zip(output_post_states) { let pre_account_id = pre.account_id; let pre_is_authorized = pre.is_authorized; + let post_states_entry = self.post_states.entry(pre.account_id); + match &post_states_entry { + Entry::Occupied(occupied) => { + #[expect( + clippy::shadow_unrelated, + reason = "Shadowing is intentional to use all fields" + )] + let AccountWithMetadata { + account: pre_account, + account_id: pre_account_id, + is_authorized: pre_is_authorized, + } = pre; - if let Some(existing) = self.post_states.get(&pre.account_id) { - #[expect( - clippy::shadow_unrelated, - reason = "Shadowing is intentional to use all fields" - )] - let AccountWithMetadata { - account: pre_account, - account_id: pre_account_id, - is_authorized: pre_is_authorized, - } = pre; - - assert_eq!( - existing, &pre_account, - "Inconsistent pre state for account {pre_account_id}", - ); - - let (previous_is_authorized, pre_state_position) = self - .pre_states - .iter() - .enumerate() - .find(|(_, acc)| acc.account_id == pre_account_id) - .map_or_else( - || panic!( - "Pre state must exist in execution state for account {pre_account_id}", - ), - |(pos, acc)| (acc.is_authorized, pos), + // Ensure that new pre state is the same as known post state + assert_eq!( + occupied.get(), + &pre_account, + "Inconsistent pre state for account {pre_account_id}", ); - let is_authorized = resolve_authorization_and_record_bindings( - &mut self.pda_family_binding, - &mut self.private_pda_bound_positions, - &self.private_pda_npk_by_position, - &mut self.authorized_accounts, - pre_account_id, - pre_state_position, - caller_program_id, - caller_pda_seeds, - previous_is_authorized, - ); + let (previous_is_authorized, pre_state_position) = self + .pre_states + .iter() + .enumerate() + .find(|(_, acc)| acc.account_id == pre_account_id) + .map_or_else( + || panic!( + "Pre state must exist in execution state for account {pre_account_id}", + ), + |(pos, acc)| (acc.is_authorized, pos) + ); - assert_eq!( - pre_is_authorized, is_authorized, - "Inconsistent authorization for account {pre_account_id}", - ); - } else { - let pre_state_position = self.pre_states.len(); - resolve_external_seed( - account_identities, - pre_state_position, - pre_account_id, - pre.is_authorized, - &mut self.private_pda_bound_positions, - &mut self.pda_family_binding, - ); - self.pre_states.push(pre); + let is_authorized = resolve_authorization_and_record_bindings( + &mut self.pda_family_binding, + &mut self.private_pda_bound_positions, + &self.private_pda_by_position, + &mut self.authorized_accounts, + pre_account_id, + pre_state_position, + caller_program_id, + caller_pda_seeds, + previous_is_authorized, + ); + + assert_eq!( + pre_is_authorized, is_authorized, + "Inconsistent authorization for account {pre_account_id}", + ); + } + Entry::Vacant(_) => { + // Pre state for the initial call + let pre_state_position = self.pre_states.len(); + let external_seed = match account_identities.get(pre_state_position) { + Some(InputAccountIdentity::PrivatePdaInit { + npk, + vpk, + identifier, + seed: Some((seed, authority_program_id)), + .. + }) => { + let expected = AccountId::for_private_pda( + authority_program_id, + seed, + npk, + vpk, + *identifier, + ); + assert_eq!( + pre_account_id, expected, + "External seed mismatch for PrivatePdaInit at position {pre_state_position}" + ); + Some((*seed, *authority_program_id)) + } + Some(InputAccountIdentity::PrivatePdaUpdate { + nsk, + 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)) + } + _ => None, + }; + // External seed is only consulted the first time the account is seen. + // Subsequent calls need no re-check because the entry is already recorded on + // private_pda_bound_positions. + if let Some((seed, authority_program_id)) = external_seed { + assert!( + !pre.is_authorized, + "Private PDA with externally-provided seed must not be authorized at position {pre_state_position}" + ); + bind_private_pda_position( + &mut self.private_pda_bound_positions, + pre_state_position, + authority_program_id, + seed, + ); + assert_family_binding( + &mut self.pda_family_binding, + authority_program_id, + seed, + pre_account_id, + ); + } + self.pre_states.push(pre); + } } if let Some(claim) = post.required_claim() { - self.process_claim( - account_identities, - &mut post, - pre_account_id, - pre_is_authorized, - program_id, - claim, + // The invoked program can only claim accounts with default program id. + assert_eq!( + post.account().program_owner, + DEFAULT_PROGRAM_ID, + "Cannot claim an initialized account {pre_account_id}" ); - } - self.post_states.insert(pre_account_id, post.into_account()); - } - } + let pre_state_position = self + .pre_states + .iter() + .position(|acc| acc.account_id == pre_account_id) + .expect("Pre state must exist at this point"); - fn process_claim( - &mut self, - account_identities: &[InputAccountIdentity], - post: &mut AccountPostState, - pre_account_id: AccountId, - pre_is_authorized: bool, - program_id: ProgramId, - claim: Claim, - ) { - assert_eq!( - post.account().program_owner, - DEFAULT_PROGRAM_ID, - "Cannot claim an initialized account {pre_account_id}" - ); - - let pre_state_position = self - .pre_states - .iter() - .position(|acc| acc.account_id == pre_account_id) - .expect("Pre state must exist at this point"); - - let account_identity = &account_identities[pre_state_position]; - if account_identity.is_public() { - match claim { - Claim::Authorized => { - assert!( - pre_is_authorized, - "Cannot claim unauthorized account {pre_account_id}" - ); + let account_identity = &account_identities[pre_state_position]; + if account_identity.is_public() { + match claim { + Claim::Authorized => { + // Note: no need to check authorized pdas because we have already + // checked consistency of authorization above. + assert!( + pre_is_authorized, + "Cannot claim unauthorized account {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let pda = AccountId::for_public_pda(&program_id, &seed); + assert_eq!( + pre_account_id, pda, + "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" + ); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); + } + } + } else { + // Private accounts: don't enforce the claim semantics. Unauthorized private + // claiming is intentionally allowed + match claim { + Claim::Authorized => {} + Claim::Pda(seed) => { + let (npk, 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, + vpk, + *identifier, + ); + assert_eq!( + pre_account_id, pda, + "Invalid private PDA claim for account {pre_account_id}" + ); + bind_private_pda_position( + &mut self.private_pda_bound_positions, + pre_state_position, + program_id, + seed, + ); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); + } + } } - Claim::Pda(seed) => { - let pda = AccountId::for_public_pda(&program_id, &seed); - assert_eq!( - pre_account_id, pda, - "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" - ); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } - } - } else { - match claim { - Claim::Authorized => {} - Claim::Pda(seed) => { - let (npk, identifier) = self - .private_pda_npk_by_position - .get(&pre_state_position) - .expect("private PDA pre_state must have an npk in the position map"); - let pda = AccountId::for_private_pda(&program_id, &seed, npk, *identifier); - assert_eq!( - pre_account_id, pda, - "Invalid private PDA claim for account {pre_account_id}" - ); - bind_private_pda_position( - &mut self.private_pda_bound_positions, - pre_state_position, - program_id, - seed, - ); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } - } - } - post.account_mut().program_owner = program_id; - } - - fn assert_all_pda_positions_bound(&self, account_identities: &[InputAccountIdentity]) { - for (pos, account_identity) in account_identities.iter().enumerate() { - if account_identity.is_private_pda() { - assert!( - self.private_pda_bound_positions.contains_key(&pos), - "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" - ); + post.account_mut().program_owner = program_id; } - } - } - fn assert_modified_accounts_claimed(&self) { - for (account_id, post) in self - .pre_states - .iter() - .filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID) - .map(|a| { - let post = self - .post_states - .get(&a.account_id) - .expect("Post state must exist for pre state"); - (a, post) - }) - .filter(|(pre_default, post)| pre_default.account != **post) - .map(|(pre, post)| (pre.account_id, post)) - { - assert_ne!( - post.program_owner, DEFAULT_PROGRAM_ID, - "Account {account_id} was modified but not claimed" - ); + post_states_entry.insert_entry(post.into_account()); } } @@ -359,150 +499,6 @@ impl ExecutionState { } } -fn verify_program_output( - chained_call: &ChainedCall, - caller_program_id: Option, - program_output: &ProgramOutput, -) { - assert_eq!( - chained_call.instruction_data, program_output.instruction_data, - "Mismatched instruction data between chained call and program output" - ); - - let program_output_words = - &to_vec(program_output).expect("program_output must be serializable"); - env::verify(chained_call.program_id, program_output_words) - .unwrap_or_else(|_: Infallible| unreachable!("Infallible error is never constructed")); - - assert_eq!( - program_output.self_program_id, chained_call.program_id, - "Program output self_program_id does not match chained call program_id" - ); - - assert_eq!( - program_output.caller_program_id, caller_program_id, - "Program output caller_program_id does not match actual caller" - ); - - if let Err(err) = validate_execution( - &program_output.pre_states, - &program_output.post_states, - chained_call.program_id, - ) { - panic!( - "Invalid program behavior in program {:?}: {err}", - chained_call.program_id - ); - } -} - -fn build_private_pda_npk_map( - account_identities: &[InputAccountIdentity], -) -> HashMap { - account_identities - .iter() - .enumerate() - .filter_map(|(pos, identity)| { - identity - .npk_if_private_pda() - .map(|(npk, identifier)| (pos, (npk, identifier))) - }) - .collect() -} - -fn intersect_validity_windows( - program_outputs: &[ProgramOutput], -) -> (BlockValidityWindow, TimestampValidityWindow) { - let block_valid_from = program_outputs - .iter() - .filter_map(|output| output.block_validity_window.start()) - .max(); - let block_valid_until = program_outputs - .iter() - .filter_map(|output| output.block_validity_window.end()) - .min(); - let ts_valid_from = program_outputs - .iter() - .filter_map(|output| output.timestamp_validity_window.start()) - .max(); - let ts_valid_until = program_outputs - .iter() - .filter_map(|output| output.timestamp_validity_window.end()) - .min(); - - let block_validity_window: BlockValidityWindow = - (block_valid_from, block_valid_until).try_into().expect( - "There should be non empty intersection in the program output block validity windows", - ); - let timestamp_validity_window: TimestampValidityWindow = (ts_valid_from, ts_valid_until) - .try_into() - .expect( - "There should be non empty intersection in the program output timestamp validity windows", - ); - - (block_validity_window, timestamp_validity_window) -} - -fn resolve_external_seed( - account_identities: &[InputAccountIdentity], - pre_state_position: usize, - pre_account_id: AccountId, - is_authorized: bool, - private_pda_bound_positions: &mut HashMap, - pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>, -) { - let external_seed = match account_identities.get(pre_state_position) { - Some(InputAccountIdentity::PrivatePdaInit { - npk, - identifier, - seed: Some((seed, authority_program_id)), - .. - }) => { - let expected = AccountId::for_private_pda(authority_program_id, seed, npk, *identifier); - assert_eq!( - pre_account_id, expected, - "External seed mismatch for PrivatePdaInit at position {pre_state_position}" - ); - Some((*seed, *authority_program_id)) - } - Some(InputAccountIdentity::PrivatePdaUpdate { - nsk, - identifier, - seed: Some((seed, authority_program_id)), - .. - }) => { - let npk = NullifierPublicKey::from(nsk); - let expected = - AccountId::for_private_pda(authority_program_id, seed, &npk, *identifier); - assert_eq!( - pre_account_id, expected, - "External seed mismatch for PrivatePdaUpdate at position {pre_state_position}" - ); - Some((*seed, *authority_program_id)) - } - _ => None, - }; - - if let Some((seed, authority_program_id)) = external_seed { - assert!( - !is_authorized, - "Private PDA with externally-provided seed must not be authorized at position {pre_state_position}" - ); - bind_private_pda_position( - private_pda_bound_positions, - pre_state_position, - authority_program_id, - seed, - ); - assert_family_binding( - pda_family_binding, - authority_program_id, - seed, - pre_account_id, - ); - } -} - /// Record or re-verify the `(program_id, seed) → account_id` family binding for the /// transaction. Any claim or caller-seed authorization that resolves a `pre_state` under /// `(program_id, seed)` must agree with every prior resolution of the same pair; otherwise a @@ -565,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, @@ -579,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/lee/privacy_preserving_circuit/src/output.rs b/lee/privacy_preserving_circuit/src/output.rs index 74c1cccc..c9543e05 100644 --- a/lee/privacy_preserving_circuit/src/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -1,244 +1,14 @@ use lee_core::{ - Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, + Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, compute_digest_for_path, + encryption::ViewingPublicKey, }; use crate::execution_state::ExecutionState; -struct PrivateOutputHandler<'ctx> { - output: &'ctx mut PrivacyPreservingCircuitOutput, - output_index: &'ctx mut u32, - pre_state: &'ctx lee_core::account::AccountWithMetadata, - post_state: Account, - epk: &'ctx EphemeralPublicKey, - view_tag: u8, - ssk: &'ctx SharedSecretKey, - identifier: u128, -} - -impl PrivateOutputHandler<'_> { - fn authorized_init(self, nsk: &NullifierSecretKey, commitment_root: &CommitmentSetDigest) { - let npk = NullifierPublicKey::from(nsk); - let account_id = - derive_and_verify_account_id(&npk, self.identifier, self.pre_state.account_id); - - assert!( - self.pre_state.is_authorized, - "Pre-state not authorized for authenticated private account" - ); - assert_eq!( - self.pre_state.account, - Account::default(), - "Found new private account with non default values" - ); - - let (new_nullifier, new_nonce) = init_nullifier_and_nonce(&account_id, commitment_root); - let kind = PrivateAccountKind::Regular(self.identifier); - - self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); - } - - fn authorized_update(self, nsk: &NullifierSecretKey, membership_proof: &MembershipProof) { - let npk = NullifierPublicKey::from(nsk); - let account_id = - derive_and_verify_account_id(&npk, self.identifier, self.pre_state.account_id); - - assert!( - self.pre_state.is_authorized, - "Pre-state not authorized for authenticated private account" - ); - - let new_nullifier = compute_update_nullifier_and_set_digest( - membership_proof, - &self.pre_state.account, - &account_id, - nsk, - ); - let new_nonce = self - .pre_state - .account - .nonce - .private_account_nonce_increment(nsk); - let kind = PrivateAccountKind::Regular(self.identifier); - - self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); - } - - fn unauthorized(self, npk: &NullifierPublicKey, commitment_root: &CommitmentSetDigest) { - let account_id = - derive_and_verify_account_id(npk, self.identifier, self.pre_state.account_id); - - assert_eq!( - self.pre_state.account, - Account::default(), - "Found new private account with non default values", - ); - assert!( - !self.pre_state.is_authorized, - "Found new private account marked as authorized." - ); - - let (new_nullifier, new_nonce) = init_nullifier_and_nonce(&account_id, commitment_root); - let kind = PrivateAccountKind::Regular(self.identifier); - - self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); - } - - fn pda_init( - self, - commitment_root: &CommitmentSetDigest, - pos: usize, - pda_seed_by_position: &std::collections::HashMap< - usize, - (lee_core::program::ProgramId, lee_core::program::PdaSeed), - >, - ) { - // The npk-to-account_id binding is established upstream in - // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` - // match. Here we only enforce the init pre-conditions. - assert!( - !self.pre_state.is_authorized, - "PrivatePdaInit requires unauthorized pre_state" - ); - assert_eq!( - self.pre_state.account, - Account::default(), - "New private PDA must be default" - ); - - let (new_nullifier, new_nonce) = - init_nullifier_and_nonce(&self.pre_state.account_id, commitment_root); - - let account_id = self.pre_state.account_id; - let (authority_program_id, seed) = pda_seed_by_position - .get(&pos) - .expect("PrivatePdaInit position must be in pda_seed_by_position"); - let kind = PrivateAccountKind::Pda { - program_id: *authority_program_id, - seed: *seed, - identifier: self.identifier, - }; - - self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); - } - - fn pda_update( - self, - nsk: &NullifierSecretKey, - membership_proof: &MembershipProof, - external_seed: Option<&(lee_core::program::PdaSeed, lee_core::program::ProgramId)>, - pos: usize, - pda_seed_by_position: &std::collections::HashMap< - usize, - (lee_core::program::ProgramId, lee_core::program::PdaSeed), - >, - ) { - // With an external seed the binding comes from the circuit input and the - // pre_state is intentionally unauthorized; without one the binding comes from - // a Claim or caller pda_seeds, so the pre_state must already be authorized. - assert!( - self.pre_state.is_authorized ^ external_seed.is_some(), - "PrivatePdaUpdate requires authorized pre_state or external seed" - ); - - let new_nullifier = compute_update_nullifier_and_set_digest( - membership_proof, - &self.pre_state.account, - &self.pre_state.account_id, - nsk, - ); - let new_nonce = self - .pre_state - .account - .nonce - .private_account_nonce_increment(nsk); - - let account_id = self.pre_state.account_id; - let (authority_program_id, seed) = pda_seed_by_position - .get(&pos) - .expect("PrivatePdaUpdate position must be in pda_seed_by_position"); - let kind = PrivateAccountKind::Pda { - program_id: *authority_program_id, - seed: *seed, - identifier: self.identifier, - }; - - self.emit_private_output(&account_id, &kind, new_nullifier, new_nonce); - } - - fn emit_private_output( - self, - account_id: &AccountId, - kind: &PrivateAccountKind, - new_nullifier: (Nullifier, CommitmentSetDigest), - new_nonce: Nonce, - ) { - self.output.new_nullifiers.push(new_nullifier); - - let mut post_with_updated_nonce = self.post_state; - post_with_updated_nonce.nonce = new_nonce; - - let commitment_post = Commitment::new(account_id, &post_with_updated_nonce); - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - kind, - self.ssk, - &commitment_post, - *self.output_index, - ); - - self.output.new_commitments.push(commitment_post); - self.output - .encrypted_private_post_states - .push(EncryptedAccountData { - ciphertext: encrypted_account, - epk: self.epk.clone(), - view_tag: self.view_tag, - }); - *self.output_index = self - .output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); - } -} - -fn init_nullifier_and_nonce( - account_id: &AccountId, - commitment_root: &CommitmentSetDigest, -) -> ((Nullifier, CommitmentSetDigest), Nonce) { - let nullifier = ( - Nullifier::for_account_initialization(account_id), - *commitment_root, - ); - let nonce = Nonce::private_account_nonce_init(account_id); - (nullifier, nonce) -} - -fn derive_and_verify_account_id( - npk: &NullifierPublicKey, - identifier: u128, - pre_state_account_id: AccountId, -) -> AccountId { - let account_id = AccountId::for_regular_private_account(npk, identifier); - assert_eq!(account_id, pre_state_account_id, "AccountId mismatch"); - account_id -} - -fn compute_update_nullifier_and_set_digest( - membership_proof: &MembershipProof, - pre_account: &Account, - account_id: &AccountId, - nsk: &NullifierSecretKey, -) -> (Nullifier, CommitmentSetDigest) { - let commitment_pre = Commitment::new(account_id, pre_account); - let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); - let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); - (nullifier, set_digest) -} - pub fn compute_circuit_output( execution_state: ExecutionState, account_identities: &[InputAccountIdentity], @@ -271,105 +41,290 @@ pub fn compute_circuit_output( output.public_post_states.push(post_state); } InputAccountIdentity::PrivateAuthorizedInit { - epk, - view_tag, - ssk, + vpk, + random_seed, nsk, identifier, commitment_root, - } => PrivateOutputHandler { - output: &mut output, - output_index: &mut output_index, - pre_state: &pre_state, - post_state, - epk, - view_tag: *view_tag, - ssk, - identifier: *identifier, + } => { + 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" + ); + assert_eq!( + pre_state.account, + Account::default(), + "Found new private account with non default values" + ); + + let new_nullifier = ( + Nullifier::for_account_initialization(&account_id), + *commitment_root, + ); + let new_nonce = Nonce::private_account_nonce_init(&account_id); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Regular(*identifier), + &npk, + vpk, + random_seed, + new_nullifier, + new_nonce, + ); } - .authorized_init(nsk, commitment_root), InputAccountIdentity::PrivateAuthorizedUpdate { - epk, - view_tag, - ssk, + vpk, + random_seed, nsk, membership_proof, identifier, - } => PrivateOutputHandler { - output: &mut output, - output_index: &mut output_index, - pre_state: &pre_state, - post_state, - epk, - view_tag: *view_tag, - ssk, - identifier: *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, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Regular(*identifier), + &npk, + vpk, + random_seed, + new_nullifier, + new_nonce, + ); } - .authorized_update(nsk, membership_proof), InputAccountIdentity::PrivateUnauthorized { - epk, - view_tag, + vpk, + random_seed, npk, - ssk, identifier, commitment_root, - } => PrivateOutputHandler { - output: &mut output, - output_index: &mut output_index, - pre_state: &pre_state, - post_state, - epk, - view_tag: *view_tag, - ssk, - identifier: *identifier, + } => { + 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 authorized." + ); + + let new_nullifier = ( + Nullifier::for_account_initialization(&account_id), + *commitment_root, + ); + let new_nonce = Nonce::private_account_nonce_init(&account_id); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Regular(*identifier), + npk, + vpk, + random_seed, + new_nullifier, + new_nonce, + ); } - .unauthorized(npk, commitment_root), InputAccountIdentity::PrivatePdaInit { - epk, - view_tag, - npk: _, - ssk, + vpk, + random_seed, + npk, identifier, commitment_root, seed: _, - } => PrivateOutputHandler { - output: &mut output, - output_index: &mut output_index, - pre_state: &pre_state, - post_state, - epk, - view_tag: *view_tag, - ssk, - identifier: *identifier, + } => { + // 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"); + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Pda { + program_id: *authority_program_id, + seed: *seed, + identifier: *identifier, + }, + npk, + vpk, + random_seed, + new_nullifier, + new_nonce, + ); } - .pda_init(commitment_root, pos, &pda_seed_by_position), InputAccountIdentity::PrivatePdaUpdate { - epk, - view_tag, - ssk, + vpk, + random_seed, nsk, membership_proof, identifier, seed: external_seed, - } => PrivateOutputHandler { - output: &mut output, - output_index: &mut output_index, - pre_state: &pre_state, - post_state, - epk, - view_tag: *view_tag, - ssk, - identifier: *identifier, + } => { + // 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 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"); + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Pda { + program_id: *authority_program_id, + seed: *seed, + identifier: *identifier, + }, + &npk, + vpk, + random_seed, + new_nullifier, + new_nonce, + ); } - .pda_update( - nsk, - membership_proof, - external_seed.as_ref(), - pos, - &pda_seed_by_position, - ), } } output } + +#[expect( + clippy::too_many_arguments, + reason = "Inputs are distinct concerns from the variant arms; bundling would be artificial" +)] +fn emit_private_output( + output: &mut PrivacyPreservingCircuitOutput, + output_index: &mut u32, + post_state: Account, + account_id: &AccountId, + kind: &PrivateAccountKind, + npk: &NullifierPublicKey, + vpk: &ViewingPublicKey, + random_seed: &[u8; 32], + new_nullifier: (Nullifier, CommitmentSetDigest), + new_nonce: Nonce, +) { + output.new_nullifiers.push(new_nullifier); + + let mut post_with_updated_nonce = post_state; + post_with_updated_nonce.nonce = new_nonce; + + let commitment_post = Commitment::new(account_id, &post_with_updated_nonce); + + let esk = EphemeralSecretKey::new(account_id, random_seed, &new_nonce); + let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(vpk, &esk); + + // Currently the view tag is properlty generated for all accounts. + // To increase privacy, this will be changed in the later version + // to only be generated explicitly for initialized accounts and + // fed by the prover directly for updated accounts. + // + // See issue 573: + // https://github.com/logos-blockchain/logos-execution-zone/issues/573 + let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk); + + let encrypted_account = EncryptionScheme::encrypt( + &post_with_updated_nonce, + kind, + &shared_secret, + &commitment_post, + *output_index, + ); + + output.new_commitments.push(commitment_post); + output + .encrypted_private_post_states + .push(EncryptedAccountData { + ciphertext: encrypted_account, + epk, + view_tag, + }); + *output_index = output_index + .checked_add(1) + .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); +} + +fn compute_update_nullifier_and_set_digest( + membership_proof: &MembershipProof, + pre_account: &Account, + account_id: &AccountId, + nsk: &NullifierSecretKey, +) -> (Nullifier, CommitmentSetDigest) { + let commitment_pre = Commitment::new(account_id, pre_account); + let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + (nullifier, set_digest) +} diff --git a/lee/state_machine/Cargo.toml b/lee/state_machine/Cargo.toml index d4014465..df43da8d 100644 --- a/lee/state_machine/Cargo.toml +++ b/lee/state_machine/Cargo.toml @@ -28,10 +28,8 @@ build_utils.workspace = true [dev-dependencies] lee_core = { workspace = true, features = ["test_utils"] } -token_core.workspace = true test_methods = { path = "test_methods" } -env_logger.workspace = true hex-literal = "1.0.0" test-case = "3.3.1" diff --git a/lee/state_machine/core/Cargo.toml b/lee/state_machine/core/Cargo.toml index 6e1f0ff0..585637ae 100644 --- a/lee/state_machine/core/Cargo.toml +++ b/lee/state_machine/core/Cargo.toml @@ -16,7 +16,7 @@ thiserror.workspace = true bytemuck.workspace = true bytesize.workspace = true base58.workspace = true -ml-kem = { workspace = true, optional = true, features = ["getrandom"] } +ml-kem = { workspace = true } chacha20 = { version = "0.10" } [dev-dependencies] @@ -24,5 +24,5 @@ serde_json.workspace = true [features] default = [] -host = ["dep:ml-kem"] +host = ["ml-kem/getrandom"] test_utils = ["host"] diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 88b214d4..d441ff19 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, 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,12 +28,11 @@ 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, + random_seed: [u8; 32], nsk: NullifierSecretKey, identifier: Identifier, commitment_root: CommitmentSetDigest, @@ -43,9 +40,8 @@ pub enum InputAccountIdentity { /// Update of an authorized standalone private account: existing on-chain commitment, with /// membership proof. PrivateAuthorizedUpdate { - epk: EphemeralPublicKey, - view_tag: ViewTag, - ssk: SharedSecretKey, + vpk: ViewingPublicKey, + random_seed: [u8; 32], nsk: NullifierSecretKey, membership_proof: MembershipProof, identifier: Identifier, @@ -53,10 +49,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, + random_seed: [u8; 32], npk: NullifierPublicKey, - ssk: SharedSecretKey, identifier: Identifier, commitment_root: CommitmentSetDigest, }, @@ -65,15 +60,14 @@ 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, + random_seed: [u8; 32], npk: NullifierPublicKey, - ssk: SharedSecretKey, identifier: Identifier, commitment_root: CommitmentSetDigest, /// When `Some((seed, authority_program_id))`, the circuit binds this position via the /// external derivation check - /// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) == + /// `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`. @@ -83,15 +77,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, + random_seed: [u8; 32], 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)>, @@ -112,17 +105,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 { .. } @@ -161,7 +160,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/encryption/mod.rs b/lee/state_machine/core/src/encryption/mod.rs index 47e0995d..f503279d 100644 --- a/lee/state_machine/core/src/encryption/mod.rs +++ b/lee/state_machine/core/src/encryption/mod.rs @@ -5,11 +5,9 @@ use chacha20::{ }; use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; -#[cfg(feature = "host")] pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey}; use crate::{Commitment, account::Account, program::PrivateAccountKind}; -#[cfg(feature = "host")] pub mod shared_key_derivation; /// Length in bytes of an ML-KEM-768 ciphertext (the `EphemeralPublicKey` payload). @@ -17,6 +15,31 @@ pub const ML_KEM_768_CIPHERTEXT_LEN: usize = 1088; pub type Scalar = [u8; 32]; +#[derive(Serialize, Deserialize, Clone, Copy)] +pub struct EphemeralSecretKey(pub [u8; 32]); + +impl EphemeralSecretKey { + /// Derives an ephemeral secret key from OS randomness and account-specific values. + /// + /// For updates, `nonce` carries `nsk`-derived entropy, making `esk` strong even + /// with a compromised RNG. For inits, `nonce` is deterministic, so `random_seed` + /// is the sole entropy source. + #[must_use] + pub fn new( + account_id: &crate::account::AccountId, + random_seed: &[u8; 32], + nonce: &crate::account::Nonce, + ) -> Self { + const PREFIX: &[u8; 14] = b"/LEE/v0.3/esk/"; + let mut input = [0_u8; 14 + 32 + 32 + 16]; + input[0..14].copy_from_slice(PREFIX); + input[14..46].copy_from_slice(account_id.value()); + input[46..78].copy_from_slice(random_seed); + input[78..94].copy_from_slice(&nonce.0.to_le_bytes()); + Self(Impl::hash_bytes(&input).as_bytes().try_into().unwrap()) + } +} + #[derive(Serialize, Deserialize, Clone, Copy)] pub struct SharedSecretKey(pub [u8; 32]); @@ -55,6 +78,18 @@ pub struct EncryptedAccountData { pub view_tag: ViewTag, } +impl EncryptedAccountData { + #[must_use] + pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag { + const PREFIX: &[u8; 18] = b"/LEE/v0.3/ViewTag/"; + let mut bytes = [0_u8; 18 + 32 + ViewingPublicKey::LEN]; + bytes[0..18].copy_from_slice(PREFIX); + bytes[18..50].copy_from_slice(&npk.to_byte_array()); + bytes[50..].copy_from_slice(vpk.to_bytes()); + Impl::hash_bytes(&bytes).as_bytes()[0] + } +} + #[cfg(feature = "host")] impl EncryptedAccountData { #[must_use] @@ -71,16 +106,6 @@ impl EncryptedAccountData { view_tag, } } - - /// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || npk || vpk). - #[must_use] - pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag { - let mut bytes = Vec::new(); - bytes.extend_from_slice(b"/LEE/v0.3/ViewTag/"); - bytes.extend_from_slice(&npk.to_byte_array()); - bytes.extend_from_slice(vpk.to_bytes()); - Impl::hash_bytes(&bytes).as_bytes()[0] - } } impl EncryptionScheme { @@ -116,12 +141,12 @@ impl EncryptionScheme { commitment: &Commitment, output_index: u32, ) -> [u8; 32] { - let mut bytes = Vec::new(); - - bytes.extend_from_slice(b"LEE/v0.2/KDF-SHA256/"); - bytes.extend_from_slice(&shared_secret.0); - bytes.extend_from_slice(&commitment.to_byte_array()); - bytes.extend_from_slice(&output_index.to_le_bytes()); + const PREFIX: &[u8; 20] = b"LEE/v0.2/KDF-SHA256/"; + let mut bytes = [0_u8; 20 + 32 + 32 + 4]; + bytes[0..20].copy_from_slice(PREFIX); + bytes[20..52].copy_from_slice(&shared_secret.0); + bytes[52..84].copy_from_slice(&commitment.to_byte_array()); + bytes[84..88].copy_from_slice(&output_index.to_le_bytes()); Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap() } @@ -238,4 +263,41 @@ mod tests { "wrong shared secret must not produce the correct plaintext" ); } + + #[test] + fn esk_is_deterministic() { + let account_id = AccountId::new([1_u8; 32]); + let random_seed = [2_u8; 32]; + let nonce = crate::account::Nonce(42); + let esk1 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce); + let esk2 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce); + assert_eq!(esk1.0, esk2.0); + } + + #[test] + fn esk_differs_for_different_account_id() { + let random_seed = [2_u8; 32]; + let nonce = crate::account::Nonce(42); + let esk_a = EphemeralSecretKey::new(&AccountId::new([0_u8; 32]), &random_seed, &nonce); + let esk_b = EphemeralSecretKey::new(&AccountId::new([1_u8; 32]), &random_seed, &nonce); + assert_ne!(esk_a.0, esk_b.0); + } + + #[test] + fn esk_differs_for_different_random_seed() { + let account_id = AccountId::new([1_u8; 32]); + let nonce = crate::account::Nonce(42); + let esk_a = EphemeralSecretKey::new(&account_id, &[0_u8; 32], &nonce); + let esk_b = EphemeralSecretKey::new(&account_id, &[1_u8; 32], &nonce); + assert_ne!(esk_a.0, esk_b.0); + } + + #[test] + fn esk_differs_for_different_nonce() { + let account_id = AccountId::new([1_u8; 32]); + let random_seed = [2_u8; 32]; + let esk_a = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(0)); + let esk_b = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(1)); + assert_ne!(esk_a.0, esk_b.0); + } } diff --git a/lee/state_machine/core/src/encryption/shared_key_derivation.rs b/lee/state_machine/core/src/encryption/shared_key_derivation.rs index 4b46165a..3255080b 100644 --- a/lee/state_machine/core/src/encryption/shared_key_derivation.rs +++ b/lee/state_machine/core/src/encryption/shared_key_derivation.rs @@ -1,5 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed}; +#[cfg(feature = "host")] +use ml_kem::Encapsulate as _; +use ml_kem::{Decapsulate as _, KeyExport as _, Seed}; use serde::{Deserialize, Serialize}; use crate::{EphemeralPublicKey, SharedSecretKey}; @@ -26,6 +28,7 @@ impl MlKem768EncapsulationKey { pub const LEN: usize = 1184; /// Construct from raw bytes, returning an error if the length is not [`Self::LEN`]. + #[cfg(feature = "host")] pub fn from_bytes(bytes: Vec) -> Result { if bytes.len() != Self::LEN { return Err(crate::error::LeeCoreError::DeserializationError(format!( @@ -59,6 +62,7 @@ impl SharedSecretKey { /// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction /// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via /// [`Self::decapsulate`]. + #[cfg(feature = "host")] #[must_use] pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) { let ek_bytes: ml_kem::kem::Key = @@ -76,30 +80,18 @@ impl SharedSecretKey { (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) } - /// Deterministically encapsulate a shared secret toward `ek` for use in tests. + /// Deterministically encapsulate a shared secret toward `ek` using a + /// pre-derived `esk` as the ML-KEM encapsulation randomness. /// - /// The shared secret has no secret entropy — it is fully determined by `ek`, - /// `message_hash`, and `output_index`, all of which are public. This makes it - /// unsuitable for real encryption but useful for producing stable, reproducible - /// shared secrets in unit tests. Use a distinct `output_index` per output to - /// avoid EPK collisions across multiple outputs in the same test. - /// - /// For production use [`Self::encapsulate`], which draws randomness from the OS. - #[cfg(any(test, feature = "test_utils"))] + /// The `esk` must be derived via `derive_esk(account_id, random_seed, nonce)` + /// which binds it to the account and incorporates OS entropy. #[must_use] pub fn encapsulate_deterministic( ek: &MlKem768EncapsulationKey, - message_hash: &[u8; 32], - output_index: u32, + esk: &crate::encryption::EphemeralSecretKey, ) -> (Self, EphemeralPublicKey) { - use risc0_zkvm::sha::{Impl, Sha256 as _}; - - let mut input = Vec::with_capacity(36); - input.extend_from_slice(message_hash); - input.extend_from_slice(&output_index.to_le_bytes()); - let hash = Impl::hash_bytes(&input); - let m: ml_kem::B32 = - ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes"); + let m: ml_kem::B32 = ml_kem::array::Array::try_from(esk.0.as_slice()) + .expect("EphemeralSecretKey is 32 bytes"); let ek_bytes: ml_kem::kem::Key = ek.0.as_slice() diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index 900de667..4bcb23a4 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -11,8 +11,8 @@ pub use commitment::{ compute_digest_for_path, }; pub use encryption::{ - EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, - SharedSecretKey, ViewTag, + EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey, + ML_KEM_768_CIPHERTEXT_LEN, SharedSecretKey, ViewTag, }; pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey}; pub use program::PrivateAccountKind; diff --git a/lee/state_machine/core/src/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index 0490ac00..0374a31a 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, + 242, 239, 57, 244, 89, 109, 65, 201, 223, 100, 43, 87, 205, 83, 148, 161, 176, 22, 208, + 220, 68, 135, 10, 171, 182, 80, 54, 74, 228, 244, 236, 7, ]); - 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, + 149, 125, 157, 109, 119, 81, 9, 163, 231, 181, 214, 43, 57, 113, 221, 72, 180, 149, + 189, 170, 32, 181, 255, 231, 19, 92, 235, 59, 153, 185, 172, 206, ]); - 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, + 30, 232, 222, 201, 233, 125, 124, 194, 58, 39, 121, 96, 185, 84, 168, 109, 80, 111, + 159, 112, 84, 100, 133, 244, 16, 34, 221, 35, 128, 131, 98, 159, ]); - 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/mod.rs b/lee/state_machine/core/src/program/mod.rs index 287751d2..770bcf2d 100644 --- a/lee/state_machine/core/src/program/mod.rs +++ b/lee/state_machine/core/src/program/mod.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), } } } diff --git a/lee/state_machine/core/src/program/tests.rs b/lee/state_machine/core/src/program/tests.rs index df9f5126..19a259a8 100644 --- a/lee/state_machine/core/src/program/tests.rs +++ b/lee/state_machine/core/src/program/tests.rs @@ -184,13 +184,14 @@ fn for_private_pda_matches_pinned_value() { 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, + 5, 87, 128, 244, 206, 244, 65, 130, 178, 88, 225, 183, 0, 159, 201, 201, 212, 206, 6, 156, + 13, 55, 32, 139, 91, 222, 209, 83, 172, 148, 123, 179, ]); assert_eq!( - AccountId::for_private_pda(&program_id, &seed, &npk, identifier), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier), expected ); } @@ -202,9 +203,10 @@ fn for_private_pda_differs_for_different_npk() { 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), ); } @@ -215,9 +217,10 @@ fn for_private_pda_differs_for_different_seed() { 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), ); } @@ -228,9 +231,10 @@ fn for_private_pda_differs_for_different_program_id() { 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), ); } @@ -241,13 +245,14 @@ fn for_private_pda_differs_for_different_identifier() { 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), ); } @@ -258,7 +263,8 @@ fn for_private_pda_differs_from_public_pda() { 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); } @@ -295,22 +301,24 @@ fn for_private_account_dispatches_correctly() { 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/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs index 970bfbd4..005a9230 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs @@ -1,8 +1,8 @@ #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] use lee_core::{ - Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, - Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey, + Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, EphemeralSecretKey, Nullifier, + PrivacyPreservingCircuitOutput, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, program::{PdaSeed, PrivateAccountKind}, }; @@ -58,7 +58,8 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() AccountId::new([0; 32]), ); - let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); let balance_to_move: u128 = 37; @@ -79,8 +80,9 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() let expected_sender_pre = sender.clone(); - let shared_secret = - SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0; + let init_nonce = Nonce::private_account_nonce_init(&recipient_account_id); + let esk = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce); + let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk).0; let (output, proof) = execute_and_prove( vec![sender, recipient], @@ -88,13 +90,9 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: shared_secret, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }, @@ -138,12 +136,14 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { data: Data::default(), }, true, - AccountId::for_regular_private_account(&sender_keys.npk(), 0), + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0), ); - let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account); - let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); let balance_to_move: u128 = 37; @@ -179,23 +179,25 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { Commitment::new(&recipient_account_id, &expected_private_account_2), ]; - let shared_secret_1 = - SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0; + let esk_1 = EphemeralSecretKey::new( + &sender_account_id, + &[0; 32], + &sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + ); + let shared_secret_1 = SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &esk_1).0; + let init_nonce_2 = Nonce::private_account_nonce_init(&recipient_account_id); + let esk_2 = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce_2); let shared_secret_2 = - SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0; + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk_2).0; let (output, proof) = execute_and_prove( vec![sender_pre, recipient], Program::serialize_instruction(balance_to_move).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: shared_secret_1, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: commitment_set .get_proof_for(&commitment_sender) @@ -203,13 +205,9 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: shared_secret_2, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }, @@ -250,7 +248,7 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { let pre = AccountWithMetadata::new( Account::default(), false, - AccountId::for_regular_private_account(&account_keys.npk(), 0), + AccountId::for_regular_private_account(&account_keys.npk(), &account_keys.vpk(), 0), ); let validity_window_chain_caller = crate::test_methods::validity_window_chain_caller(); @@ -265,9 +263,6 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { )) .unwrap(); - let shared_secret = - SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0; - let program_with_deps = ProgramWithDependencies::new( validity_window_chain_caller, [(validity_window.id(), validity_window)].into(), @@ -277,13 +272,9 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { vec![pre], instruction, vec![InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &account_keys.npk(), - &account_keys.vpk(), - ), + vpk: account_keys.vpk(), + random_seed: [0; 32], npk: account_keys.npk(), - ssk: shared_secret, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }], @@ -302,19 +293,21 @@ fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; + let account_id = + AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); + let init_nonce = Nonce::private_account_nonce_init(&account_id); + let esk = EphemeralSecretKey::new(&account_id, &[0; 32], &init_nonce); + let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let (output, _proof) = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret, identifier, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -343,11 +336,8 @@ fn private_pda_init() { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = - SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - // PDA (new, private PDA) - let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); let auth_id = simple_transfer.id(); @@ -361,10 +351,9 @@ fn private_pda_init() { vec![pda_pre], instruction, vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret_pda, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -386,11 +375,8 @@ fn private_pda_withdraw() { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = - SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - // PDA (new, private PDA) - let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); // Recipient (public) @@ -417,10 +403,9 @@ fn private_pda_withdraw() { instruction, vec![ InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret_pda, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -445,8 +430,6 @@ fn shared_account_receives_via_simple_transfer() { let shared_keys = test_private_account_keys_1(); let shared_npk = shared_keys.npk(); let shared_identifier: u128 = 42; - let shared_secret = - SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0; // Sender: public account with balance, owned by auth-transfer let sender_id = AccountId::new([99; 32]); @@ -461,7 +444,7 @@ fn shared_account_receives_via_simple_transfer() { ); // Recipient: shared private account (new, unauthorized) - let shared_account_id = AccountId::from((&shared_npk, shared_identifier)); + let shared_account_id = AccountId::from((&shared_npk, &shared_keys.vpk(), shared_identifier)); let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id); let balance_to_move: u128 = 100; @@ -473,10 +456,9 @@ fn shared_account_receives_via_simple_transfer() { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&shared_npk, &shared_keys.vpk()), + vpk: shared_keys.vpk(), + random_seed: [0; 32], npk: shared_npk, - ssk: shared_secret, identifier: shared_identifier, commitment_root: DUMMY_COMMITMENT_HASH, }, @@ -496,17 +478,21 @@ fn private_authorized_init_encrypts_regular_kind_with_identifier() { let program = crate::test_methods::claimer(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &account_id, + &[0; 32], + &Nonce::private_account_nonce_init(&account_id), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; let pre = AccountWithMetadata::new(Account::default(), true, account_id); let (output, _) = execute_and_prove( vec![pre], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), - ssk, + vpk: keys.vpk(), + random_seed: [0; 32], nsk: keys.nsk, identifier, commitment_root: DUMMY_COMMITMENT_HASH, @@ -528,18 +514,22 @@ fn private_unauthorized_init_encrypts_regular_kind_with_identifier() { let program = crate::test_methods::claimer(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier); + let recipient_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &recipient_id, + &[0; 32], + &Nonce::private_account_nonce_init(&recipient_id), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id); let (output, _) = execute_and_prove( vec![recipient], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk: keys.npk(), - ssk, identifier, commitment_root: DUMMY_COMMITMENT_HASH, }], @@ -560,8 +550,13 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() { let program = crate::test_methods::noop(); let keys = test_private_account_keys_1(); let identifier: u128 = 99; - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &account_id, + &[0; 32], + &Nonce::default().private_account_nonce_increment(&keys.nsk), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; let account = Account { program_owner: program.id(), balance: 1, @@ -577,9 +572,8 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() { vec![sender], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), - ssk, + vpk: keys.vpk(), + random_seed: [0; 32], nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), identifier, @@ -604,10 +598,14 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let simple_transfer_id = simple_transfer.id(); - let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &pda_id, + &[0; 32], + &Nonce::default().private_account_nonce_increment(&keys.nsk), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; let pda_account = Account { program_owner: simple_transfer_id, balance: 1, @@ -630,9 +628,8 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() { Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), - ssk, + vpk: keys.vpk(), + random_seed: [0; 32], nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier, @@ -660,19 +657,16 @@ fn private_pda_init_identifier_mismatch_fails() { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret, identifier: 99, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -690,10 +684,8 @@ fn private_pda_update_identifier_mismatch_fails() { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let simple_transfer_id = simple_transfer.id(); - let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5); let pda_account = Account { program_owner: simple_transfer_id, balance: 1, @@ -714,9 +706,8 @@ fn private_pda_update_identifier_mismatch_fails() { Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), - ssk, + vpk: keys.vpk(), + random_seed: [0; 32], nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), identifier: 99, diff --git a/lee/state_machine/src/privacy_preserving_transaction/message.rs b/lee/state_machine/src/privacy_preserving_transaction/message.rs index 1315cf4a..c1f2615c 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/message.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/message.rs @@ -103,8 +103,8 @@ impl Message { #[cfg(test)] pub mod tests { use lee_core::{ - Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind, - SharedSecretKey, + Commitment, EncryptionScheme, EphemeralSecretKey, Nullifier, NullifierPublicKey, + PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, encryption::ViewingPublicKey, program::{BlockValidityWindow, TimestampValidityWindow}, @@ -123,6 +123,7 @@ pub mod tests { let npk1 = NullifierPublicKey::from(&nsk1); let npk2 = NullifierPublicKey::from(&nsk2); + let vpk = ViewingPublicKey::from_seed(&[7; 32], &[8; 32]); let public_account_ids = vec![AccountId::new([1; 32])]; @@ -132,10 +133,10 @@ pub mod tests { let encrypted_private_post_states = Vec::new(); - let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, 0); + let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, &vpk, 0); let new_commitments = vec![Commitment::new(&account_id2, &account2)]; - let account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, 0); + let account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, &vpk, 0); let old_commitment = Commitment::new(&account_id1, &account1); let new_nullifiers = vec![( Nullifier::for_account_update(&old_commitment, &nsk1), @@ -225,9 +226,10 @@ pub mod tests { let npk = NullifierPublicKey::from(&[1; 32]); let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]); let account = Account::default(); - let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, 0); + let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, &vpk, 0); let commitment = Commitment::new(&account_id, &account); - let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0_u8; 32], 0); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&vpk, &EphemeralSecretKey([0_u8; 32])); let ciphertext = EncryptionScheme::encrypt( &account, &PrivateAccountKind::Regular(0), diff --git a/lee/state_machine/src/state/tests/changer_claimer.rs b/lee/state_machine/src/state/tests/changer_claimer.rs index f9ddce25..9a581c63 100644 --- a/lee/state_machine/src/state/tests/changer_claimer.rs +++ b/lee/state_machine/src/state/tests/changer_claimer.rs @@ -60,8 +60,11 @@ fn public_changer_claimer_data_change_no_claim_fails() { fn private_changer_claimer_no_data_change_no_claim_succeeds() { let program = crate::test_methods::changer_claimer(); let sender_keys = test_private_account_keys_1(); - let private_account = - AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0)); + let private_account = AccountWithMetadata::new( + Account::default(), + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); // Don't change data (None) and don't claim (false) let instruction: (Option>, bool) = (None, false); @@ -69,12 +72,8 @@ fn private_changer_claimer_no_data_change_no_claim_succeeds() { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -90,8 +89,11 @@ fn private_changer_claimer_no_data_change_no_claim_succeeds() { fn private_changer_claimer_data_change_no_claim_fails() { let program = crate::test_methods::changer_claimer(); let sender_keys = test_private_account_keys_1(); - let private_account = - AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0)); + let private_account = AccountWithMetadata::new( + Account::default(), + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); // Change data but don't claim (false) - should fail let new_data = vec![1, 2, 3, 4, 5]; let instruction: (Option>, bool) = (Some(new_data), false); @@ -100,12 +102,8 @@ fn private_changer_claimer_data_change_no_claim_fails() { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, diff --git a/lee/state_machine/src/state/tests/circuit.rs b/lee/state_machine/src/state/tests/circuit.rs index 0860e98b..c13a7370 100644 --- a/lee/state_machine/src/state/tests/circuit.rs +++ b/lee/state_machine/src/state/tests/circuit.rs @@ -45,10 +45,13 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() { ..Account::default() }, true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let private_account_2 = AccountWithMetadata::new( + Account::default(), + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); // Setting the recipient nsk to authorize the sender. // This should be set to the sender private account in a normal circumstance. @@ -59,32 +62,18 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) - .0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: recipient_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: SharedSecretKey::encapsulate_deterministic( - &recipient_keys.vpk(), - &[0_u8; 32], - 0, - ) - .0, - commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -105,7 +94,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid ..Account::default() }, true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -114,7 +103,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid ..Account::default() }, false, - (&recipient_keys.npk(), 0), + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), ); let result = execute_and_prove( @@ -122,32 +111,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) - .0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: SharedSecretKey::encapsulate_deterministic( - &recipient_keys.vpk(), - &[0_u8; 32], - 0, - ) - .0, - commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -168,7 +143,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_ ..Account::default() }, true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -177,7 +152,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_ ..Account::default() }, false, - (&recipient_keys.npk(), 0), + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), ); let result = execute_and_prove( @@ -185,32 +160,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_ Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) - .0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: SharedSecretKey::encapsulate_deterministic( - &recipient_keys.vpk(), - &[0_u8; 32], - 0, - ) - .0, - commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -231,7 +192,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided( ..Account::default() }, true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -240,7 +201,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided( ..Account::default() }, false, - (&recipient_keys.npk(), 0), + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), ); let result = execute_and_prove( @@ -248,32 +209,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided( Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) - .0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: SharedSecretKey::encapsulate_deterministic( - &recipient_keys.vpk(), - &[0_u8; 32], - 0, - ) - .0, - commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -294,7 +241,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided ..Account::default() }, true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account { @@ -303,7 +250,7 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided ..Account::default() }, false, - (&recipient_keys.npk(), 0), + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), ); let result = execute_and_prove( @@ -311,32 +258,18 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) - .0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: SharedSecretKey::encapsulate_deterministic( - &recipient_keys.vpk(), - &[0_u8; 32], - 0, - ) - .0, - commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -358,13 +291,13 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu ..Account::default() }, true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), ); let private_account_2 = AccountWithMetadata::new( Account::default(), // This should be set to false in normal circumstances true, - (&recipient_keys.npk(), 0), + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), ); let result = execute_and_prove( @@ -372,32 +305,18 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) - .0, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: SharedSecretKey::encapsulate_deterministic( - &recipient_keys.vpk(), - &[0_u8; 32], - 0, - ) - .0, - commitment_root: DUMMY_COMMITMENT_HASH, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), @@ -415,7 +334,6 @@ fn private_pda_without_binding_fails() { let program = crate::test_methods::simple_balance_transfer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let public_account_1 = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -434,10 +352,9 @@ fn private_pda_without_binding_fails() { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -460,19 +377,17 @@ fn private_pda_claim_succeeds() { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX); + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -500,22 +415,21 @@ fn private_pda_npk_mismatch_fails() { let npk_a = keys_a.npk(); let npk_b = keys_b.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0; // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in // the circuit must reject. - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a, u128::MAX); + let account_id = + AccountId::for_private_pda(&program.id(), &seed, &npk_a, &keys_a.vpk(), u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk_b, &keys_b.vpk()), + vpk: keys_b.vpk(), + random_seed: [0; 32], npk: npk_b, - ssk: shared_secret, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -538,9 +452,9 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([77; 32]); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX); + let account_id = + AccountId::for_private_pda(&delegator.id(), &seed, &npk, &keys.vpk(), u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let callee_id = callee.id(); @@ -550,10 +464,9 @@ fn caller_pda_seeds_authorize_private_pda_for_callee() { vec![pre_state], Program::serialize_instruction((seed, seed, callee_id)).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -579,9 +492,9 @@ fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() { let npk = keys.npk(); let claim_seed = PdaSeed::new([77; 32]); let wrong_delegated_seed = PdaSeed::new([88; 32]); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; - let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX); + let account_id = + AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, &keys.vpk(), u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let callee_id = callee.id(); @@ -591,10 +504,9 @@ fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() { vec![pre_state], Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -619,11 +531,21 @@ fn two_private_pda_claims_under_same_seed_are_rejected() { let keys_a = test_private_account_keys_1(); let keys_b = test_private_account_keys_2(); let seed = PdaSeed::new([55; 32]); - let shared_a = SharedSecretKey::encapsulate_deterministic(&keys_a.vpk(), &[0_u8; 32], 0).0; - let shared_b = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0; - let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk(), u128::MAX); - let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk(), u128::MAX); + let account_a = AccountId::for_private_pda( + &program.id(), + &seed, + &keys_a.npk(), + &keys_a.vpk(), + u128::MAX, + ); + let account_b = AccountId::for_private_pda( + &program.id(), + &seed, + &keys_b.npk(), + &keys_b.vpk(), + u128::MAX, + ); let pre_a = AccountWithMetadata::new(Account::default(), false, account_a); let pre_b = AccountWithMetadata::new(Account::default(), false, account_b); @@ -633,19 +555,17 @@ fn two_private_pda_claims_under_same_seed_are_rejected() { Program::serialize_instruction(seed).unwrap(), vec![ InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&keys_a.npk(), &keys_a.vpk()), + vpk: keys_a.vpk(), + random_seed: [0; 32], npk: keys_a.npk(), - ssk: shared_a, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }, InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&keys_b.npk(), &keys_b.vpk()), + vpk: keys_b.vpk(), + random_seed: [0; 32], npk: keys_b.npk(), - ssk: shared_b, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -667,12 +587,11 @@ fn private_pda_top_level_reuse_rejected_by_binding_check() { let program = crate::test_methods::noop(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let seed = PdaSeed::new([99; 32]); // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = // true, account_id derived via the private formula. - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX); + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX); let owned_pre_state = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -686,10 +605,9 @@ fn private_pda_top_level_reuse_rejected_by_binding_check() { vec![owned_pre_state], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), + vpk: keys.vpk(), + random_seed: [0; 32], npk, - ssk: shared_secret, identifier: u128::MAX, commitment_root: DUMMY_COMMITMENT_HASH, seed: None, @@ -766,33 +684,23 @@ fn circuit_should_fail_if_there_are_repeated_ids() { ..Account::default() }, true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), ); - let shared_secret = - SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0; let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: shared_secret, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (1, vec![]), identifier: 0, }, InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: shared_secret, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: (1, vec![]), identifier: 0, @@ -812,14 +720,15 @@ fn private_authorized_uninitialized_account() { let private_keys = test_private_account_keys_1(); // Create an authorized private account with default values (new account being initialized) - let authorized_account = - AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0)); + let authorized_account = AccountWithMetadata::new( + Account::default(), + true, + (&private_keys.npk(), &private_keys.vpk(), 0), + ); let program = crate::test_methods::simple_balance_transfer(); // Set up parameters for the new account - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let instruction: u128 = 0; @@ -828,12 +737,8 @@ fn private_authorized_uninitialized_account() { vec![authorized_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &private_keys.npk(), - &private_keys.vpk(), - ), - ssk: shared_secret, + vpk: private_keys.vpk(), + random_seed: [0; 32], nsk: private_keys.nsk, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, @@ -851,7 +756,8 @@ fn private_authorized_uninitialized_account() { let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(result.is_ok()); - let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0); + let account_id = + AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); let nullifier = Nullifier::for_account_initialization(&account_id); assert!(state.private_state.1.contains(&nullifier)); } @@ -865,24 +771,21 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() { // especially PDAs. Private PDAs are not useful in practice because there is no way to // operate them without the corresponding private keys, so unauthorized private claiming // remains allowed. - let unauthorized_account = - AccountWithMetadata::new(Account::default(), false, (&private_keys.npk(), 0)); + let unauthorized_account = AccountWithMetadata::new( + Account::default(), + false, + (&private_keys.npk(), &private_keys.vpk(), 0), + ); let program = crate::test_methods::claimer(); - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let (output, proof) = execute_and_prove( vec![unauthorized_account], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivateUnauthorized { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &private_keys.npk(), - &private_keys.vpk(), - ), + vpk: private_keys.vpk(), + random_seed: [0; 32], npk: private_keys.npk(), - ssk: shared_secret, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }], @@ -899,7 +802,8 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() { .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); - let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0); + let account_id = + AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); let nullifier = Nullifier::for_account_initialization(&account_id); assert!(state.private_state.1.contains(&nullifier)); } @@ -912,14 +816,15 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { let private_keys = test_private_account_keys_1(); // Step 1: Create a new private account with authorization - let authorized_account = - AccountWithMetadata::new(Account::default(), true, (&private_keys.npk(), 0)); + let authorized_account = AccountWithMetadata::new( + Account::default(), + true, + (&private_keys.npk(), &private_keys.vpk(), 0), + ); let claimer_program = crate::test_methods::claimer(); // Set up parameters for claiming the new account - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let instruction = (); @@ -928,12 +833,8 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { vec![authorized_account.clone()], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &private_keys.npk(), - &private_keys.vpk(), - ), - ssk: shared_secret, + vpk: private_keys.vpk(), + random_seed: [0; 32], nsk: private_keys.nsk, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, @@ -955,7 +856,8 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { ); // Verify the account is now initialized (nullifier exists) - let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0); + let account_id = + AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); let nullifier = Nullifier::for_account_initialization(&account_id); assert!(state.private_state.1.contains(&nullifier)); @@ -967,20 +869,14 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { }; let noop_program = crate::test_methods::noop(); - let shared_secret2 = - SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0).0; // Step 3: Try to execute noop program with authentication but without initialization let res = execute_and_prove( vec![account_metadata], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &private_keys.npk(), - &private_keys.vpk(), - ), - ssk: shared_secret2, + vpk: private_keys.vpk(), + random_seed: [0; 32], nsk: private_keys.nsk, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, @@ -1010,8 +906,10 @@ fn two_private_pda_family_members_receive_and_spend() { ); let funder_id = funder_keys.account_id(); - let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); - let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1); + let alice_pda_0_id = + AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 0); + let alice_pda_1_id = + AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 1); let recipient_id = test_public_account_keys_2().account_id(); let recipient_signing_key = test_public_account_keys_2().signing_key; @@ -1031,11 +929,6 @@ fn two_private_pda_family_members_receive_and_spend() { ..Account::default() }; - let (alice_shared_0, alice_epk_0) = - SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 0); - let (alice_shared_1, alice_epk_1) = - SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 1); - // Fund alice_pda_0 via authenticated_transfer directly. { let funder_account = state.get_account_by_id(funder_id); @@ -1049,10 +942,9 @@ fn two_private_pda_family_members_receive_and_spend() { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { - epk: alice_epk_0.clone(), - view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()), + vpk: alice_keys.vpk(), + random_seed: [0; 32], npk: alice_npk, - ssk: alice_shared_0, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, seed: Some((seed, proxy_id)), @@ -1086,10 +978,9 @@ fn two_private_pda_family_members_receive_and_spend() { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { - epk: alice_epk_1.clone(), - view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()), + vpk: alice_keys.vpk(), + random_seed: [0; 32], npk: alice_npk, - ssk: alice_shared_1, identifier: 1, commitment_root: DUMMY_COMMITMENT_HASH, seed: Some((seed, proxy_id)), @@ -1127,9 +1018,8 @@ fn two_private_pda_family_members_receive_and_spend() { Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { - epk: alice_epk_0, - view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()), - ssk: alice_shared_0, + vpk: alice_keys.vpk(), + random_seed: [0; 32], nsk: alice_keys.nsk, membership_proof: state .get_proof_for_commitment(&commitment_pda_0) @@ -1165,9 +1055,8 @@ fn two_private_pda_family_members_receive_and_spend() { Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { - epk: alice_epk_1, - view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()), - ssk: alice_shared_1, + vpk: alice_keys.vpk(), + random_seed: [0; 32], nsk: alice_keys.nsk, membership_proof: state .get_proof_for_commitment(&commitment_pda_1) @@ -1205,7 +1094,6 @@ fn two_private_pda_family_members_receive_and_spend() { }; let commitment_pda_1_after_spend = Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend); - let alice_shared_1_refund = SharedSecretKey([12; 32]); { let recipient_account = state.get_account_by_id(recipient_id); let recipient_nonce = recipient_account.nonce; @@ -1218,10 +1106,9 @@ fn two_private_pda_family_members_receive_and_spend() { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaUpdate { - epk: EphemeralPublicKey(vec![12_u8; ML_KEM_768_CIPHERTEXT_LEN]), - view_tag: EncryptedAccountData::compute_view_tag(&alice_npk, &alice_keys.vpk()), + vpk: alice_keys.vpk(), + random_seed: [0; 32], nsk: alice_keys.nsk, - ssk: alice_shared_1_refund, membership_proof: state .get_proof_for_commitment(&commitment_pda_1_after_spend) .expect("pda_1 after spend must be in state"), diff --git a/lee/state_machine/src/state/tests/claiming.rs b/lee/state_machine/src/state/tests/claiming.rs index aba5c447..acfe9834 100644 --- a/lee/state_machine/src/state/tests/claiming.rs +++ b/lee/state_machine/src/state/tests/claiming.rs @@ -303,19 +303,21 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() { balance: 100, ..Account::default() }; - let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id); let mut state = V03State::new().with_private_accounts([(sender_commitment.clone(), sender_init_nullifier)]); - let sender_pre = - AccountWithMetadata::new(sender_private_account, true, (&sender_keys.npk(), 0)); + let sender_pre = AccountWithMetadata::new( + sender_private_account, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap(); let recipient_account_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id); - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); let balance = 37; @@ -324,12 +326,8 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() { Program::serialize_instruction(balance).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: shared_secret, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: state .get_proof_for_commitment(&sender_commitment) @@ -383,7 +381,7 @@ fn private_chained_call(number_of_calls: u32) { ..Account::default() }, true, - (&from_keys.npk(), 0), + (&from_keys.npk(), &from_keys.vpk(), 0), ); let to_account = AccountWithMetadata::new( Account { @@ -391,11 +389,12 @@ fn private_chained_call(number_of_calls: u32) { ..Account::default() }, true, - (&to_keys.npk(), 0), + (&to_keys.npk(), &to_keys.vpk(), 0), ); - let from_account_id = AccountId::for_regular_private_account(&from_keys.npk(), 0); - let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), 0); + let from_account_id = + AccountId::for_regular_private_account(&from_keys.npk(), &from_keys.vpk(), 0); + let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), &to_keys.vpk(), 0); let from_commitment = Commitment::new(&from_account_id, &from_account.account); let to_commitment = Commitment::new(&to_account_id, &to_account.account); let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id); @@ -414,12 +413,6 @@ fn private_chained_call(number_of_calls: u32) { None, ); - let (from_ss, from_epk) = - SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0_u8; 32], 0); - - let (to_ss, to_epk) = - SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0_u8; 32], 1); - let mut dependencies = HashMap::new(); dependencies.insert(simple_transfers.id(), simple_transfers); @@ -448,9 +441,8 @@ fn private_chained_call(number_of_calls: u32) { Program::serialize_instruction(instruction).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: to_epk, - view_tag: EncryptedAccountData::compute_view_tag(&to_keys.npk(), &to_keys.vpk()), - ssk: to_ss, + vpk: from_keys.vpk(), + random_seed: [0; 32], nsk: from_keys.nsk, membership_proof: state .get_proof_for_commitment(&from_commitment) @@ -458,12 +450,8 @@ fn private_chained_call(number_of_calls: u32) { identifier: 0, }, InputAccountIdentity::PrivateAuthorizedUpdate { - epk: from_epk, - view_tag: EncryptedAccountData::compute_view_tag( - &from_keys.npk(), - &from_keys.vpk(), - ), - ssk: from_ss, + vpk: to_keys.vpk(), + random_seed: [0; 32], nsk: to_keys.nsk, membership_proof: state .get_proof_for_commitment(&to_commitment) diff --git a/lee/state_machine/src/state/tests/genesis.rs b/lee/state_machine/src/state/tests/genesis.rs index d782f325..f67628ee 100644 --- a/lee/state_machine/src/state/tests/genesis.rs +++ b/lee/state_machine/src/state/tests/genesis.rs @@ -43,8 +43,8 @@ fn new_includes_nullifiers_for_private_accounts() { ..Account::default() }; - let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), 0); - let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), 0); + let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), &keys1.vpk(), 0); + let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), &keys2.vpk(), 0); let init_commitment1 = Commitment::new(&account_id1, &account); let init_commitment2 = Commitment::new(&account_id2, &account); diff --git a/lee/state_machine/src/state/tests/mod.rs b/lee/state_machine/src/state/tests/mod.rs index f6b56be6..55a99ead 100644 --- a/lee/state_machine/src/state/tests/mod.rs +++ b/lee/state_machine/src/state/tests/mod.rs @@ -7,10 +7,10 @@ use std::collections::HashMap; use lee_core::{ - BlockId, Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, - Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp, + BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, + NullifierPublicKey, NullifierSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, - encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey}, + encryption::ViewingPublicKey, program::{ BlockValidityWindow, ExecutionValidationError, MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, TimestampValidityWindow, WrappedBalanceSum, @@ -118,7 +118,7 @@ impl V03State { #[must_use] pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self { - let account_id = AccountId::for_regular_private_account(&keys.npk(), 0); + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0); let commitment = Commitment::new(&account_id, account); self.private_state.0.extend(&[commitment]); self @@ -267,10 +267,11 @@ fn shielded_balance_transfer_for_tests( let sender_nonce = sender.account.nonce; - let recipient = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0); + let recipient = AccountWithMetadata::new( + Account::default(), + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( vec![sender, recipient], @@ -278,13 +279,9 @@ fn shielded_balance_transfer_for_tests( vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: shared_secret, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }, @@ -312,33 +309,27 @@ fn private_balance_transfer_for_tests( state: &V03State, ) -> PrivacyPreservingTransaction { let program = crate::test_methods::simple_balance_transfer(); - let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); let sender_pre = AccountWithMetadata::new( sender_private_account.clone(), true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let recipient_pre = AccountWithMetadata::new( + Account::default(), + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), ); - let recipient_pre = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - let (shared_secret_1, epk_1) = - SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); - - let (shared_secret_2, epk_2) = - SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1); let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: epk_1, - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: shared_secret_1, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: state .get_proof_for_commitment(&sender_commitment) @@ -346,13 +337,9 @@ fn private_balance_transfer_for_tests( identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { - epk: epk_2, - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), + vpk: recipient_keys.vpk(), + random_seed: [0; 32], npk: recipient_keys.npk(), - ssk: shared_secret_2, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }, @@ -376,12 +363,13 @@ fn deshielded_balance_transfer_for_tests( state: &V03State, ) -> PrivacyPreservingTransaction { let program = crate::test_methods::simple_balance_transfer(); - let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); let sender_pre = AccountWithMetadata::new( sender_private_account.clone(), true, - (&sender_keys.npk(), 0), + (&sender_keys.npk(), &sender_keys.vpk(), 0), ); let recipient_pre = AccountWithMetadata::new( state.get_account_by_id(*recipient_account_id), @@ -389,20 +377,13 @@ fn deshielded_balance_transfer_for_tests( *recipient_account_id, ); - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); - let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.npk(), - &sender_keys.vpk(), - ), - ssk: shared_secret, + vpk: sender_keys.vpk(), + random_seed: [0; 32], nsk: sender_keys.nsk, membership_proof: state .get_proof_for_commitment(&sender_commitment) diff --git a/lee/state_machine/src/state/tests/privacy_preserving.rs b/lee/state_machine/src/state/tests/privacy_preserving.rs index 0205b5fd..8c33e202 100644 --- a/lee/state_machine/src/state/tests/privacy_preserving.rs +++ b/lee/state_machine/src/state/tests/privacy_preserving.rs @@ -68,8 +68,10 @@ fn transition_from_privacy_preserving_transaction_private() { &state, ); - let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); - let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); let expected_new_commitment_1 = Commitment::new( &sender_account_id, &Account { @@ -203,7 +205,8 @@ fn transition_from_privacy_preserving_transaction_deshielded() { &state, ); - let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); let expected_new_commitment = Commitment::new( &sender_account_id, &Account { @@ -487,10 +490,14 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { false, sender_keys.account_id(), ); - let recipient_account = - AccountWithMetadata::new(Account::default(), true, (&recipient_keys.npk(), 0)); + let recipient_account = AccountWithMetadata::new( + Account::default(), + true, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); - let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); let recipient_commitment = Commitment::new(&recipient_account_id, &recipient_account.account); let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id); let state = V03State::new() @@ -504,9 +511,6 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { let balance_to_transfer = 10_u128; let instruction = (balance_to_transfer, simple_transfers.id()); - let recipient = - SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0; - let mut dependencies = HashMap::new(); dependencies.insert(simple_transfers.id(), simple_transfers); let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies); @@ -518,12 +522,8 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(Vec::new()), - view_tag: EncryptedAccountData::compute_view_tag( - &recipient_keys.npk(), - &recipient_keys.vpk(), - ), - ssk: recipient, + vpk: recipient_keys.vpk(), + random_seed: [0; 32], nsk: recipient_keys.nsk, membership_proof: state .get_proof_for_commitment(&recipient_commitment) diff --git a/lee/state_machine/src/state/tests/validity_window.rs b/lee/state_machine/src/state/tests/validity_window.rs index 5a46bd5b..12e7c229 100644 --- a/lee/state_machine/src/state/tests/validity_window.rs +++ b/lee/state_machine/src/state/tests/validity_window.rs @@ -124,12 +124,13 @@ fn validity_window_works_in_privacy_preserving_transactions( let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); let validity_window_program = crate::test_methods::validity_window(); let account_keys = test_private_account_keys_1(); - let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); + let pre = AccountWithMetadata::new( + Account::default(), + false, + (&account_keys.npk(), &account_keys.vpk(), 0), + ); let mut state = V03State::new().with_test_programs(); let tx = { - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0); - let instruction = ( block_validity_window, TimestampValidityWindow::new_unbounded(), @@ -138,13 +139,9 @@ fn validity_window_works_in_privacy_preserving_transactions( vec![pre], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateUnauthorized { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &account_keys.npk(), - &account_keys.vpk(), - ), + vpk: account_keys.vpk(), + random_seed: [0; 32], npk: account_keys.npk(), - ssk: shared_secret, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }], @@ -192,12 +189,13 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions( let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap(); let validity_window_program = crate::test_methods::validity_window(); let account_keys = test_private_account_keys_1(); - let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); + let pre = AccountWithMetadata::new( + Account::default(), + false, + (&account_keys.npk(), &account_keys.vpk(), 0), + ); let mut state = V03State::new().with_test_programs(); let tx = { - let (shared_secret, epk) = - SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0); - let instruction = ( BlockValidityWindow::new_unbounded(), timestamp_validity_window, @@ -206,13 +204,9 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions( vec![pre], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateUnauthorized { - epk, - view_tag: EncryptedAccountData::compute_view_tag( - &account_keys.npk(), - &account_keys.vpk(), - ), + vpk: account_keys.vpk(), + random_seed: [0; 32], npk: account_keys.npk(), - ssk: shared_secret, identifier: 0, commitment_root: DUMMY_COMMITMENT_HASH, }], diff --git a/lee/state_machine/src/validated_state_diff/tests.rs b/lee/state_machine/src/validated_state_diff/tests.rs index 3cb61e7d..c471676c 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, EncryptedAccountData, InputAccountIdentity, SharedSecretKey, + Commitment, InputAccountIdentity, account::{Account, AccountWithMetadata}, }; @@ -105,8 +105,8 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { // Attacker controls a private account. let attacker_keys = test_private_account_keys_1(); - let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0); - let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk()); + let attacker_id = + AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0); let victim_id = AccountId::new([20_u8; 32]); let recipient_id = AccountId::new([42_u8; 32]); @@ -165,12 +165,8 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { // [2] recipient — first seen in simple_balance_transfer's program_output.pre_states let account_identities = vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: attacker_epk, - view_tag: EncryptedAccountData::compute_view_tag( - &attacker_keys.npk(), - &attacker_keys.vpk(), - ), - ssk: attacker_ssk, + vpk: attacker_keys.vpk(), + random_seed: [0; 32], nsk: attacker_keys.nsk, membership_proof, identifier: 0, @@ -232,7 +228,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { #[test] fn privacy_malicious_programs_cannot_drain_private_victim() { use lee_core::{ - Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey, + Commitment, InputAccountIdentity, account::{Account, AccountWithMetadata}, }; @@ -262,12 +258,13 @@ fn privacy_malicious_programs_cannot_drain_private_victim() { // Attacker controls a private account. let attacker_keys = test_private_account_keys_1(); - let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0); - let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk()); + let attacker_id = + AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0); // Victim is a private account — not registered in public chain state. let victim_keys = test_private_account_keys_2(); - let victim_id = AccountId::for_regular_private_account(&victim_keys.npk(), 0); + let victim_id = + AccountId::for_regular_private_account(&victim_keys.npk(), &victim_keys.vpk(), 0); let victim_balance = 5_000_u128; let recipient_id = AccountId::new([42_u8; 32]); @@ -328,12 +325,8 @@ fn privacy_malicious_programs_cannot_drain_private_victim() { // so PrivateAuthorizedUpdate is not an option. let account_identities = vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: attacker_epk, - view_tag: EncryptedAccountData::compute_view_tag( - &attacker_keys.npk(), - &attacker_keys.vpk(), - ), - ssk: attacker_ssk, + vpk: attacker_keys.vpk(), + random_seed: [0; 32], nsk: attacker_keys.nsk, membership_proof, identifier: 0, diff --git a/lez/indexer/core/Cargo.toml b/lez/indexer/core/Cargo.toml index 758acdd6..b49f6fee 100644 --- a/lez/indexer/core/Cargo.toml +++ b/lez/indexer/core/Cargo.toml @@ -7,6 +7,10 @@ license = { workspace = true } [lints] workspace = true +[features] +default = [] +testnet = [] + [dependencies] common.workspace = true logos-blockchain-zone-sdk.workspace = true @@ -30,4 +34,3 @@ tokio.workspace = true [dev-dependencies] tempfile.workspace = true -authenticated_transfer_core.workspace = true diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index c67148bd..4d583686 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -23,7 +23,12 @@ impl IndexerStore { /// Starting database at the start of new chain. /// Creates files if necessary. pub fn open_db(location: &Path) -> Result { + #[cfg(not(feature = "testnet"))] let initial_state = testnet_initial_state::initial_state(); + + #[cfg(feature = "testnet")] + let initial_state = testnet_initial_state::initial_state_testnet(); + let dbio = RocksDBIO::open_or_create(location, &initial_state)?; let current_state = dbio.final_state()?; diff --git a/lez/indexer/ffi/Cargo.toml b/lez/indexer/ffi/Cargo.toml index a1615b75..6b74ab8b 100644 --- a/lez/indexer/ffi/Cargo.toml +++ b/lez/indexer/ffi/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" [dependencies] lee.workspace = true -indexer_core.workspace = true +indexer_core = { workspace = true, features = ["testnet"] } indexer_service_protocol = { workspace = true, features = ["convert"] } env_logger.workspace = true diff --git a/lez/indexer/service/Cargo.toml b/lez/indexer/service/Cargo.toml index a07a2285..44ab7068 100644 --- a/lez/indexer/service/Cargo.toml +++ b/lez/indexer/service/Cargo.toml @@ -10,7 +10,7 @@ workspace = true [dependencies] indexer_service_protocol = { workspace = true, features = ["convert"] } indexer_service_rpc = { workspace = true, features = ["server"] } -indexer_core.workspace = true +indexer_core = { workspace = true, features = ["testnet"] } clap = { workspace = true, features = ["derive"] } anyhow.workspace = true diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index a5c535a0..4b5ebfbc 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -10,15 +10,15 @@ use common::{ }; use key_protocol::key_management::KeyChain; use lee::{ - Account, AccountId, Data, EphemeralPublicKey, PrivacyPreservingTransaction, PrivateKey, - PublicKey, PublicTransaction, SharedSecretKey, V03State, + Account, AccountId, Data, PrivacyPreservingTransaction, PrivateKey, PublicKey, + PublicTransaction, V03State, error::LeeError, execute_and_prove, privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies}, program::Program, }; use lee_core::{ - Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier, + Commitment, InputAccountIdentity, Nullifier, account::{AccountWithMetadata, Nonce}, program::PdaSeed, }; @@ -790,8 +790,11 @@ async fn block_production_aborts_when_clock_account_data_is_corrupted() { #[test] fn private_bridge_withdraw_invocation_is_dropped() { let sender_keys = KeyChain::new_os_random(); - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.nullifier_public_key, 0); + let sender_account_id = AccountId::for_regular_private_account( + &sender_keys.nullifier_public_key, + &sender_keys.viewing_public_key, + 0, + ); let sender_private_account = Account { program_owner: programs::authenticated_transfer().id(), balance: 100, @@ -812,7 +815,11 @@ fn private_bridge_withdraw_invocation_is_dropped() { let sender_pre = AccountWithMetadata::new( sender_private_account, true, - (&sender_keys.nullifier_public_key, 0), + ( + &sender_keys.nullifier_public_key, + &sender_keys.viewing_public_key, + 0, + ), ); let bridge_pre = AccountWithMetadata::new( state.get_account_by_id(bridge_account_id), @@ -820,8 +827,6 @@ fn private_bridge_withdraw_invocation_is_dropped() { bridge_account_id, ); - let shared_secret = SharedSecretKey::encapsulate(&sender_keys.viewing_public_key).0; - let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw { amount: 1, bedrock_account_pk: [0; 32], @@ -842,12 +847,8 @@ fn private_bridge_withdraw_invocation_is_dropped() { instruction, vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(vec![12_u8; 1088]), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.nullifier_public_key, - &sender_keys.viewing_public_key, - ), - ssk: shared_secret, + vpk: sender_keys.viewing_public_key.clone(), + random_seed: [0; 32], nsk: sender_keys.private_key_holder.nullifier_secret_key, membership_proof: state .get_proof_for_commitment(&sender_commitment) diff --git a/lez/testnet_initial_state/src/lib.rs b/lez/testnet_initial_state/src/lib.rs index 423d6eda..3296b953 100644 --- a/lez/testnet_initial_state/src/lib.rs +++ b/lez/testnet_initial_state/src/lib.rs @@ -86,6 +86,7 @@ pub struct PublicAccountPublicInitialData { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PrivateAccountPublicInitialData { pub npk: lee_core::NullifierPublicKey, + pub vpk: lee_core::encryption::ViewingPublicKey, pub account: lee_core::account::Account, } @@ -108,6 +109,7 @@ impl PrivateAccountPrivateInitialData { pub fn account_id(&self) -> lee::AccountId { lee::AccountId::for_regular_private_account( &self.key_chain.nullifier_public_key, + &self.key_chain.viewing_public_key, self.identifier, ) } @@ -183,6 +185,7 @@ fn initial_commitments() -> Vec { .into_iter() .map(|data| PrivateAccountPublicInitialData { npk: data.key_chain.nullifier_public_key, + vpk: data.key_chain.viewing_public_key.clone(), account: data.account, }) .collect() @@ -193,7 +196,8 @@ fn initial_private_accounts() -> Vec<(lee_core::Commitment, lee_core::Nullifier) .iter() .map(|init_comm_data| { let npk = &init_comm_data.npk; - let account_id = lee::AccountId::for_regular_private_account(npk, 0); + let account_id = + lee::AccountId::for_regular_private_account(npk, &init_comm_data.vpk, 0); let mut acc = init_comm_data.account.clone(); @@ -304,8 +308,8 @@ mod tests { const PUB_ACC_A_TEXT_ADDR: &str = "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV"; const PUB_ACC_B_TEXT_ADDR: &str = "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"; - const PRIV_ACC_A_TEXT_ADDR: &str = "4eGX3M3rgjHsme8n3sSp89af8JRZtYVTesbJjLqaX1VQ"; - const PRIV_ACC_B_TEXT_ADDR: &str = "3m6HQmCgmAvsxZtxAHPqqEqoBG4335fCG8TzxigyW7rE"; + const PRIV_ACC_A_TEXT_ADDR: &str = "EVesBKsYRVtkjnTcsbk8tWHkBn2xZmzAXzwgrP3ZaVoZ"; + const PRIV_ACC_B_TEXT_ADDR: &str = "94MXhZnueurjX6v37CYDKVEKYBiyhYArvtEdceq2XDQP"; #[test] fn pub_state_consistency() { @@ -440,6 +444,10 @@ mod tests { init_comms[0], PrivateAccountPublicInitialData { npk: NullifierPublicKey(NPK_PRIV_ACC_A), + vpk: init_private_accs_keys[0] + .key_chain + .viewing_public_key + .clone(), account: Account { program_owner: DEFAULT_PROGRAM_OWNER, balance: PRIV_ACC_A_INITIAL_BALANCE, @@ -453,6 +461,10 @@ mod tests { init_comms[1], PrivateAccountPublicInitialData { npk: NullifierPublicKey(NPK_PRIV_ACC_B), + vpk: init_private_accs_keys[1] + .key_chain + .viewing_public_key + .clone(), account: Account { program_owner: DEFAULT_PROGRAM_OWNER, balance: PRIV_ACC_B_INITIAL_BALANCE, diff --git a/lez/wallet-ffi/Cargo.toml b/lez/wallet-ffi/Cargo.toml index 47d18c53..b5fe7635 100644 --- a/lez/wallet-ffi/Cargo.toml +++ b/lez/wallet-ffi/Cargo.toml @@ -28,9 +28,6 @@ vault_core.workspace = true [build-dependencies] cbindgen = "0.29" -[dev-dependencies] -tempfile = "3" - [features] default = ["prove"] prove = ["lee/prove"] diff --git a/lez/wallet-ffi/src/types.rs b/lez/wallet-ffi/src/types.rs index 98590619..4f041c55 100644 --- a/lez/wallet-ffi/src/types.rs +++ b/lez/wallet-ffi/src/types.rs @@ -645,9 +645,10 @@ mod tests { let identifier = u128::from_le_bytes([45; 16]); let private_reg_acc_id = - AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)); + AccountId::for_private_account(&npk, &vpk, &PrivateAccountKind::Regular(identifier)); let private_pda_acc_id = AccountId::for_private_account( &npk, + &vpk, &PrivateAccountKind::Pda { program_id: [46; 8], seed: PdaSeed::new([47; 32]), diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index dae8fd31..a5f900d6 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -1,15 +1,15 @@ use core::fmt; use anyhow::Result; -use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use keycard_wallet::{KeycardWallet, python_path}; use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ CommitmentSetDigest, DUMMY_COMMITMENT_HASH, Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{AccountWithMetadata, Nonce}, - encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey}, + encryption::ViewingPublicKey, }; +use rand::{RngCore as _, rngs::OsRng}; use crate::{ExecutionFailureKind, WalletCore}; @@ -201,15 +201,38 @@ impl AccountManager { for account in accounts { let state = match account { AccountIdentity::Public(account_id) => { - prepare_public_state(wallet, account_id, true).await? + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let sk = wallet.get_account_public_signing_key(account_id).cloned(); + let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); + + State::Public { account, sk } } AccountIdentity::PublicNoSign(account_id) => { - prepare_public_state(wallet, account_id, false).await? + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let sk = None; + let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); + + State::Public { account, sk } } AccountIdentity::PublicKeycard { account_id, key_path, } => { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let account = AccountWithMetadata::new(acc.clone(), true, account_id); + if pin.is_none() { pin = Some( crate::helperfunctions::read_pin() @@ -225,40 +248,75 @@ impl AccountManager { .to_owned(), ); } - prepare_public_keycard_state(wallet, account_id, key_path).await? + + State::PublicKeycard { account, key_path } + } + AccountIdentity::PrivateOwned(account_id) => { + let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?; + + State::Private(pre) } - AccountIdentity::PrivateOwned(account_id) => State::Private( - private_key_tree_acc_preparation(wallet, account_id, false).await?, - ), AccountIdentity::PrivateForeign { npk, vpk, identifier, - } => State::Private(private_foreign_acc_preparation(npk, vpk, identifier)), - AccountIdentity::PrivatePdaOwned(account_id) => State::Private( - private_key_tree_acc_preparation(wallet, account_id, true).await?, - ), + } => { + let acc = lee_core::account::Account::default(); + let auth_acc = AccountWithMetadata::new(acc, false, (&npk, &vpk, identifier)); + let mut random_seed: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut random_seed); + let pre = AccountPreparedData { + nsk: None, + npk, + identifier, + vpk, + pre_state: auth_acc, + proof: None, + random_seed, + is_pda: false, + }; + + State::Private(pre) + } + AccountIdentity::PrivatePdaOwned(account_id) => { + let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?; + State::Private(pre) + } AccountIdentity::PrivatePdaForeign { account_id, npk, vpk, identifier, - } => State::Private(private_pda_foreign_acc_preparation( - account_id, npk, vpk, identifier, - )), + } => { + let acc = lee_core::account::Account::default(); + let auth_acc = AccountWithMetadata::new(acc, false, account_id); + let mut random_seed: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut random_seed); + let pre = AccountPreparedData { + nsk: None, + npk, + identifier, + vpk, + pre_state: auth_acc, + proof: None, + random_seed, + is_pda: true, + }; + State::Private(pre) + } AccountIdentity::PrivateShared { nsk, npk, vpk, identifier, } => { - let account_id = lee::AccountId::from((&npk, identifier)); - State::Private( - private_shared_acc_preparation( - wallet, account_id, nsk, npk, vpk, identifier, false, - ) - .await?, + let account_id = lee::AccountId::from((&npk, &vpk, identifier)); + let pre = private_shared_acc_preparation( + wallet, account_id, nsk, npk, vpk, identifier, false, ) + .await?; + + State::Private(pre) } AccountIdentity::PrivatePdaShared { account_id, @@ -266,12 +324,14 @@ impl AccountManager { npk, vpk, identifier, - } => State::Private( - private_shared_acc_preparation( + } => { + let pre = private_shared_acc_preparation( wallet, account_id, nsk, npk, vpk, identifier, true, ) - .await?, - ), + .await?; + + State::Private(pre) + } }; states.push(state); @@ -327,9 +387,26 @@ impl AccountManager { self.states .iter() .filter_map(|state| match state { - State::Private(pre) => Some(PrivateAccountKeys { ssk: pre.ssk }), + State::Private(pre) => Some(pre), State::Public { .. } | State::PublicKeycard { .. } => None, }) + .map(|pre| { + let nonce = if pre.proof.is_some() { + pre.pre_state.account.nonce.private_account_nonce_increment( + pre.nsk.as_ref().expect("update variant must have nsk"), + ) + } else { + lee_core::account::Nonce::private_account_nonce_init(&pre.pre_state.account_id) + }; + let esk = lee_core::EphemeralSecretKey::new( + &pre.pre_state.account_id, + &pre.random_seed, + &nonce, + ); + PrivateAccountKeys { + ssk: SharedSecretKey::encapsulate_deterministic(&pre.vpk, &esk).0, + } + }) .collect() } @@ -344,19 +421,17 @@ impl AccountManager { 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 { - epk: pre.epk.clone(), - view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), - ssk: pre.ssk, + vpk: pre.vpk.clone(), + random_seed: pre.random_seed, nsk, membership_proof, identifier: pre.identifier, seed: None, }, _ => InputAccountIdentity::PrivatePdaInit { - epk: pre.epk.clone(), - view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), + vpk: pre.vpk.clone(), + random_seed: pre.random_seed, npk: pre.npk, - ssk: pre.ssk, identifier: pre.identifier, commitment_root: self.dummy_commitment_root, seed: None, @@ -365,27 +440,24 @@ impl AccountManager { State::Private(pre) => match (pre.nsk, pre.proof.clone()) { (Some(nsk), Some(membership_proof)) => { InputAccountIdentity::PrivateAuthorizedUpdate { - epk: pre.epk.clone(), - view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), - ssk: pre.ssk, + vpk: pre.vpk.clone(), + random_seed: pre.random_seed, nsk, membership_proof, identifier: pre.identifier, } } (Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit { - epk: pre.epk.clone(), - view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), - ssk: pre.ssk, + vpk: pre.vpk.clone(), + random_seed: pre.random_seed, nsk, identifier: pre.identifier, commitment_root: self.dummy_commitment_root, }, (None, _) => InputAccountIdentity::PrivateUnauthorized { - epk: pre.epk.clone(), - view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), + vpk: pre.vpk.clone(), + random_seed: pre.random_seed, npk: pre.npk, - ssk: pre.ssk, identifier: pre.identifier, commitment_root: self.dummy_commitment_root, }, @@ -462,49 +534,12 @@ struct AccountPreparedData { vpk: ViewingPublicKey, pre_state: AccountWithMetadata, proof: Option, - /// Cached shared-secret key derived once at `AccountManager::new`. Reused for both the - /// circuit input variant (`account_identities()`) and the message ephemeral-key tuples - /// (`private_account_keys()`), so all consumers see the same key. The corresponding - /// `EphemeralKeyHolder` uses `OsRng` and would produce a different value on a second call. - ssk: SharedSecretKey, - /// Cached ephemeral public key, paired with `ssk`. - epk: EphemeralPublicKey, + 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. is_pda: bool, } -async fn prepare_public_state( - wallet: &WalletCore, - account_id: AccountId, - lookup_signing_key: bool, -) -> Result { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - let sk = if lookup_signing_key { - wallet.get_account_public_signing_key(account_id).cloned() - } else { - None - }; - let account = AccountWithMetadata::new(acc, sk.is_some(), account_id); - Ok(State::Public { account, sk }) -} - -async fn prepare_public_keycard_state( - wallet: &WalletCore, - account_id: AccountId, - key_path: String, -) -> Result { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - let account = AccountWithMetadata::new(acc, true, account_id); - Ok(State::PublicKeycard { account, key_path }) -} - async fn private_key_tree_acc_preparation( wallet: &WalletCore, account_id: AccountId, @@ -530,9 +565,8 @@ async fn private_key_tree_acc_preparation( // support from that in the wallet. let sender_pre = AccountWithMetadata::new(from_acc.account.clone(), true, account_id); - let eph_holder = EphemeralKeyHolder::new(&from_vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); + let mut random_seed: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut random_seed); Ok(AccountPreparedData { nsk: Some(nsk), @@ -541,8 +575,7 @@ async fn private_key_tree_acc_preparation( vpk: from_vpk, pre_state: sender_pre, proof, - ssk, - epk, + random_seed, is_pda, }) } @@ -570,9 +603,8 @@ async fn private_shared_acc_preparation( .await .unwrap_or(None); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); + let mut random_seed: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut random_seed); Ok(AccountPreparedData { nsk: Some(nsk), @@ -581,61 +613,11 @@ async fn private_shared_acc_preparation( vpk, pre_state, proof, - ssk, - epk, + random_seed, is_pda, }) } -fn private_foreign_acc_preparation( - npk: NullifierPublicKey, - vpk: ViewingPublicKey, - identifier: Identifier, -) -> AccountPreparedData { - let acc = lee_core::account::Account::default(); - let pre_state = AccountWithMetadata::new(acc, false, (&npk, identifier)); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); - - AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state, - proof: None, - ssk, - epk, - is_pda: false, - } -} - -fn private_pda_foreign_acc_preparation( - account_id: AccountId, - npk: NullifierPublicKey, - vpk: ViewingPublicKey, - identifier: Identifier, -) -> AccountPreparedData { - let acc = lee_core::account::Account::default(); - let pre_state = AccountWithMetadata::new(acc, false, account_id); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); - - AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state, - proof: None, - ssk, - epk, - is_pda: true, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/lez/wallet/src/cli/account.rs b/lez/wallet/src/cli/account.rs index b49cfb25..275e13fc 100644 --- a/lez/wallet/src/cli/account.rs +++ b/lez/wallet/src/cli/account.rs @@ -581,8 +581,11 @@ impl WalletSubcommand for ImportSubcommand { let key_chain: KeyChain = serde_json::from_str(&key_chain_json) .map_err(|err| anyhow::anyhow!("Invalid key chain JSON: {err}"))?; let account = lee::Account::from(account_state); - let account_id = - lee::AccountId::from((&key_chain.nullifier_public_key, identifier)); + let account_id = lee::AccountId::from(( + &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, + identifier, + )); wallet_core .storage_mut() diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 3f7a900d..0b49e726 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -369,7 +369,7 @@ impl WalletCore { let keys = holder.derive_keys_for_pda(&program_id, &pda_seed); let npk = keys.generate_nullifier_public_key(); let vpk = keys.generate_viewing_public_key(); - let account_id = AccountId::for_private_pda(&program_id, &pda_seed, &npk, identifier); + let account_id = AccountId::for_private_pda(&program_id, &pda_seed, &npk, &vpk, identifier); self.register_shared_account( account_id, @@ -403,7 +403,7 @@ impl WalletCore { let keys = holder.derive_regular_shared_account_keys_from_identifier(identifier); let npk = keys.generate_nullifier_public_key(); let vpk = keys.generate_viewing_public_key(); - let account_id = AccountId::from((&npk, identifier)); + let account_id = AccountId::from((&npk, &vpk, identifier)); self.register_shared_account(account_id, group_name, identifier, None, None); @@ -803,7 +803,11 @@ impl WalletCore { ) .map(|(kind, res_acc)| { let npk = &key_chain.nullifier_public_key; - let account_id = lee::AccountId::for_private_account(npk, &kind); + let account_id = lee::AccountId::for_private_account( + npk, + &key_chain.viewing_public_key, + &kind, + ); let nsk = key_chain.private_key_holder.nullifier_secret_key; (account_id, kind, res_acc, nsk) }) diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 51b3762c..72dcb78f 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -212,7 +212,8 @@ impl UserKeyChain { .1 .first_key_value() .expect("Newly created key chain node must have at least one account"); - let account_id = AccountId::for_private_account(&npk, kind); + let account_id = + AccountId::for_private_account(&npk, &node.value.0.viewing_public_key, kind); (account_id, chain_index) } @@ -248,8 +249,11 @@ impl UserKeyChain { #[must_use] pub fn private_account(&self, account_id: AccountId) -> Option> { self.private_accounts().find_map(|found| { - let expected_id = - AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind); + let expected_id = AccountId::for_private_account( + &found.key_chain.nullifier_public_key, + &found.key_chain.viewing_public_key, + found.kind, + ); (expected_id == account_id).then_some(found) }) } @@ -305,8 +309,11 @@ impl UserKeyChain { .iter() .flat_map(|(key, data)| { data.accounts.keys().map(|kind| { - let account_id = - AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind); + let account_id = AccountId::for_private_account( + &key.key_chain.nullifier_public_key, + &key.key_chain.viewing_public_key, + kind, + ); (account_id, &key.key_chain, key.chain_index.as_ref()) }) }) @@ -346,8 +353,11 @@ impl UserKeyChain { // For each (regular) found account the user owns, compute its nullifier and put // into the map. This is the next nullifier it will look for. for found in self.private_accounts() { - let account_id = - AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind); + let account_id = AccountId::for_private_account( + &found.key_chain.nullifier_public_key, + &found.key_chain.viewing_public_key, + found.kind, + ); let nsk = found.key_chain.private_key_holder.nullifier_secret_key; index.track(account_id, found.account, &nsk); } @@ -496,8 +506,11 @@ impl UserKeyChain { // Then try to update imported account for (key, data) in &mut self.imported_private_accounts { for (kind, imported_account) in &mut data.accounts { - let expected_id = - AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind); + let expected_id = AccountId::for_private_account( + &key.key_chain.nullifier_public_key, + &key.key_chain.viewing_public_key, + kind, + ); if expected_id == account_id { debug!("Updating imported private account {account_id}"); *imported_account = account; @@ -534,8 +547,11 @@ impl UserKeyChain { // Node not yet in account_id_map — find it by checking all nodes for (ci, node) in &mut self.private_key_tree.key_map { - let expected_id = - lee::AccountId::for_private_account(&node.value.0.nullifier_public_key, &kind); + let expected_id = lee::AccountId::for_private_account( + &node.value.0.nullifier_public_key, + &node.value.0.viewing_public_key, + &kind, + ); if expected_id == account_id { match node.value.1.entry(kind) { Entry::Occupied(mut occupied) => { @@ -585,8 +601,11 @@ impl UserKeyChain { .iter() .flat_map(|(key, data)| { data.accounts.keys().map(|kind| { - let account_id = - AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind); + let account_id = AccountId::for_private_account( + &key.key_chain.nullifier_public_key, + &key.key_chain.viewing_public_key, + kind, + ); (account_id, key.chain_index.as_ref()) }) }) @@ -851,6 +870,7 @@ mod tests { let identifier = 0; let account_id = AccountId::for_private_account( &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, &PrivateAccountKind::Regular(identifier), ); @@ -913,7 +933,7 @@ mod tests { let npk = keys.generate_nullifier_public_key(); let vpk = keys.generate_viewing_public_key(); let nsk = keys.nullifier_secret_key; - let account_id = AccountId::from((&npk, identifier)); + let account_id = AccountId::from((&npk, &vpk, identifier)); kc.insert_group_key_holder(label.clone(), holder); let old_account = Account::default(); @@ -975,6 +995,7 @@ mod tests { let identifier = 0; let account_id = AccountId::for_private_account( &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, &PrivateAccountKind::Regular(identifier), ); let account = Account::default(); @@ -1032,7 +1053,11 @@ mod tests { let mut user_data = UserKeyChain::default(); let key_chain = KeyChain::new_os_random(); - let account_id = AccountId::from((&key_chain.nullifier_public_key, 0)); + let account_id = AccountId::from(( + &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, + 0, + )); let account = lee_core::account::Account::default(); user_data.add_imported_private_account(key_chain, None, 0, account); @@ -1047,7 +1072,11 @@ mod tests { let mut user_data = UserKeyChain::default(); let key_chain = KeyChain::new_os_random(); - let account_id = AccountId::from((&key_chain.nullifier_public_key, 0)); + let account_id = AccountId::from(( + &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, + 0, + )); let account = lee_core::account::Account::default(); user_data.add_imported_private_account(key_chain, None, 0, account.clone()); @@ -1092,7 +1121,11 @@ mod tests { let mut user_data = UserKeyChain::default(); let key_chain = KeyChain::new_os_random(); - let account_id = AccountId::from((&key_chain.nullifier_public_key, 0)); + let account_id = AccountId::from(( + &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, + 0, + )); let new_account = lee_core::account::Account { balance: 100, @@ -1113,7 +1146,11 @@ mod tests { let mut user_data = UserKeyChain::default(); let key_chain = KeyChain::new_os_random(); - let account_id1 = AccountId::from((&key_chain.nullifier_public_key, 0)); + let account_id1 = AccountId::from(( + &key_chain.nullifier_public_key, + &key_chain.viewing_public_key, + 0, + )); let account = lee_core::account::Account::default(); user_data.add_imported_private_account(key_chain, None, 0, account); diff --git a/test_fixtures/src/config.rs b/test_fixtures/src/config.rs index 8684dd4d..92ed6a18 100644 --- a/test_fixtures/src/config.rs +++ b/test_fixtures/src/config.rs @@ -23,7 +23,11 @@ pub struct InitialPrivateAccountForWallet { impl InitialPrivateAccountForWallet { #[must_use] pub fn account_id(&self) -> AccountId { - AccountId::from((&self.key_chain.nullifier_public_key, self.identifier)) + AccountId::from(( + &self.key_chain.nullifier_public_key, + &self.key_chain.viewing_public_key, + self.identifier, + )) } } diff --git a/tools/crypto_primitives_bench/Cargo.toml b/tools/crypto_primitives_bench/Cargo.toml index 780fac77..30d38681 100644 --- a/tools/crypto_primitives_bench/Cargo.toml +++ b/tools/crypto_primitives_bench/Cargo.toml @@ -11,7 +11,6 @@ workspace = true [dev-dependencies] key_protocol.workspace = true lee_core = { workspace = true, features = ["host"] } -rand = { workspace = true } criterion.workspace = true [[bench]] diff --git a/tools/crypto_primitives_bench/benches/primitives.rs b/tools/crypto_primitives_bench/benches/primitives.rs index 11c11d9b..9a42305c 100644 --- a/tools/crypto_primitives_bench/benches/primitives.rs +++ b/tools/crypto_primitives_bench/benches/primitives.rs @@ -48,7 +48,8 @@ fn bench_encryption(c: &mut Criterion) { let recipient_kc = KeyChain::new_os_random(); let npk = recipient_kc.nullifier_public_key; let account = Account::default(); - let account_id = AccountId::for_regular_private_account(&npk, 0); + let account_id = + AccountId::for_regular_private_account(&npk, &recipient_kc.viewing_public_key, 0); let commitment = Commitment::new(&account_id, &account); let (shared, _epk) = SharedSecretKey::encapsulate(&recipient_kc.viewing_public_key); let kind = PrivateAccountKind::Regular(0_u128);