From 30822a890550a2f6891935497455b05c3c0fef13 Mon Sep 17 00:00:00 2001 From: Marvin Jones Date: Fri, 29 May 2026 19:54:53 -0400 Subject: [PATCH] addressed comments --- .../token-transfer.md | 34 +++--- integration_tests/tests/amm.rs | 4 + integration_tests/tests/ata.rs | 3 + .../tests/auth_transfer/private.rs | 30 ++++-- .../tests/auth_transfer/public.rs | 7 ++ integration_tests/tests/keys.rs | 11 +- integration_tests/tests/shared_accounts.rs | 5 +- integration_tests/tests/token.rs | 12 ++- .../src/key_management/group_key_holder.rs | 24 +++-- .../key_management/key_tree/keys_private.rs | 26 ++--- lee/key_protocol/src/key_management/mod.rs | 21 ++++ .../src/key_management/secret_holders.rs | 32 +++--- lee/state_machine/Cargo.toml | 1 + lee/state_machine/core/Cargo.toml | 1 + lee/state_machine/core/src/encryption/mod.rs | 2 +- .../src/encryption/shared_key_derivation.rs | 101 +++++++++++------- lez/testnet_initial_state/src/lib.rs | 10 +- lez/wallet/src/cli/account.rs | 35 +++++- lez/wallet/src/cli/mod.rs | 99 +++++++++++++++++ .../src/cli/programs/native_token_transfer.rs | 33 ++++-- lez/wallet/src/cli/programs/token.rs | 62 ++++++++--- lez/wallet/src/lib.rs | 4 +- tools/integration_bench/src/scenarios/amm.rs | 1 + .../integration_bench/src/scenarios/fanout.rs | 1 + .../src/scenarios/parallel.rs | 2 + .../src/scenarios/private.rs | 3 + .../integration_bench/src/scenarios/token.rs | 2 + 27 files changed, 438 insertions(+), 128 deletions(-) diff --git a/docs/LEZ testnet v0.1 tutorials/token-transfer.md b/docs/LEZ testnet v0.1 tutorials/token-transfer.md index 3a1ef43f..e3d04663 100644 --- a/docs/LEZ testnet v0.1 tutorials/token-transfer.md +++ b/docs/LEZ testnet v0.1 tutorials/token-transfer.md @@ -155,7 +155,7 @@ wallet account new private # Output: Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951 -With vpk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17 +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` > [!Tip] @@ -231,19 +231,29 @@ wallet account new private-accounts-key # Output: Generated new private accounts key at path /1 With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e -With vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` -> [!Tip] -> Ignore the account ID here and use the `npk` and `vpk` values to send to a foreign private account. +> [!Important] +> The VPK is now a 1184-byte ML-KEM-768 encapsulation key — too large to copy-paste into a command. +> The recommended workflow is: +> +> **Recipient:** export both keys to a single file and send the file to the sender (e.g. as an email attachment): +> ```bash +> wallet account show-keys --account-id Private/ > recipient.keys +> # Send recipient.keys to the sender out-of-band +> ``` +> The file contains two lines: the npk (hex) on line 1, the vpk (hex) on line 2. +> +> **Sender:** reference the received file with `--to-keys`: -### b. Send 3 tokens using the recipient’s npk and vpk +### b. Send 3 tokens using the recipient’s keys file ```bash +# The sender has received recipient.keys from the recipient out-of-band wallet auth-transfer send \ --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \ - --to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \ - --to-vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \ + --to-keys recipient.keys \ --amount 3 ``` @@ -270,18 +280,19 @@ wallet account new private-accounts-key # Output: Generated new private accounts key at path /2 With npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 -With vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` Alice shares the `npk` and `vpk` values with Bob and Charlie out of band. ### b. Bob sends 10 tokens to Alice using identifier 1 +Bob uses the received `alice.keys` file: + ```bash wallet auth-transfer send \ --from Public/BobXqJprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPA \ - --to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \ - --to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \ + --to-keys alice.keys \ --to-identifier 1 \ --amount 10 ``` @@ -291,8 +302,7 @@ wallet auth-transfer send \ ```bash wallet auth-transfer send \ --from Public/CharlieYrP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPB \ - --to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \ - --to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \ + --to-keys alice.keys \ --to-identifier 2 \ --amount 5 ``` diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index b7a747f1..9f953001 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -132,6 +132,7 @@ async fn amm_public() -> Result<()> { to: Some(public_mention(recipient_account_id_1)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 7, }; @@ -158,6 +159,7 @@ async fn amm_public() -> Result<()> { to: Some(public_mention(recipient_account_id_2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 7, }; @@ -530,6 +532,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to: Some(public_mention(holding_a_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 5, }; @@ -551,6 +554,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to: Some(public_mention(holding_b_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 5, }; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 5b4a6c0b..9e37061b 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -260,6 +260,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> { to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), @@ -487,6 +488,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> { to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), @@ -598,6 +600,7 @@ async fn burn_via_ata_private_owner() -> Result<()> { to: Some(public_mention(holder_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 75be6d06..98e641a5 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -15,6 +15,14 @@ use lee_core::{ encryption::{EphemeralPublicKey, ViewingPublicKey}, }; use log::info; +<<<<<<< HEAD +======= +use nssa::{AccountId, program::Program}; +use nssa_core::{ + NullifierPublicKey, + encryption::{MlKem768EncapsulationKey, ViewingPublicKey}, +}; +>>>>>>> f4315d18 (addressed comments) use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::{ @@ -38,6 +46,7 @@ async fn private_transfer_to_owned_account() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -77,7 +86,8 @@ async fn private_transfer_to_foreign_account() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(to_npk_string), - to_vpk: Some(hex::encode(to_vpk.0)), + to_vpk: Some(hex::encode(to_vpk.to_bytes())), + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -127,6 +137,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { to: Some(public_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -189,7 +200,8 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())), + to_keys: None, to_identifier: Some(to.kind.identifier()), amount: 100, }); @@ -239,6 +251,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -281,7 +294,8 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { from: public_mention(from), to: None, to_npk: Some(to_npk_string), - to_vpk: Some(hex::encode(to_vpk.0)), + to_vpk: Some(hex::encode(to_vpk.to_bytes())), + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -351,7 +365,8 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())), + to_keys: None, to_identifier: Some(to.kind.identifier()), amount: 100, }); @@ -452,6 +467,7 @@ async fn private_transfer_using_from_label() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -545,7 +561,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { }; let npk_hex = hex::encode(npk.0); - let vpk_hex = hex::encode(vpk.0); + let vpk_hex = hex::encode(vpk.to_bytes()); let identifier_1 = 1_u128; let identifier_2 = 2_u128; @@ -560,6 +576,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { to: None, to_npk: Some(npk_hex.clone()), to_vpk: Some(vpk_hex.clone()), + to_keys: None, to_identifier: Some(identifier_1), amount: 100, }), @@ -573,6 +590,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { to: None, to_npk: Some(npk_hex), to_vpk: Some(vpk_hex), + to_keys: None, to_identifier: Some(identifier_2), amount: 200, }), @@ -654,7 +672,7 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { let auth_transfer_program_id = Program::authenticated_transfer_program().id(); let nsk: lee_core::NullifierSecretKey = [3; 32]; let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey(vec![4_u8; 1184]); + let vpk = MlKem768EncapsulationKey::from_bytes(vec![4_u8; 1184]).unwrap(); let ssk = SharedSecretKey([55_u8; 32]); let epk = EphemeralPublicKey(vec![55_u8; 1088]); let attacker_vault_id = { diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index cd19e55d..d00b7964 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -25,6 +25,7 @@ async fn successful_transfer_to_existing_account() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -83,6 +84,7 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { to: Some(public_mention(new_persistent_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -120,6 +122,7 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000_000, }); @@ -159,6 +162,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -192,6 +196,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -274,6 +279,7 @@ async fn successful_transfer_using_from_label() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -319,6 +325,7 @@ async fn successful_transfer_using_to_label() -> Result<()> { to: Some(CliAccountMention::Label(label)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); diff --git a/integration_tests/tests/keys.rs b/integration_tests/tests/keys.rs index 59628798..01af23cd 100644 --- a/integration_tests/tests/keys.rs +++ b/integration_tests/tests/keys.rs @@ -71,7 +71,10 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to_account.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to_account.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode( + to_account.key_chain.viewing_public_key.to_bytes(), + )), + to_keys: None, to_identifier: Some(to_account.kind.identifier()), amount: 100, }); @@ -147,6 +150,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id1)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -158,6 +162,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 101, }); @@ -197,6 +202,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id3)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 102, }); @@ -208,6 +214,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 103, }); @@ -268,6 +275,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 10, }); @@ -278,6 +286,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 11, }); diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs index 7dbe8d2a..48207977 100644 --- a/integration_tests/tests/shared_accounts.rs +++ b/integration_tests/tests/shared_accounts.rs @@ -108,7 +108,9 @@ async fn group_invite_join_key_agreement() -> Result<()> { .sealing_secret_key() .context("Sealing key not found")?; let sealing_pk = key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes( - nssa_core::encryption::ViewingPublicKey::from_seed(&sealing_sk.d, &sealing_sk.z).0, + nssa_core::encryption::ViewingPublicKey::from_seed(&sealing_sk.d, &sealing_sk.z) + .to_bytes() + .to_vec(), ); let holder = ctx @@ -205,6 +207,7 @@ async fn fund_shared_account_from_public() -> Result<()> { to: Some(private_mention(shared_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: None, amount: 100, }); diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index a5bd0ac9..b0c569e8 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -133,6 +133,7 @@ async fn create_and_transfer_public_token() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -223,6 +224,7 @@ async fn create_and_transfer_public_token() -> Result<()> { holder: Some(public_mention(recipient_account_id)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount, }; @@ -365,6 +367,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -554,6 +557,7 @@ async fn create_token_with_private_definition() -> Result<()> { holder: Some(public_mention(recipient_account_id_public)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount_public, }; @@ -601,6 +605,7 @@ async fn create_token_with_private_definition() -> Result<()> { holder: Some(private_mention(recipient_account_id_private)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount_private, }; @@ -740,6 +745,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -868,6 +874,7 @@ async fn shielded_token_transfer() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -991,6 +998,7 @@ async fn deshielded_token_transfer() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -1124,7 +1132,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { definition: private_mention(definition_account_id), holder: None, holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)), - holder_vpk: Some(hex::encode(&holder_keys.viewing_public_key.0)), + holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.to_bytes())), + holder_keys: None, holder_identifier: Some(holder_identifier), amount: mint_amount, }; @@ -1323,6 +1332,7 @@ async fn transfer_token_using_from_label() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; 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 b25f39c8..09b828d0 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -1,7 +1,7 @@ use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; use lee_core::{ SharedSecretKey, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::{EphemeralPublicKey, MlKem768EncapsulationKey}, program::{PdaSeed, ProgramId}, }; use rand::{RngCore as _, rngs::OsRng}; @@ -156,8 +156,9 @@ impl GroupKeyHolder { /// different ciphertexts. #[must_use] pub fn seal_for(&self, recipient_key: &SealingPublicKey) -> Vec { - let vpk = ViewingPublicKey(recipient_key.0.clone()); - let (shared, kem_ct) = SharedSecretKey::encapsulate(&vpk); + let sealing_key = MlKem768EncapsulationKey::from_bytes(recipient_key.0.clone()) + .expect("key_protocol::group_key_holder::GroupKeyHolder::seal_for: SealingPublicKey must be a valid ML-KEM-768 encapsulation key"); + let (shared, kem_ct) = SharedSecretKey::encapsulate(&sealing_key); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -404,7 +405,9 @@ mod tests { let recipient_vpk = recipient_keys.generate_viewing_public_key(); let recipient_vsk = recipient_keys.viewing_secret_key; - let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal"); assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms()); @@ -434,7 +437,9 @@ mod tests { .produce_private_key_holder(None) .viewing_secret_key; - let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } @@ -449,7 +454,9 @@ mod tests { let recipient_vpk = recipient_keys.generate_viewing_public_key(); let recipient_vsk = recipient_keys.viewing_secret_key; - let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0)); + let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); // Flip a byte in the AES-GCM ciphertext portion (after KEM ciphertext + nonce). let last = sealed.len() - 1; sealed[last] ^= 0xFF; @@ -468,7 +475,7 @@ mod tests { .produce_private_key_holder(None) .generate_viewing_public_key(); - let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.0); + let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.to_bytes().to_vec()); let sealed_a = holder.seal_for(&sealing_key); let sealed_b = holder.seal_for(&sealing_key); assert_ne!(sealed_a, sealed_b); @@ -531,7 +538,8 @@ mod tests { let bob_vpk = bob_keys.generate_viewing_public_key(); let bob_vsk = bob_keys.viewing_secret_key; - let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0)); + let sealed = + alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.to_bytes().to_vec())); let bob_holder = GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS"); 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 7b5df88c..ec4aa6de 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 @@ -38,7 +38,7 @@ impl ChildKeysPrivate { let vsk = ssk.generate_viewing_secret_seed_key(None); let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from(&vsk); + let vpk = MlKem768EncapsulationKey::from(&vsk); Self { value: ( @@ -60,15 +60,17 @@ impl ChildKeysPrivate { #[must_use] pub fn nth_child(&self, cci: u32) -> Self { + // `parent_hash`` is used to incorporate entropy based on the parent node's keys + // to generate the `ssk` and `ccc` values. let mut parent_hash = sha2::Sha256::new(); parent_hash.update(b"LEE/keys"); - parent_hash.update([0_u8; 16]); - parent_hash.update([9_u8]); parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key); parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.d); parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.z); let parent_pt = parent_hash.finalize(); + // Each child (of the same parent node) share the same `parent_pt`. + // To ensure that each child generates unique keys, we include the child index. let mut input = vec![]; input.extend_from_slice(b"LEE_seed_priv"); input.extend_from_slice(&parent_pt); @@ -90,7 +92,7 @@ impl ChildKeysPrivate { let vsk = ssk.generate_viewing_secret_seed_key(Some(cci)); let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from(&vsk); + let vpk = MlKem768EncapsulationKey::from(&vsk); Self { value: ( @@ -167,16 +169,16 @@ mod tests { 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2, ]); - let expected_vsk: ViewingSecretKey = ViewingSecretKey { - d: [ + let expected_vsk = ViewingSecretKey::new( + [ 187, 143, 146, 12, 68, 148, 25, 203, 21, 92, 131, 2, 221, 81, 117, 62, 98, 194, 159, 177, 102, 254, 236, 182, 76, 242, 116, 219, 17, 166, 99, 36, ], - z: [ + [ 80, 97, 83, 209, 145, 99, 168, 99, 89, 29, 153, 236, 82, 99, 134, 114, 168, 19, 223, 69, 34, 47, 76, 76, 15, 97, 245, 184, 25, 103, 251, 82, ], - }; + ); let expected_vpk: [u8; 1184] = [ 127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220, @@ -281,16 +283,16 @@ mod tests { 219, 114, 113, 16, 42, 27, 220, 96, 151, 124, 8, 65, ]); - let expected_vsk: ViewingSecretKey = ViewingSecretKey { - d: [ + let expected_vsk = ViewingSecretKey::new( + [ 81, 154, 68, 152, 72, 163, 82, 17, 125, 156, 193, 135, 129, 93, 227, 55, 224, 104, 119, 232, 13, 101, 241, 20, 175, 72, 192, 186, 176, 246, 140, 211, ], - z: [ + [ 31, 40, 109, 41, 185, 61, 173, 79, 102, 171, 158, 245, 232, 71, 57, 157, 142, 117, 184, 235, 216, 71, 55, 44, 33, 156, 167, 133, 184, 92, 47, 174, ], - }; + ); let expected_vpk: [u8; 1184] = [ 67, 150, 145, 133, 41, 124, 194, 102, 104, 131, 195, 8, 168, 170, 200, 40, 210, 84, 85, diff --git a/lee/key_protocol/src/key_management/mod.rs b/lee/key_protocol/src/key_management/mod.rs index a24745f3..459badf0 100644 --- a/lee/key_protocol/src/key_management/mod.rs +++ b/lee/key_protocol/src/key_management/mod.rs @@ -106,6 +106,27 @@ mod tests { let _shared_secret = account_id_key_holder.calculate_shared_secret_receiver(&epk); } + #[test] + fn calculate_shared_secret_receiver_returns_none_for_malformed_epk() { + let key_chain = KeyChain::new_os_random(); + + let short_epk = EphemeralPublicKey(vec![42_u8; 100]); + assert!( + key_chain + .calculate_shared_secret_receiver(&short_epk) + .is_none(), + "short EphemeralPublicKey must return None" + ); + + let long_epk = EphemeralPublicKey(vec![42_u8; 1089]); + assert!( + key_chain + .calculate_shared_secret_receiver(&long_epk) + .is_none(), + "long EphemeralPublicKey must return None" + ); + } + #[test] fn key_generation_test() { let seed_holder = SeedHolder::new_os_random(); diff --git a/lee/key_protocol/src/key_management/secret_holders.rs b/lee/key_protocol/src/key_management/secret_holders.rs index 29ddcd91..0fafbdb8 100644 --- a/lee/key_protocol/src/key_management/secret_holders.rs +++ b/lee/key_protocol/src/key_management/secret_holders.rs @@ -28,6 +28,13 @@ pub struct ViewingSecretKey { pub z: [u8; 32], } +impl ViewingSecretKey { + #[must_use] + pub const fn new(d: [u8; 32], z: [u8; 32]) -> Self { + Self { d, z } + } +} + /// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret /// for recepient. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -142,22 +149,22 @@ impl SecretSpendingKey { let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed"); - ViewingSecretKey { - d: *full_seed + ViewingSecretKey::new( + *full_seed .first_chunk::<32>() .expect("hash_value is 64 bytes, must be safe to get first 32"), - z: *full_seed + *full_seed .last_chunk::<32>() .expect("hash_value is 64 bytes, must be safe to get last 32"), - } + ) } #[must_use] pub const fn generate_viewing_secret_key(seed: [u8; 64]) -> ViewingSecretKey { - ViewingSecretKey { - d: *seed.first_chunk::<32>().expect("seed is 64 bytes"), - z: *seed.last_chunk::<32>().expect("seed is 64 bytes"), - } + ViewingSecretKey::new( + *seed.first_chunk::<32>().expect("seed is 64 bytes"), + *seed.last_chunk::<32>().expect("seed is 64 bytes"), + ) } #[must_use] @@ -169,14 +176,15 @@ impl SecretSpendingKey { } } -impl From<&ViewingSecretKey> for ViewingPublicKey { +impl From<&ViewingSecretKey> for MlKem768EncapsulationKey { fn from(sk: &ViewingSecretKey) -> Self { use ml_kem::{Kem, KeyExport as _, MlKem768, Seed}; let mut seed_bytes = [0_u8; 64]; seed_bytes[..32].copy_from_slice(&sk.d); seed_bytes[32..].copy_from_slice(&sk.z); let dk = ::DecapsulationKey::from_seed(Seed::from(seed_bytes)); - Self(dk.encapsulation_key().to_bytes().to_vec()) + Self::from_bytes(dk.encapsulation_key().to_bytes().to_vec()) + .expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always 1184 bytes") } } @@ -187,8 +195,8 @@ impl PrivateKeyHolder { } #[must_use] - pub fn generate_viewing_public_key(&self) -> ViewingPublicKey { - ViewingPublicKey::from(&self.viewing_secret_key) + pub fn generate_viewing_public_key(&self) -> MlKem768EncapsulationKey { + MlKem768EncapsulationKey::from(&self.viewing_secret_key) } } diff --git a/lee/state_machine/Cargo.toml b/lee/state_machine/Cargo.toml index 194c402f..7b62d188 100644 --- a/lee/state_machine/Cargo.toml +++ b/lee/state_machine/Cargo.toml @@ -31,6 +31,7 @@ risc0-build = "3.0.3" risc0-binfmt = "3.0.2" [dev-dependencies] +nssa_core = { workspace = true, features = ["test_utils"] } token_core.workspace = true authenticated_transfer_core.workspace = true test_program_methods.workspace = true diff --git a/lee/state_machine/core/Cargo.toml b/lee/state_machine/core/Cargo.toml index 04fd2dc9..6e1f0ff0 100644 --- a/lee/state_machine/core/Cargo.toml +++ b/lee/state_machine/core/Cargo.toml @@ -25,3 +25,4 @@ serde_json.workspace = true [features] default = [] host = ["dep:ml-kem"] +test_utils = ["host"] diff --git a/lee/state_machine/core/src/encryption/mod.rs b/lee/state_machine/core/src/encryption/mod.rs index 4dca476b..37745d4f 100644 --- a/lee/state_machine/core/src/encryption/mod.rs +++ b/lee/state_machine/core/src/encryption/mod.rs @@ -6,7 +6,7 @@ use chacha20::{ use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] -pub use shared_key_derivation::{EphemeralPublicKey, ViewingPublicKey}; +pub use shared_key_derivation::{EphemeralPublicKey, MlKem768EncapsulationKey, ViewingPublicKey}; use crate::{Commitment, account::Account, program::PrivateAccountKind}; #[cfg(feature = "host")] 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 9eb2fcc3..0e8f0f94 100644 --- a/lee/state_machine/core/src/encryption/shared_key_derivation.rs +++ b/lee/state_machine/core/src/encryption/shared_key_derivation.rs @@ -22,17 +22,32 @@ pub struct EphemeralPublicKey(pub Vec); BorshSerialize, BorshDeserialize, )] -pub struct ViewingPublicKey(pub Vec); +pub struct MlKem768EncapsulationKey(Vec); + +pub type ViewingPublicKey = MlKem768EncapsulationKey; + +impl MlKem768EncapsulationKey { + /// Expected byte length of an ML-KEM-768 encapsulation key. + pub const LEN: usize = 1184; + + /// Construct from raw bytes, returning an error if the length is not [`Self::LEN`]. + pub fn from_bytes(bytes: Vec) -> Result { + if bytes.len() != Self::LEN { + return Err(crate::error::NssaCoreError::DeserializationError(format!( + "MlKem768EncapsulationKey must be {} bytes, got {}", + Self::LEN, + bytes.len() + ))); + } + Ok(Self(bytes)) + } -impl ViewingPublicKey { #[must_use] pub fn to_bytes(&self) -> &[u8] { &self.0 } /// Derive the ML-KEM-768 encapsulation key from the FIPS 203 seed halves `d` and `z`. - /// Allows any crate to construct a VPK from raw seed bytes without importing - /// `key_protocol::ViewingSecretKey`. #[must_use] pub fn from_seed(d: &[u8; 32], z: &[u8; 32]) -> Self { let mut seed = Seed::default(); @@ -44,21 +59,21 @@ impl ViewingPublicKey { } impl SharedSecretKey { - /// Sender: encapsulate a fresh shared secret toward `vpk`. + /// Sender: encapsulate a fresh shared secret toward `ek`. /// /// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction /// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via - /// [`decapsulate`][Self::decapsulate]. + /// [`Self::decapsulate`]. #[must_use] - pub fn encapsulate(vpk: &ViewingPublicKey) -> (Self, EphemeralPublicKey) { - let ek_bytes: ml_kem::kem::Key = vpk - .0 - .as_slice() - .try_into() - .expect("ViewingPublicKey must be 1184 bytes (ML-KEM-768 encapsulation key)"); - let ek = ml_kem::EncapsulationKey768::new(&ek_bytes) - .expect("ViewingPublicKey bytes must encode a valid ML-KEM-768 encapsulation key"); - let (ct, ss) = ek.encapsulate(); + pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) { + let ek_bytes: ml_kem::kem::Key = + ek.0.as_slice() + .try_into() + .expect("MlKem768EncapsulationKey must be 1184 bytes"); + let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( + "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", + ); + let (ct, ss) = ek_obj.encapsulate(); let ss_bytes: [u8; 32] = ss .as_slice() .try_into() @@ -66,15 +81,19 @@ impl SharedSecretKey { (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) } - /// Sender: deterministically encapsulate a shared secret toward `vpk`. + /// Deterministically encapsulate a shared secret toward `ek` for use in tests. /// - /// The KEM randomness is derived as `SHA-256(message_hash || output_index_le)`, - /// making the ciphertext reproducible from the same `(vpk, message_hash, output_index)` - /// triple. Use a distinct `output_index` for each private account output in the same - /// transaction to ensure per-output EPK uniqueness. + /// 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"))] #[must_use] pub fn encapsulate_deterministic( - vpk: &ViewingPublicKey, + ek: &MlKem768EncapsulationKey, message_hash: &[u8; 32], output_index: u32, ) -> (Self, EphemeralPublicKey) { @@ -87,14 +106,14 @@ impl SharedSecretKey { let m: ml_kem::B32 = ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes"); - let ek_bytes: ml_kem::kem::Key = vpk - .0 - .as_slice() - .try_into() - .expect("ViewingPublicKey must be 1184 bytes (ML-KEM-768 encapsulation key)"); - let ek = ml_kem::EncapsulationKey768::new(&ek_bytes) - .expect("ViewingPublicKey bytes must encode a valid ML-KEM-768 encapsulation key"); - let (ct, ss) = ek.encapsulate_deterministic(&m); + let ek_bytes: ml_kem::kem::Key = + ek.0.as_slice() + .try_into() + .expect("MlKem768EncapsulationKey must be 1184 bytes"); + let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( + "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", + ); + let (ct, ss) = ek_obj.encapsulate_deterministic(&m); let ss_bytes: [u8; 32] = ss .as_slice() .try_into() @@ -109,7 +128,11 @@ impl SharedSecretKey { /// /// `d` and `z` are the two 32-byte halves of the FIPS 203 `ViewingSecretKey` seed. #[must_use] - pub fn decapsulate(ciphertext: &EphemeralPublicKey, d: &[u8; 32], z: &[u8; 32]) -> Option { + pub fn decapsulate( + ciphertext: &EphemeralPublicKey, + d: &[u8; 32], + z: &[u8; 32], + ) -> Option { let mut seed = Seed::default(); seed[..32].copy_from_slice(d); seed[32..].copy_from_slice(z); @@ -140,15 +163,15 @@ mod tests { let dk = ml_kem::DecapsulationKey768::from_seed(seed); let ek_bytes = dk.encapsulation_key().to_bytes(); - let vpk = ViewingPublicKey(ek_bytes.to_vec()); + let ek = MlKem768EncapsulationKey(ek_bytes.to_vec()); - let (sender_ss, epk) = SharedSecretKey::encapsulate(&vpk); + let (sender_ss, epk) = SharedSecretKey::encapsulate(&ek); let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap(); assert_eq!(sender_ss.0, receiver_ss.0, "shared secrets must match"); assert_eq!(epk.0.len(), 1088, "ML-KEM-768 ciphertext is 1088 bytes"); assert_eq!( - vpk.0.len(), + ek.0.len(), 1184, "ML-KEM-768 encapsulation key is 1184 bytes" ); @@ -186,23 +209,23 @@ mod tests { let (d1, z1) = ([1_u8; 32], [2_u8; 32]); let (d2, z2) = ([3_u8; 32], [4_u8; 32]); - let vpk1 = { + let ek1 = { let mut seed = Seed::default(); seed[..32].copy_from_slice(&d1); seed[32..].copy_from_slice(&z1); let dk = ml_kem::DecapsulationKey768::from_seed(seed); - ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec()) + MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec()) }; - let vpk2 = { + let ek2 = { let mut seed = Seed::default(); seed[..32].copy_from_slice(&d2); seed[32..].copy_from_slice(&z2); let dk = ml_kem::DecapsulationKey768::from_seed(seed); - ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec()) + MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec()) }; - let (ss1, _) = SharedSecretKey::encapsulate(&vpk1); - let (ss2, _) = SharedSecretKey::encapsulate(&vpk2); + let (ss1, _) = SharedSecretKey::encapsulate(&ek1); + let (ss2, _) = SharedSecretKey::encapsulate(&ek2); assert_ne!(ss1.0, ss2.0); } diff --git a/lez/testnet_initial_state/src/lib.rs b/lez/testnet_initial_state/src/lib.rs index 90e22bac..5bb6e1b4 100644 --- a/lez/testnet_initial_state/src/lib.rs +++ b/lez/testnet_initial_state/src/lib.rs @@ -136,10 +136,7 @@ pub fn initial_priv_accounts_private_keys() -> Vec Vec alice.keys + /// ``` + #[command(name = "show-keys")] + ShowKeys { + /// Either 32 byte base58 account id string with privacy prefix or a label. + #[arg(long)] + account_id: CliAccountMention, + }, } /// Represents generic register CLI subcommand. @@ -225,7 +239,7 @@ impl WalletSubcommand for NewSubcommand { println!("Shared account from group '{group}'"); println!("AccountId: Private/{}", info.account_id); println!("NPK: {}", hex::encode(info.npk.0)); - println!("VPK: {}", hex::encode(&info.vpk.0)); + println!("VPK: {}", hex::encode(info.vpk.to_bytes())); wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::RegisterAccount { @@ -419,6 +433,25 @@ impl WalletSubcommand for AccountSubcommand { Self::Import(import_subcommand) => { import_subcommand.handle_subcommand(wallet_core).await } + Self::ShowKeys { account_id } => { + let resolved = account_id.resolve(wallet_core.storage())?; + let AccountIdWithPrivacy::Private(account_id) = resolved else { + anyhow::bail!( + "wallet::cli::account::AccountSubcommand::ShowKeys: show-keys is only available for private accounts" + ); + }; + let entry = wallet_core + .storage() + .key_chain() + .private_account(account_id) + .ok_or_else(|| anyhow::anyhow!("wallet::cli::account::AccountSubcommand::ShowKeys: private account not found in wallet"))?; + println!("{}", hex::encode(entry.key_chain.nullifier_public_key.0)); + println!( + "{}", + hex::encode(entry.key_chain.viewing_public_key.to_bytes()) + ); + Ok(SubcommandReturnValue::Empty) + } } } } diff --git a/lez/wallet/src/cli/mod.rs b/lez/wallet/src/cli/mod.rs index c6e5ef3e..99b1f0b2 100644 --- a/lez/wallet/src/cli/mod.rs +++ b/lez/wallet/src/cli/mod.rs @@ -285,6 +285,31 @@ pub fn read_password_from_stdin() -> Result { Ok(password.trim().to_owned()) } +/// Parse a keys file exported by `wallet account show-keys`. +/// +/// The file format is two lines: +/// - Line 1: npk as hex (64 chars, 32 bytes). +/// - Line 2: vpk as hex (2368 chars, 1184 bytes). +/// +/// Returns `(npk_bytes, vpk_bytes)`. +pub fn read_keys_file(path: &str) -> Result<(Vec, Vec)> { + let content = std::fs::read_to_string(path).with_context(|| { + format!("wallet::cli::read_keys_file: failed to read keys file: {path}") + })?; + let mut lines = content.lines().filter(|l| !l.trim().is_empty()); + let npk_hex = lines.next().ok_or_else(|| { + anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing npk (line 1)") + })?; + let vpk_hex = lines.next().ok_or_else(|| { + anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing vpk (line 2)") + })?; + let npk = hex::decode(npk_hex.trim()) + .context("wallet::cli::read_keys_file: npk in keys file must be valid hex")?; + let vpk = hex::decode(vpk_hex.trim()) + .context("wallet::cli::read_keys_file: vpk in keys file must be valid hex")?; + Ok((npk, vpk)) +} + pub fn read_mnemonic_from_stdin() -> Result { let mut phrase = String::new(); @@ -328,3 +353,77 @@ pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_keys_file_roundtrip() { + let npk = [0xab_u8; 32]; + let vpk = [0xcd_u8; 1184]; + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.keys"); + + // Simulate what `wallet account show-keys` writes. + std::fs::write( + &path, + format!("{}\n{}\n", hex::encode(npk), hex::encode(vpk)), + ) + .unwrap(); + + let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap(); + + assert_eq!(parsed_npk, npk, "npk must round-trip through the keys file"); + assert_eq!( + parsed_vpk, + vpk.to_vec(), + "vpk must round-trip through the keys file" + ); + } + + #[test] + fn read_keys_file_missing_vpk_returns_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("incomplete.keys"); + std::fs::write(&path, format!("{}\n", hex::encode([0xab_u8; 32]))).unwrap(); + + let result = read_keys_file(path.to_str().unwrap()); + assert!(result.is_err(), "missing vpk line must return an error"); + assert!( + result.unwrap_err().to_string().contains("missing vpk"), + "error must mention missing vpk" + ); + } + + #[test] + fn read_keys_file_invalid_hex_returns_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("badhex.keys"); + std::fs::write(&path, "not-hex\nalso-not-hex\n").unwrap(); + + let result = read_keys_file(path.to_str().unwrap()); + assert!(result.is_err(), "invalid hex must return an error"); + } + + #[test] + fn read_keys_file_ignores_blank_lines() { + let npk = [0x11_u8; 32]; + let vpk = [0x22_u8; 1184]; + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("blanks.keys"); + + // Extra blank lines around the data should be tolerated. + std::fs::write( + &path, + format!("\n{}\n\n{}\n\n", hex::encode(npk), hex::encode(vpk)), + ) + .unwrap(); + + let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap(); + assert_eq!(parsed_npk, npk); + assert_eq!(parsed_vpk, vpk.to_vec()); + } +} diff --git a/lez/wallet/src/cli/programs/native_token_transfer.rs b/lez/wallet/src/cli/programs/native_token_transfer.rs index 174e3cb9..3589f189 100644 --- a/lez/wallet/src/cli/programs/native_token_transfer.rs +++ b/lez/wallet/src/cli/programs/native_token_transfer.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::Subcommand; use common::transaction::LeeTransaction; use lee::AccountId; @@ -34,13 +34,17 @@ pub enum AuthTransferSubcommand { #[arg(long)] to: Option, /// `to_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "to_keys")] to_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "to_keys")] to_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`. + #[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])] + to_keys: Option, /// Identifier for the recipient's private account (only used when sending to a foreign - /// private account via `--to-npk`/`--to-vpk`). + /// private account via `--to-npk`/`--to-vpk` or `--to-keys`). #[arg(long)] to_identifier: Option, /// amount - amount of balance to move. @@ -100,9 +104,18 @@ impl WalletSubcommand for AuthTransferSubcommand { to: to_account, to_npk, to_vpk, + to_keys, to_identifier, amount, } => { + // Resolve --to-keys into --to-npk / --to-vpk equivalents. + let (to_npk, to_vpk) = if let Some(path) = to_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (to_npk, to_vpk) + }; + let from = from_account.resolve(wallet_core.storage())?; let to = to_account .as_ref() @@ -258,7 +271,7 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: String, - /// `to_vpk` - valid 33 byte hex string. + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] to_vpk: String, /// Identifier for the recipient's private account. @@ -298,7 +311,7 @@ pub enum NativeTokenTransferProgramSubcommandPrivate { /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: String, - /// `to_vpk` - valid 33 byte hex string. + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] to_vpk: String, /// Identifier for the recipient's private account. @@ -350,7 +363,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { to_npk.copy_from_slice(&to_npk_res); let to_npk = lee_core::NullifierPublicKey(to_npk); - let to_vpk_res = hex::decode(to_vpk)?; + let to_vpk_res = hex::decode(&to_vpk) + .context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?; let to_vpk = lee_core::encryption::ViewingPublicKey(to_vpk_res); let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core) @@ -424,7 +438,8 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { to_npk.copy_from_slice(&to_npk_res); let to_npk = lee_core::NullifierPublicKey(to_npk); - let to_vpk_res = hex::decode(to_vpk)?; + let to_vpk_res = hex::decode(&to_vpk) + .context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?; let to_vpk = lee_core::encryption::ViewingPublicKey(to_vpk_res); let (tx_hash, _) = NativeTokenTransfer(wallet_core) diff --git a/lez/wallet/src/cli/programs/token.rs b/lez/wallet/src/cli/programs/token.rs index 6e756607..e007a134 100644 --- a/lez/wallet/src/cli/programs/token.rs +++ b/lez/wallet/src/cli/programs/token.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::Subcommand; use common::transaction::LeeTransaction; use lee::AccountId; @@ -41,13 +41,17 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] to: Option, /// `to_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "to_keys")] to_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "to_keys")] to_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`. + #[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])] + to_keys: Option, /// Identifier for the recipient's private account (only used when sending to a foreign - /// private account via `--to-npk`/`--to-vpk`). + /// private account via `--to-npk`/`--to-vpk` or `--to-keys`). #[arg(long)] to_identifier: Option, /// amount - amount of balance to move. @@ -87,13 +91,17 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] holder: Option, /// `holder_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "holder_keys")] holder_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `holder_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "holder_keys")] holder_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--holder-npk` and `--holder-vpk`. + #[arg(long, conflicts_with_all = ["holder_npk", "holder_vpk"])] + holder_keys: Option, /// Identifier for the holder's private account (only used when minting to a foreign - /// private account via `--holder-npk`/`--holder-vpk`). + /// private account via `--holder-npk`/`--holder-vpk` or `--holder-keys`). #[arg(long)] holder_identifier: Option, /// amount - amount of balance to mint. @@ -170,9 +178,17 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { to, to_npk, to_vpk, + to_keys, to_identifier, amount, } => { + let (to_npk, to_vpk) = if let Some(path) = to_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (to_npk, to_vpk) + }; + let from = from.resolve(wallet_core.storage())?; let to = to .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -309,9 +325,17 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder, holder_npk, holder_vpk, + holder_keys, holder_identifier, amount, } => { + let (holder_npk, holder_vpk) = if let Some(path) = holder_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (holder_npk, holder_vpk) + }; + let definition = definition.resolve(wallet_core.storage())?; let holder = holder .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -475,7 +499,7 @@ pub enum TokenProgramSubcommandPrivate { /// `recipient_npk` - valid 32 byte hex string. #[arg(long)] recipient_npk: String, - /// `recipient_vpk` - valid 33 byte hex string. + /// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] recipient_vpk: String, /// Identifier for the recipient's private account. @@ -569,7 +593,7 @@ pub enum TokenProgramSubcommandShielded { /// `recipient_npk` - valid 32 byte hex string. #[arg(long)] recipient_npk: String, - /// `recipient_vpk` - valid 33 byte hex string. + /// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] recipient_vpk: String, /// Identifier for the recipient's private account. @@ -764,7 +788,9 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { recipient_npk.copy_from_slice(&recipient_npk_res); let recipient_npk = lee_core::NullifierPublicKey(recipient_npk); - let recipient_vpk_res = hex::decode(recipient_vpk)?; + let recipient_vpk_res = hex::decode(&recipient_vpk).context( + "wallet::cli::programs::token: recipient_vpk must be a valid hex string", + )?; let recipient_vpk = lee_core::encryption::ViewingPublicKey(recipient_vpk_res); let (tx_hash, [secret_sender, _]) = Token(wallet_core) @@ -872,7 +898,9 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { holder_npk.copy_from_slice(&holder_npk_res); let holder_npk = lee_core::NullifierPublicKey(holder_npk); - let holder_vpk_res = hex::decode(holder_vpk)?; + let holder_vpk_res = hex::decode(&holder_vpk).context( + "wallet::cli::programs::token: holder_vpk must be a valid hex string", + )?; let holder_vpk = lee_core::encryption::ViewingPublicKey(holder_vpk_res); let (tx_hash, [secret_definition, _]) = Token(wallet_core) @@ -1024,7 +1052,9 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { recipient_npk.copy_from_slice(&recipient_npk_res); let recipient_npk = lee_core::NullifierPublicKey(recipient_npk); - let recipient_vpk_res = hex::decode(recipient_vpk)?; + let recipient_vpk_res = hex::decode(&recipient_vpk).context( + "wallet::cli::programs::token: recipient_vpk must be a valid hex string", + )?; let recipient_vpk = lee_core::encryption::ViewingPublicKey(recipient_vpk_res); let (tx_hash, _) = Token(wallet_core) @@ -1151,7 +1181,9 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { holder_npk.copy_from_slice(&holder_npk_res); let holder_npk = lee_core::NullifierPublicKey(holder_npk); - let holder_vpk_res = hex::decode(holder_vpk)?; + let holder_vpk_res = hex::decode(&holder_vpk).context( + "wallet::cli::programs::token: holder_vpk must be a valid hex string", + )?; let holder_vpk = lee_core::encryption::ViewingPublicKey(holder_vpk_res); let (tx_hash, _) = Token(wallet_core) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 5f601a04..79a223fe 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -741,8 +741,8 @@ impl WalletCore { .filter_map(move |(ciph_id, encrypted_data)| { let ciphertext = &encrypted_data.ciphertext; let commitment = &new_commitments[ciph_id]; - let shared_secret = key_chain - .calculate_shared_secret_receiver(&encrypted_data.epk)?; + let shared_secret = + key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?; lee_core::EncryptionScheme::decrypt( ciphertext, diff --git a/tools/integration_bench/src/scenarios/amm.rs b/tools/integration_bench/src/scenarios/amm.rs index a6001ebe..483010eb 100644 --- a/tools/integration_bench/src/scenarios/amm.rs +++ b/tools/integration_bench/src/scenarios/amm.rs @@ -180,6 +180,7 @@ async fn timed_token_send( to: Some(public_mention(to_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount, }), diff --git a/tools/integration_bench/src/scenarios/fanout.rs b/tools/integration_bench/src/scenarios/fanout.rs index 332e83f6..d230523b 100644 --- a/tools/integration_bench/src/scenarios/fanout.rs +++ b/tools/integration_bench/src/scenarios/fanout.rs @@ -50,6 +50,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER, }), diff --git a/tools/integration_bench/src/scenarios/parallel.rs b/tools/integration_bench/src/scenarios/parallel.rs index 24265bc3..3e69ad2b 100644 --- a/tools/integration_bench/src/scenarios/parallel.rs +++ b/tools/integration_bench/src/scenarios/parallel.rs @@ -69,6 +69,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(sender_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER * 5, }), @@ -97,6 +98,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(*recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER, }), diff --git a/tools/integration_bench/src/scenarios/private.rs b/tools/integration_bench/src/scenarios/private.rs index e46d6059..be6bb33b 100644 --- a/tools/integration_bench/src/scenarios/private.rs +++ b/tools/integration_bench/src/scenarios/private.rs @@ -46,6 +46,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_a)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000, }), @@ -64,6 +65,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(public_recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }), @@ -82,6 +84,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_b)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 200, }), diff --git a/tools/integration_bench/src/scenarios/token.rs b/tools/integration_bench/src/scenarios/token.rs index bfc41d5d..3cff19e9 100644 --- a/tools/integration_bench/src/scenarios/token.rs +++ b/tools/integration_bench/src/scenarios/token.rs @@ -41,6 +41,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000, }), @@ -61,6 +62,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 500, }),