diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index 639da90a..69cfcd94 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 e67b8a69..412f58b8 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 58522a1e..98969806 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 cd1a9b2a..b6ed736d 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 9c18a4e2..9c2c3b91 100644 Binary files a/artifacts/lez/programs/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/lez/programs/bridge_lock.bin b/artifacts/lez/programs/bridge_lock.bin index 34864afc..f141b9aa 100644 Binary files a/artifacts/lez/programs/bridge_lock.bin and b/artifacts/lez/programs/bridge_lock.bin differ diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index 4d216551..1e3bc48a 100644 Binary files a/artifacts/lez/programs/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/lez/programs/cross_zone_inbox.bin b/artifacts/lez/programs/cross_zone_inbox.bin index f8ba9029..c2f18db3 100644 Binary files a/artifacts/lez/programs/cross_zone_inbox.bin and b/artifacts/lez/programs/cross_zone_inbox.bin differ diff --git a/artifacts/lez/programs/cross_zone_outbox.bin b/artifacts/lez/programs/cross_zone_outbox.bin index c463dd5c..e9ead7b3 100644 Binary files a/artifacts/lez/programs/cross_zone_outbox.bin and b/artifacts/lez/programs/cross_zone_outbox.bin differ diff --git a/artifacts/lez/programs/faucet.bin b/artifacts/lez/programs/faucet.bin index f9abdd3a..b87cd081 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 ba555a20..4b5f9b6c 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 23e71794..16bb21be 100644 Binary files a/artifacts/lez/programs/pinata_token.bin and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/ping_receiver.bin b/artifacts/lez/programs/ping_receiver.bin index 33316416..c5f5e3f3 100644 Binary files a/artifacts/lez/programs/ping_receiver.bin and b/artifacts/lez/programs/ping_receiver.bin differ diff --git a/artifacts/lez/programs/ping_sender.bin b/artifacts/lez/programs/ping_sender.bin index 7ba0ba41..d9b1fb66 100644 Binary files a/artifacts/lez/programs/ping_sender.bin and b/artifacts/lez/programs/ping_sender.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index 40a96ac2..baab0124 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 fa90bdba..cf2e5d54 100644 Binary files a/artifacts/lez/programs/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/artifacts/lez/programs/wrapped_token.bin b/artifacts/lez/programs/wrapped_token.bin index 52a61dfe..930f4427 100644 Binary files a/artifacts/lez/programs/wrapped_token.bin and b/artifacts/lez/programs/wrapped_token.bin differ diff --git a/examples/program_deployment/README.md b/examples/program_deployment/README.md index 240079a5..35a1cd5b 100644 --- a/examples/program_deployment/README.md +++ b/examples/program_deployment/README.md @@ -348,9 +348,9 @@ Check the `run_hello_world_private.rs` file to see how it is used. # 8. Account authorization mechanism The Hello world example does not enforce any authorization on the input account. This means any user can execute it on any account, regardless of ownership. -LEE provides a mechanism for programs to enforce proper authorization before an execution can succeed. The meaning of authorization differs between public and private accounts: -- Public accounts: authorization requires that the transaction is signed with the account’s signing key. -- Private accounts: authorization requires that the circuit verifies knowledge of the account’s nullifier secret key. +LEE provides a mechanism for programs to enforce proper authorization before an execution can succeed. For both private and public accounts, the authorization is checked against knowledge of a secret key, yet the check is different: +- Public accounts: the transaction is signed with the account’s signing key. +- Private accounts: the circuit verifies knowledge of the account’s authorization secret key (`ask`), the key from which the account’s nullifier secret key is derived. From the program development perspective it is very simple: input accounts come with a flag indicating whether they has been properly authorized. And so, the only difference between the program `hello_world.rs` and `hello_world_with_authorization.rs` is in the lines diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index b62fe892..b60ee956 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -592,7 +592,8 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { let faucet_program_id = programs::faucet().id(); let vault_program_id = programs::vault().id(); let auth_transfer_program_id = programs::authenticated_transfer().id(); - let nsk: lee_core::NullifierSecretKey = [3; 32]; + let ask = lee_core::AuthorizationSecretKey([3; 32]); + let nsk = lee_core::NullifierSecretKey::from(&ask); let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); let attacker_vault_id = { @@ -661,7 +662,8 @@ async fn prove_init_with_commitment_root( sender_id, ); - let nsk: lee_core::NullifierSecretKey = [7; 32]; + let ask = lee_core::AuthorizationSecretKey([7; 32]); + let nsk = lee_core::NullifierSecretKey::from(&ask); let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); @@ -678,7 +680,7 @@ async fn prove_init_with_commitment_root( vpk, random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { ask: Some(ask) }, nullifier: NullifierWitness::Init { npk, commitment_root, @@ -697,7 +699,8 @@ async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> { let (_, expected_digest) = ctx.sequencer_client().get_proofs_and_root(vec![]).await?; - let nsk: lee_core::NullifierSecretKey = [7; 32]; + let ask = lee_core::AuthorizationSecretKey([7; 32]); + let nsk = lee_core::NullifierSecretKey::from(&ask); let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 5977dfc1..ab08ffdc 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -22,8 +22,8 @@ use lee::{ public_transaction as putx, }; use lee_core::{ - DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey, - NullifierWitness, PrivateWitness, WitnessKind, + AuthorizationSecretKey, DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, + NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, WitnessKind, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -255,7 +255,8 @@ pub async fn tps_test() -> Result<()> { #[expect(dead_code, reason = "No idea if we need this, should we remove it?")] fn build_privacy_transaction() -> PrivacyPreservingTransaction { let program = programs::authenticated_transfer(); - let sender_nsk = [1; 32]; + let sender_ask = AuthorizationSecretKey([1; 32]); + let sender_nsk = NullifierSecretKey::from(&sender_ask); let sender_vpk = ViewingPublicKey::from_seed(&[99_u8; 32], &[100_u8; 32]); let sender_npk = NullifierPublicKey::from(&sender_nsk); let sender_pre = AccountWithMetadata::new( @@ -268,7 +269,8 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { true, AccountId::for_regular_private_account(&sender_npk, &sender_vpk, 0), ); - let recipient_nsk = [2; 32]; + let recipient_ask = AuthorizationSecretKey([2; 32]); + let recipient_nsk = NullifierSecretKey::from(&recipient_ask); let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]); let recipient_npk = NullifierPublicKey::from(&recipient_nsk); let recipient_pre = AccountWithMetadata::new( @@ -296,7 +298,9 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { vpk: sender_vpk, random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, nsk: sender_nsk, @@ -307,7 +311,9 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { vpk: recipient_vpk, random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_ask), + }, nullifier: NullifierWitness::Init { npk: recipient_npk, commitment_root: DUMMY_COMMITMENT_HASH, 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 1aef6c91..289ccfe4 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -339,7 +339,7 @@ mod tests { } /// Pins the end-to-end derivation for a fixed (GMS, `ProgramId`, `PdaSeed`). Any change - /// to `secret_spending_key_for_pda`, the `PrivateKeyHolder` nsk/npk chain, or the + /// to `secret_spending_key_for_pda`, the `PrivateKeyHolder` ask/nsk/npk chain, or the /// `AccountId::for_private_pda` formula breaks this test. Mirrors the pinned-value /// pattern from `for_private_pda_matches_pinned_value` in `lee_core`. #[test] @@ -357,8 +357,8 @@ mod tests { 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, - 215, 76, 92, 60, 166, 104, 35, 51, 91, 16, 114, 188, 112, + 73, 227, 101, 209, 210, 127, 85, 171, 217, 20, 52, 68, 63, 127, 88, 157, 162, 165, 221, + 15, 86, 162, 128, 15, 56, 89, 95, 33, 216, 229, 181, 123, ]); // 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. 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 8165e808..18144a47 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 @@ -1,6 +1,8 @@ use std::collections::BTreeMap; -use lee_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey}; +use lee_core::{ + NullifierPublicKey, NullifierSecretKey, PrivateAccountKind, encryption::ViewingPublicKey, +}; use serde::{Deserialize, Serialize}; use sha2::Digest as _; @@ -32,11 +34,13 @@ impl ChildKeysPrivate { #[must_use] pub fn nth_child(&self, cci: u32) -> Self { + const DOMAIN: &[u8; 21] = b"/LEE/v0.3/Keys/Parent"; + // `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(self.value.0.private_key_holder.nullifier_secret_key); + parent_hash.update(DOMAIN); + 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(); @@ -58,10 +62,10 @@ impl ChildKeysPrivate { } fn from_ssk_and_ccc(ssk: SecretSpendingKey, ccc: [u8; 32], cci: Option) -> Self { - let nsk = ssk.generate_nullifier_secret_key(cci); + let ask = ssk.generate_authorization_secret_key(cci); let vsk = ssk.generate_viewing_secret_seed_key(cci); - let npk = NullifierPublicKey::from(&nsk); + let npk = NullifierPublicKey::from(&NullifierSecretKey::from(&ask)); let vpk = ViewingPublicKey::from(&vsk); Self { @@ -71,7 +75,7 @@ impl ChildKeysPrivate { nullifier_public_key: npk, viewing_public_key: vpk, private_key_holder: PrivateKeyHolder { - nullifier_secret_key: nsk, + authorization_secret_key: ask, viewing_secret_key: vsk, }, }, @@ -130,95 +134,104 @@ mod tests { 111, 13, 5, 195, 75, 20, 255, 162, 85, 40, 251, 8, 168, ]; + let expected_ask = lee_core::AuthorizationSecretKey([ + 3, 154, 34, 187, 166, 138, 64, 10, 172, 210, 224, 75, 165, 157, 94, 27, 81, 209, 194, + 189, 60, 171, 252, 226, 25, 136, 158, 59, 56, 39, 60, 175, + ]); + let expected_nsk: NullifierSecretKey = [ - 154, 102, 103, 5, 34, 235, 227, 13, 22, 182, 226, 11, 7, 67, 110, 162, 99, 193, 174, - 34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163, + 227, 13, 41, 248, 160, 185, 37, 158, 48, 134, 157, 185, 50, 249, 13, 114, 128, 43, 92, + 148, 161, 91, 158, 206, 209, 246, 46, 49, 114, 165, 72, 64, ]; let expected_npk = lee_core::NullifierPublicKey([ - 7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97, - 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2, + 42, 60, 83, 112, 244, 198, 238, 159, 150, 105, 13, 134, 103, 228, 213, 247, 121, 42, + 65, 51, 122, 196, 228, 163, 244, 251, 219, 119, 8, 14, 68, 16, ]); 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, + 92, 182, 50, 80, 228, 152, 149, 83, 44, 33, 179, 59, 237, 153, 45, 46, 216, 142, + 62, 31, 28, 18, 44, 27, 130, 54, 10, 13, 148, 111, 214, 107, ], [ - 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, + 135, 73, 174, 183, 171, 136, 40, 174, 28, 18, 73, 1, 183, 13, 208, 39, 113, 79, + 136, 163, 234, 119, 117, 192, 103, 49, 193, 16, 188, 111, 4, 78, ], ); - // Length matches MlKem768EncapsulationKey::LEN. + // Length matches MlKem768EncapsulationKey::LEN. Oracle-sourced from the ML-KEM-768 + // implementation, unlike every other vector here; its trailing 32 bytes are + // rho = SHA3-512(d || 3)[..32] per FIPS-203, checked against the independent `d`. let expected_vpk: [u8; 1184] = [ - 127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220, - 150, 100, 203, 34, 34, 127, 232, 156, 43, 218, 109, 6, 160, 67, 35, 210, 194, 25, 181, - 118, 237, 25, 129, 51, 160, 189, 51, 99, 184, 57, 28, 121, 240, 236, 2, 170, 198, 26, - 91, 172, 110, 52, 32, 186, 35, 179, 202, 234, 249, 15, 242, 100, 198, 168, 163, 120, - 205, 118, 85, 195, 210, 187, 95, 150, 154, 8, 68, 165, 237, 87, 166, 101, 57, 4, 18, - 11, 122, 235, 180, 199, 154, 165, 158, 55, 136, 30, 237, 43, 167, 215, 68, 80, 102, 0, - 71, 90, 130, 206, 240, 215, 69, 199, 83, 7, 60, 184, 128, 230, 184, 61, 93, 201, 204, - 165, 104, 9, 127, 220, 52, 246, 217, 131, 251, 2, 170, 133, 6, 51, 40, 224, 101, 61, - 16, 135, 32, 182, 201, 68, 58, 171, 54, 161, 184, 243, 38, 106, 200, 251, 17, 172, 8, - 24, 73, 230, 55, 85, 20, 147, 222, 165, 200, 116, 135, 47, 20, 227, 56, 220, 64, 120, - 215, 245, 58, 86, 102, 149, 252, 193, 163, 160, 59, 82, 138, 249, 171, 1, 54, 199, 193, - 171, 85, 38, 64, 56, 121, 106, 84, 57, 252, 94, 147, 16, 191, 196, 104, 47, 129, 84, - 21, 252, 160, 81, 207, 184, 199, 3, 177, 74, 117, 115, 175, 138, 108, 36, 198, 5, 32, - 15, 218, 3, 20, 19, 15, 251, 209, 86, 128, 139, 148, 78, 10, 34, 144, 149, 74, 102, 48, - 59, 70, 124, 47, 193, 100, 26, 9, 104, 178, 102, 156, 199, 242, 101, 147, 161, 87, 27, - 234, 192, 204, 41, 36, 43, 83, 219, 15, 211, 66, 91, 76, 73, 13, 113, 155, 203, 193, - 160, 130, 84, 103, 47, 70, 100, 147, 169, 65, 119, 84, 121, 122, 161, 76, 203, 144, - 248, 145, 22, 8, 46, 121, 44, 77, 20, 149, 66, 179, 56, 149, 231, 98, 184, 9, 64, 14, - 67, 196, 34, 8, 123, 21, 80, 169, 168, 223, 230, 133, 0, 66, 159, 230, 69, 201, 205, - 169, 105, 196, 21, 71, 84, 70, 58, 165, 165, 134, 186, 232, 60, 70, 51, 57, 239, 74, - 174, 116, 234, 36, 178, 49, 42, 168, 250, 104, 141, 106, 0, 109, 52, 86, 104, 243, 62, - 214, 137, 48, 107, 2, 152, 206, 227, 175, 147, 236, 19, 113, 27, 191, 231, 235, 167, - 114, 104, 23, 126, 203, 94, 242, 149, 171, 115, 170, 89, 244, 58, 29, 176, 73, 203, 44, - 8, 32, 9, 226, 32, 78, 246, 38, 235, 149, 133, 25, 243, 47, 124, 180, 200, 211, 165, - 137, 56, 169, 117, 31, 244, 65, 91, 135, 146, 158, 20, 75, 102, 32, 65, 250, 103, 199, - 36, 48, 31, 155, 164, 191, 222, 85, 37, 66, 243, 17, 120, 104, 0, 228, 83, 200, 116, 6, - 199, 106, 236, 139, 246, 216, 152, 241, 211, 85, 106, 200, 44, 231, 240, 66, 3, 193, - 147, 16, 145, 65, 49, 33, 53, 247, 69, 47, 44, 113, 86, 117, 6, 20, 193, 183, 128, 178, - 181, 21, 251, 99, 39, 149, 210, 146, 106, 181, 186, 7, 36, 63, 186, 234, 191, 164, 193, - 162, 127, 250, 122, 189, 219, 21, 92, 48, 86, 209, 184, 99, 160, 201, 162, 145, 20, - 138, 154, 18, 37, 180, 209, 165, 165, 51, 187, 78, 193, 175, 135, 6, 55, 216, 178, 10, - 40, 246, 98, 128, 80, 14, 38, 69, 113, 123, 54, 94, 43, 50, 106, 167, 17, 77, 163, 148, - 117, 225, 9, 7, 253, 240, 157, 96, 103, 33, 100, 37, 37, 20, 53, 138, 234, 55, 45, 232, - 154, 9, 150, 192, 116, 36, 119, 106, 95, 119, 34, 220, 84, 174, 19, 227, 33, 209, 96, - 197, 148, 230, 197, 59, 117, 130, 7, 116, 11, 0, 197, 16, 249, 151, 31, 4, 64, 29, 165, - 247, 110, 176, 166, 4, 112, 136, 101, 208, 7, 179, 38, 183, 134, 58, 107, 207, 160, 38, - 159, 67, 112, 20, 225, 199, 179, 133, 117, 144, 54, 199, 15, 204, 80, 154, 116, 84, 88, - 109, 113, 5, 207, 226, 21, 62, 247, 122, 14, 156, 9, 8, 76, 26, 148, 67, 196, 128, 176, - 78, 51, 161, 151, 75, 248, 154, 31, 168, 9, 4, 3, 107, 222, 245, 178, 21, 84, 7, 25, - 155, 118, 97, 135, 63, 89, 233, 11, 207, 148, 155, 38, 106, 104, 102, 140, 104, 67, - 149, 20, 30, 196, 44, 197, 128, 34, 182, 80, 30, 32, 137, 34, 212, 164, 177, 164, 12, - 115, 41, 156, 111, 71, 230, 120, 111, 218, 25, 117, 218, 75, 167, 32, 37, 57, 50, 99, - 181, 203, 40, 105, 248, 150, 114, 121, 73, 127, 198, 191, 161, 44, 56, 213, 243, 71, 2, - 56, 192, 243, 107, 179, 27, 96, 21, 116, 169, 64, 15, 97, 166, 151, 200, 11, 40, 204, - 71, 168, 220, 9, 55, 43, 146, 244, 212, 166, 192, 180, 189, 237, 162, 42, 29, 33, 52, - 193, 4, 178, 157, 244, 28, 209, 44, 26, 36, 147, 126, 94, 164, 37, 47, 115, 38, 23, - 165, 96, 106, 140, 42, 69, 146, 194, 93, 71, 175, 49, 147, 32, 246, 97, 94, 41, 116, - 127, 174, 18, 16, 14, 163, 17, 180, 213, 203, 166, 33, 139, 214, 18, 170, 27, 41, 59, - 175, 200, 101, 14, 128, 45, 179, 167, 136, 232, 138, 56, 124, 145, 75, 233, 132, 161, - 196, 164, 72, 80, 60, 187, 38, 90, 90, 17, 66, 134, 59, 2, 165, 29, 76, 24, 38, 211, - 177, 83, 119, 20, 239, 59, 77, 34, 3, 42, 47, 60, 89, 46, 103, 168, 120, 17, 199, 50, - 17, 103, 107, 48, 8, 53, 220, 159, 212, 65, 198, 80, 8, 11, 235, 97, 203, 196, 240, 44, - 56, 121, 77, 91, 196, 160, 129, 242, 149, 226, 57, 106, 180, 76, 161, 203, 18, 37, 166, - 153, 44, 40, 28, 74, 8, 11, 6, 166, 54, 10, 103, 247, 23, 35, 7, 47, 173, 133, 71, 85, - 3, 168, 250, 120, 126, 174, 37, 80, 128, 107, 7, 161, 130, 155, 136, 92, 48, 215, 119, - 196, 124, 85, 157, 234, 2, 166, 137, 65, 121, 222, 112, 47, 17, 43, 23, 111, 88, 5, - 195, 41, 8, 191, 227, 21, 173, 35, 199, 196, 188, 162, 191, 195, 204, 137, 54, 16, 73, - 178, 150, 249, 234, 22, 216, 123, 157, 144, 218, 118, 53, 193, 67, 65, 84, 162, 244, - 165, 24, 110, 246, 146, 228, 212, 180, 150, 116, 201, 37, 128, 76, 41, 188, 42, 79, - 148, 52, 196, 176, 178, 224, 48, 168, 13, 129, 193, 131, 185, 131, 93, 40, 145, 56, - 180, 29, 153, 83, 39, 69, 232, 96, 238, 137, 104, 150, 2, 202, 239, 149, 248, 154, 115, - 115, 127, 3, 8, 32, 61, 96, 66, 25, 181, 14, 72, 73, 97, 186, 134, 140, 33, 69, 33, 74, + 95, 42, 170, 49, 164, 173, 200, 156, 66, 32, 71, 126, 122, 140, 148, 144, 114, 143, + 233, 199, 104, 82, 179, 49, 43, 114, 130, 182, 71, 4, 45, 101, 65, 136, 196, 72, 129, + 128, 204, 239, 137, 84, 230, 210, 18, 214, 252, 40, 198, 210, 24, 158, 53, 151, 166, + 24, 47, 143, 8, 158, 119, 240, 204, 210, 242, 96, 191, 147, 106, 98, 198, 93, 193, 163, + 31, 132, 36, 16, 50, 83, 24, 225, 250, 106, 55, 231, 188, 90, 194, 128, 10, 225, 186, + 41, 225, 165, 126, 57, 32, 163, 129, 42, 68, 113, 177, 239, 106, 144, 217, 188, 192, + 174, 38, 161, 189, 24, 107, 14, 54, 167, 221, 120, 194, 6, 22, 163, 86, 96, 47, 220, + 227, 176, 173, 52, 150, 183, 25, 40, 200, 19, 134, 51, 172, 126, 35, 147, 79, 207, 235, + 9, 243, 197, 84, 4, 194, 142, 207, 118, 121, 133, 58, 12, 58, 226, 22, 106, 172, 56, + 223, 161, 145, 60, 28, 47, 95, 84, 127, 1, 235, 72, 0, 131, 202, 15, 151, 93, 52, 18, + 13, 247, 91, 80, 240, 229, 85, 72, 135, 84, 230, 113, 196, 162, 3, 24, 87, 176, 80, + 202, 99, 44, 87, 229, 96, 254, 27, 181, 181, 58, 191, 116, 19, 68, 235, 35, 86, 227, + 89, 49, 70, 102, 54, 153, 224, 117, 34, 113, 57, 121, 202, 42, 248, 24, 125, 134, 134, + 57, 126, 204, 131, 191, 181, 71, 197, 184, 137, 48, 76, 29, 174, 137, 154, 253, 50, 68, + 184, 122, 173, 106, 144, 207, 48, 213, 156, 182, 26, 103, 203, 133, 131, 47, 184, 189, + 109, 4, 182, 126, 71, 180, 153, 18, 82, 77, 201, 23, 176, 92, 12, 146, 48, 26, 236, + 139, 157, 174, 214, 77, 253, 163, 94, 52, 133, 88, 200, 251, 156, 197, 201, 7, 239, + 117, 83, 57, 188, 85, 31, 196, 106, 164, 147, 36, 32, 241, 143, 54, 121, 195, 183, 98, + 182, 135, 90, 84, 118, 212, 91, 115, 41, 75, 193, 156, 44, 9, 196, 199, 241, 123, 148, + 31, 105, 126, 160, 234, 16, 196, 149, 192, 66, 34, 199, 132, 160, 98, 229, 90, 158, 46, + 108, 112, 126, 165, 115, 234, 128, 164, 241, 132, 171, 186, 212, 121, 74, 217, 165, + 111, 216, 21, 169, 89, 86, 173, 163, 183, 61, 28, 117, 104, 211, 206, 30, 194, 180, 34, + 180, 151, 150, 212, 90, 75, 139, 138, 253, 52, 60, 252, 5, 126, 152, 12, 153, 77, 232, + 167, 14, 163, 130, 76, 18, 117, 96, 113, 144, 234, 22, 56, 106, 210, 78, 83, 50, 43, + 99, 120, 20, 172, 89, 61, 10, 75, 121, 118, 226, 153, 53, 161, 144, 53, 246, 37, 213, + 216, 48, 183, 124, 58, 161, 145, 126, 238, 120, 112, 103, 65, 176, 40, 104, 60, 47, 10, + 138, 154, 89, 174, 164, 69, 182, 168, 196, 131, 68, 18, 189, 204, 74, 180, 16, 233, + 178, 175, 57, 180, 212, 58, 148, 92, 2, 16, 255, 103, 27, 212, 117, 12, 10, 54, 105, + 253, 9, 124, 250, 210, 14, 127, 151, 74, 49, 209, 59, 125, 184, 183, 175, 251, 200, + 172, 120, 59, 41, 89, 199, 3, 161, 189, 138, 50, 69, 108, 102, 155, 210, 17, 73, 235, + 75, 145, 132, 67, 89, 88, 225, 182, 156, 248, 199, 112, 52, 22, 134, 80, 40, 250, 42, + 185, 57, 200, 90, 137, 16, 158, 98, 114, 48, 151, 35, 128, 49, 49, 118, 195, 57, 40, + 94, 103, 156, 186, 1, 112, 130, 178, 59, 22, 71, 153, 173, 195, 178, 216, 149, 24, 202, + 245, 123, 117, 106, 44, 55, 128, 37, 165, 26, 103, 158, 52, 10, 188, 10, 195, 146, 204, + 85, 66, 66, 162, 73, 25, 59, 107, 57, 149, 100, 216, 24, 69, 49, 134, 233, 96, 29, 176, + 8, 188, 121, 145, 44, 35, 199, 4, 48, 24, 76, 69, 250, 92, 126, 40, 52, 162, 72, 113, + 81, 96, 116, 105, 150, 59, 211, 236, 141, 87, 178, 9, 17, 117, 43, 139, 17, 150, 153, + 114, 195, 212, 2, 192, 56, 91, 70, 200, 75, 2, 57, 171, 147, 184, 236, 15, 64, 26, 191, + 131, 179, 13, 195, 195, 166, 208, 180, 93, 186, 155, 102, 189, 57, 82, 73, 39, 44, 249, + 249, 183, 33, 112, 59, 130, 20, 193, 41, 40, 128, 131, 106, 136, 51, 75, 56, 188, 167, + 119, 5, 118, 73, 84, 168, 38, 121, 182, 190, 252, 182, 87, 142, 33, 66, 131, 75, 36, + 216, 181, 186, 213, 148, 191, 182, 115, 159, 83, 1, 14, 170, 55, 21, 251, 65, 135, 117, + 171, 147, 38, 210, 129, 251, 151, 177, 213, 1, 18, 22, 241, 62, 173, 80, 76, 85, 129, + 139, 192, 137, 205, 203, 114, 181, 121, 40, 141, 9, 194, 58, 20, 200, 126, 151, 51, + 129, 146, 92, 156, 93, 192, 72, 26, 33, 138, 107, 138, 124, 193, 138, 8, 244, 84, 116, + 28, 156, 123, 1, 19, 186, 119, 231, 157, 70, 160, 5, 34, 80, 201, 4, 39, 38, 217, 85, + 53, 10, 40, 136, 145, 225, 26, 65, 32, 76, 33, 245, 72, 166, 5, 165, 44, 67, 86, 99, + 87, 9, 148, 131, 72, 223, 71, 179, 243, 39, 36, 34, 145, 86, 134, 12, 127, 103, 3, 191, + 254, 216, 195, 12, 197, 184, 238, 67, 34, 226, 4, 100, 135, 165, 40, 164, 113, 110, + 132, 68, 100, 72, 217, 67, 169, 199, 96, 120, 152, 27, 26, 241, 103, 61, 162, 154, 113, + 55, 75, 156, 17, 114, 105, 145, 158, 13, 251, 50, 221, 219, 150, 88, 5, 184, 92, 137, + 164, 25, 117, 51, 87, 233, 93, 5, 84, 125, 251, 162, 110, 231, 36, 2, 235, 251, 185, + 45, 180, 132, 53, 104, 206, 144, 133, 67, 164, 76, 84, 152, 236, 157, 253, 115, 97, + 195, 177, 172, 233, 51, 161, 196, 66, 59, 233, 88, 133, 12, 146, 172, 148, 236, 58, 5, + 226, 48, 53, 219, 185, 72, 86, 7, 249, 151, 205, 32, 57, 163, 17, 71, 37, 162, 97, 137, + 142, 252, 190, 58, 196, 70, 181, 4, 48, 123, 9, 75, 198, 100, 134, 36, 18, 45, 99, 18, + 191, 75, 55, 30, 144, 197, 0, 44, 71, 199, 78, 121, 92, 76, 84, 43, 133, 139, 77, 105, + 83, 178, 221, 215, 108, 55, 58, 7, 106, 96, 146, 9, 70, 140, 250, 187, 206, 95, 54, 74, + 30, 146, 15, 182, 5, 79, 41, 135, 59, 75, 103, 82, 63, 39, 69, 178, 215, 49, 234, 146, + 127, 186, 192, 189, 107, 140, 11, 39, 162, 120, 90, 133, 106, 184, 87, 144, 5, 80, 80, + 22, 241, 181, 128, 201, 61, 186, 124, 9, 165, 192, 78, 67, 141, 57, 10, 94, 36, 75, + 118, 21, 105, 252, 45, 196, 60, 23, 182, 189, 252, 152, 182, 72, 229, 213, 89, 165, + 222, 151, 52, 182, 110, 127, 158, ]; assert!(expected_ssk == keys.value.0.secret_spending_key); assert!(expected_ccc == keys.ccc); - assert!(expected_nsk == keys.value.0.private_key_holder.nullifier_secret_key); + assert!(expected_ask == keys.value.0.private_key_holder.authorization_secret_key); + assert!(expected_nsk == keys.value.0.private_key_holder.nullifier_secret_key()); assert!(expected_npk == keys.value.0.nullifier_public_key); assert!(expected_vsk == keys.value.0.private_key_holder.viewing_secret_key); assert!(expected_vpk == keys.value.0.viewing_public_key.to_bytes()); @@ -230,105 +243,120 @@ mod tests { let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32); let expected_ssk = key_management::secret_holders::SecretSpendingKey([ - 151, 183, 113, 151, 215, 187, 207, 64, 197, 182, 207, 32, 5, 49, 180, 98, 119, 14, 248, - 175, 39, 100, 47, 109, 148, 173, 217, 253, 159, 234, 209, 113, + 109, 21, 107, 97, 112, 105, 143, 134, 185, 35, 168, 205, 138, 110, 125, 155, 193, 57, + 36, 19, 214, 180, 194, 46, 107, 235, 43, 80, 132, 19, 254, 231, ]); let expected_ccc = [ - 138, 243, 142, 163, 62, 107, 63, 131, 230, 158, 185, 60, 204, 50, 243, 222, 13, 123, - 98, 116, 131, 194, 7, 25, 129, 209, 163, 72, 178, 143, 192, 240, + 64, 218, 41, 59, 115, 126, 128, 3, 77, 77, 54, 84, 87, 253, 181, 112, 244, 254, 176, + 243, 86, 127, 219, 255, 64, 164, 218, 129, 83, 65, 176, 179, ]; + let expected_ask = lee_core::AuthorizationSecretKey([ + 33, 29, 199, 63, 191, 14, 196, 15, 51, 70, 236, 125, 93, 120, 78, 99, 90, 239, 220, + 168, 226, 46, 208, 238, 70, 117, 17, 28, 163, 89, 177, 129, + ]); + let expected_nsk: NullifierSecretKey = [ - 196, 33, 11, 39, 220, 84, 119, 182, 187, 194, 135, 20, 124, 33, 244, 205, 96, 58, 102, - 52, 74, 67, 110, 213, 24, 16, 160, 64, 247, 3, 107, 235, + 97, 83, 94, 170, 125, 55, 27, 105, 106, 42, 73, 99, 169, 221, 210, 124, 117, 52, 98, + 131, 98, 202, 79, 95, 151, 196, 239, 242, 6, 127, 160, 160, ]; let expected_npk = lee_core::NullifierPublicKey([ - 247, 253, 217, 86, 157, 208, 39, 172, 59, 190, 88, 165, 7, 173, 183, 106, 172, 211, 4, - 180, 51, 107, 177, 107, 51, 117, 231, 176, 200, 103, 1, 121, + 81, 199, 141, 154, 209, 135, 105, 75, 106, 240, 124, 190, 245, 233, 152, 34, 225, 234, + 212, 221, 121, 68, 255, 97, 231, 142, 26, 54, 169, 53, 69, 242, ]); let expected_vsk = ViewingSecretKey::new( [ - 185, 209, 179, 92, 7, 131, 98, 121, 215, 46, 154, 56, 238, 106, 162, 225, 83, 82, - 134, 3, 80, 186, 35, 178, 161, 204, 205, 163, 28, 19, 149, 18, + 14, 173, 255, 235, 8, 1, 246, 119, 243, 18, 235, 31, 209, 92, 142, 7, 175, 223, + 228, 201, 173, 165, 148, 137, 141, 160, 175, 161, 110, 139, 54, 183, ], [ - 174, 24, 72, 205, 129, 123, 131, 9, 146, 152, 224, 151, 10, 184, 224, 109, 94, 149, - 117, 60, 26, 10, 212, 125, 113, 147, 87, 67, 73, 26, 101, 193, + 10, 253, 101, 172, 213, 221, 88, 85, 178, 89, 218, 73, 28, 212, 1, 2, 105, 161, + 180, 24, 49, 6, 182, 155, 22, 220, 121, 42, 175, 59, 233, 109, ], ); - // Length matches MlKem768EncapsulationKey::LEN. + // Length matches MlKem768EncapsulationKey::LEN. Oracle-sourced from the ML-KEM-768 + // implementation, unlike every other vector here; its trailing 32 bytes are + // rho = SHA3-512(d || 3)[..32] per FIPS-203, checked against the independent `d`. let expected_vpk: [u8; 1184] = [ - 215, 229, 207, 120, 148, 177, 148, 197, 72, 222, 134, 3, 231, 146, 123, 226, 36, 84, - 232, 179, 205, 16, 241, 142, 9, 81, 58, 54, 12, 115, 148, 182, 19, 245, 22, 203, 57, - 71, 11, 204, 156, 130, 30, 170, 199, 201, 25, 2, 21, 34, 155, 136, 124, 145, 223, 128, - 177, 207, 92, 38, 252, 165, 118, 61, 128, 71, 154, 242, 105, 165, 52, 7, 6, 244, 120, - 227, 134, 191, 25, 169, 150, 123, 246, 138, 25, 196, 126, 156, 144, 33, 123, 120, 44, - 142, 89, 201, 49, 219, 205, 87, 236, 110, 64, 129, 102, 100, 155, 26, 101, 121, 42, - 236, 82, 111, 141, 117, 75, 71, 194, 73, 123, 170, 110, 69, 149, 107, 96, 195, 55, 122, - 140, 131, 106, 140, 156, 147, 75, 28, 128, 138, 113, 86, 37, 63, 173, 214, 200, 2, 214, - 84, 234, 176, 120, 252, 184, 99, 192, 65, 112, 150, 99, 26, 174, 187, 183, 187, 64, 90, - 248, 100, 66, 63, 195, 3, 44, 43, 128, 59, 149, 107, 66, 180, 67, 200, 183, 200, 36, - 91, 7, 65, 228, 159, 79, 44, 89, 35, 163, 145, 92, 227, 104, 2, 72, 5, 7, 193, 21, 51, - 116, 198, 184, 6, 192, 188, 68, 183, 163, 193, 142, 244, 217, 155, 197, 187, 189, 174, - 225, 45, 126, 112, 93, 194, 156, 102, 150, 1, 188, 222, 76, 108, 73, 149, 44, 28, 219, - 66, 95, 215, 204, 148, 217, 16, 36, 121, 112, 2, 51, 10, 195, 137, 12, 93, 203, 146, - 138, 211, 15, 201, 42, 72, 146, 186, 160, 222, 235, 127, 83, 48, 182, 49, 248, 29, 138, - 16, 32, 232, 179, 163, 187, 161, 174, 152, 187, 93, 76, 166, 48, 230, 219, 111, 123, - 181, 103, 130, 28, 109, 235, 115, 45, 57, 193, 206, 160, 17, 52, 92, 194, 25, 3, 80, - 97, 142, 249, 151, 94, 250, 95, 12, 57, 11, 165, 92, 47, 85, 182, 48, 22, 60, 97, 244, - 59, 194, 135, 180, 133, 106, 227, 56, 192, 60, 91, 15, 241, 146, 89, 240, 130, 219, - 202, 187, 43, 85, 98, 50, 104, 64, 114, 113, 80, 54, 69, 69, 5, 43, 90, 19, 0, 0, 188, - 251, 184, 70, 160, 18, 117, 76, 53, 209, 166, 96, 34, 224, 137, 115, 183, 168, 243, 19, - 1, 255, 4, 97, 162, 199, 104, 72, 213, 111, 62, 54, 172, 82, 184, 82, 143, 71, 99, 25, - 104, 74, 120, 70, 84, 235, 32, 22, 20, 218, 163, 77, 194, 125, 75, 22, 72, 236, 192, - 200, 107, 91, 156, 201, 10, 178, 87, 19, 181, 211, 91, 17, 145, 200, 17, 179, 65, 75, - 200, 186, 89, 144, 91, 184, 116, 214, 51, 91, 42, 162, 243, 202, 92, 18, 54, 0, 213, - 67, 149, 151, 51, 29, 220, 196, 160, 201, 68, 113, 210, 164, 175, 152, 121, 168, 231, - 161, 91, 132, 218, 1, 171, 176, 84, 100, 57, 1, 3, 2, 196, 194, 76, 181, 79, 171, 157, - 35, 162, 155, 192, 210, 149, 142, 120, 189, 127, 151, 96, 202, 225, 73, 242, 81, 112, - 237, 224, 155, 130, 130, 34, 196, 153, 131, 161, 113, 163, 172, 114, 48, 207, 32, 151, - 172, 83, 145, 79, 210, 100, 161, 92, 82, 216, 90, 104, 238, 212, 38, 50, 107, 17, 228, - 195, 190, 6, 151, 165, 148, 245, 102, 51, 8, 185, 8, 85, 59, 247, 219, 95, 219, 170, - 155, 233, 123, 27, 64, 251, 56, 24, 200, 16, 181, 212, 146, 61, 116, 106, 215, 214, 62, - 118, 27, 68, 233, 148, 73, 135, 199, 74, 184, 89, 159, 217, 139, 24, 208, 250, 30, 224, - 97, 185, 237, 193, 8, 216, 23, 186, 5, 50, 41, 161, 203, 22, 217, 23, 194, 191, 148, - 124, 10, 212, 171, 209, 210, 145, 184, 171, 74, 35, 220, 43, 145, 241, 23, 43, 92, 171, - 216, 43, 114, 77, 155, 147, 156, 86, 56, 170, 27, 1, 54, 182, 169, 96, 22, 201, 51, - 145, 94, 143, 133, 106, 47, 176, 112, 197, 197, 96, 80, 73, 164, 207, 179, 22, 229, - 171, 201, 223, 219, 13, 219, 1, 91, 224, 252, 171, 199, 217, 25, 60, 128, 135, 9, 71, - 105, 231, 86, 34, 21, 155, 50, 0, 105, 72, 117, 108, 175, 140, 9, 181, 249, 139, 97, 3, - 161, 66, 248, 42, 67, 113, 132, 8, 119, 232, 6, 169, 18, 157, 222, 53, 176, 56, 137, - 120, 18, 115, 199, 187, 112, 48, 223, 211, 206, 152, 252, 108, 179, 129, 20, 227, 248, - 183, 234, 87, 202, 49, 17, 69, 215, 118, 89, 188, 180, 33, 238, 245, 206, 40, 179, 129, - 242, 59, 73, 254, 117, 114, 250, 179, 103, 109, 250, 202, 99, 152, 2, 167, 130, 169, - 35, 71, 89, 211, 140, 71, 103, 154, 121, 108, 147, 191, 186, 73, 10, 73, 203, 23, 55, - 106, 144, 98, 227, 157, 25, 27, 81, 67, 11, 57, 88, 227, 116, 61, 100, 94, 23, 166, - 146, 57, 226, 72, 124, 33, 65, 226, 35, 167, 206, 156, 202, 213, 213, 158, 89, 249, - 181, 19, 113, 109, 217, 71, 168, 142, 180, 122, 30, 5, 54, 170, 155, 73, 56, 170, 124, - 139, 4, 165, 103, 82, 32, 183, 84, 7, 239, 117, 135, 239, 48, 24, 28, 210, 49, 137, 6, - 158, 65, 211, 113, 205, 135, 146, 83, 10, 46, 90, 27, 97, 135, 135, 185, 173, 69, 58, - 34, 247, 141, 150, 6, 158, 117, 23, 198, 139, 65, 81, 179, 187, 194, 247, 203, 127, - 106, 232, 119, 122, 215, 197, 110, 69, 203, 174, 227, 63, 185, 106, 14, 184, 104, 113, - 233, 83, 92, 104, 38, 188, 9, 135, 107, 108, 121, 193, 33, 209, 89, 39, 137, 17, 208, - 26, 21, 238, 169, 86, 181, 193, 153, 82, 8, 151, 53, 39, 88, 91, 252, 3, 33, 75, 127, - 9, 168, 53, 34, 1, 173, 202, 123, 157, 174, 170, 199, 254, 187, 196, 144, 37, 29, 48, - 112, 173, 107, 147, 155, 69, 134, 137, 156, 247, 123, 242, 72, 5, 43, 106, 89, 179, - 204, 41, 15, 60, 48, 78, 214, 180, 26, 170, 67, 71, 66, 146, 113, 220, 159, 153, 201, - 176, 116, 154, 21, 186, 33, 180, 72, 39, 187, 240, 80, 112, 132, 144, 173, 210, 12, 76, - 184, 146, 89, 178, 178, 82, 109, 71, 201, 241, 160, 207, 219, 124, 77, 2, 105, 124, - 178, 71, 3, 38, 64, 41, 83, 170, 137, 82, 242, 144, 76, 102, 82, 7, 25, 149, 141, 169, - 46, 4, 68, 40, 244, 146, 131, 107, 148, 18, 111, 85, 104, 243, 28, 75, 176, 249, 88, - 82, 123, 89, 29, 104, 135, 230, 117, 67, 26, 249, 108, 145, 76, 38, 175, 89, 185, 94, - 106, 128, 201, 150, 151, 194, 133, 21, 81, 213, 231, 15, 117, 44, 61, 86, 223, 162, 56, - 190, 166, 177, 157, 137, 60, 208, 155, 234, 158, 252, 30, + 153, 195, 136, 113, 42, 34, 28, 116, 169, 190, 53, 81, 59, 6, 163, 170, 199, 98, 201, + 54, 24, 171, 26, 191, 144, 248, 95, 63, 199, 113, 27, 67, 97, 49, 210, 66, 190, 74, 54, + 252, 12, 143, 197, 243, 27, 111, 198, 43, 23, 146, 3, 183, 171, 41, 72, 92, 46, 112, + 184, 160, 0, 25, 155, 109, 203, 70, 196, 88, 135, 121, 147, 12, 96, 230, 205, 217, 66, + 55, 78, 215, 196, 6, 8, 160, 186, 73, 56, 218, 76, 10, 31, 148, 182, 170, 131, 26, 101, + 210, 125, 125, 242, 160, 146, 35, 195, 59, 182, 55, 130, 198, 67, 109, 245, 66, 229, + 226, 154, 189, 112, 5, 160, 39, 7, 139, 152, 58, 197, 167, 81, 160, 37, 96, 175, 250, + 78, 232, 181, 152, 131, 41, 92, 45, 33, 184, 142, 25, 157, 62, 166, 111, 148, 137, 143, + 241, 176, 74, 176, 243, 178, 26, 232, 204, 119, 181, 17, 128, 91, 134, 8, 90, 35, 11, + 124, 98, 76, 76, 43, 82, 135, 85, 215, 72, 177, 37, 218, 131, 158, 139, 59, 155, 104, + 13, 13, 150, 34, 192, 54, 115, 90, 44, 84, 132, 51, 174, 155, 64, 26, 42, 246, 28, 30, + 252, 136, 227, 69, 174, 27, 233, 56, 214, 102, 117, 141, 177, 133, 181, 177, 8, 150, + 85, 104, 243, 123, 22, 25, 7, 42, 152, 226, 88, 93, 213, 201, 197, 235, 17, 185, 84, + 57, 161, 179, 37, 251, 129, 62, 214, 48, 161, 247, 124, 147, 120, 180, 197, 9, 83, 63, + 38, 182, 206, 20, 116, 28, 143, 134, 173, 55, 54, 1, 254, 86, 9, 67, 213, 144, 95, 48, + 29, 47, 119, 156, 188, 70, 18, 37, 202, 65, 173, 22, 177, 14, 244, 120, 166, 20, 82, + 203, 162, 24, 56, 240, 154, 249, 11, 31, 182, 55, 71, 171, 196, 3, 20, 151, 112, 159, + 131, 91, 201, 80, 116, 228, 6, 184, 140, 55, 205, 4, 91, 116, 46, 224, 32, 216, 146, + 46, 232, 75, 156, 109, 54, 193, 146, 248, 2, 49, 17, 102, 214, 150, 202, 1, 6, 110, 2, + 202, 34, 162, 107, 205, 241, 123, 83, 108, 152, 145, 231, 219, 158, 24, 171, 48, 121, + 2, 193, 15, 21, 9, 173, 172, 30, 111, 58, 94, 0, 18, 1, 140, 7, 52, 209, 214, 59, 127, + 228, 30, 154, 102, 119, 233, 170, 160, 120, 108, 154, 117, 28, 23, 70, 56, 35, 11, 9, + 39, 198, 86, 79, 119, 213, 88, 193, 5, 15, 214, 139, 199, 236, 243, 159, 171, 185, 22, + 35, 216, 20, 149, 137, 17, 74, 117, 165, 55, 116, 139, 171, 181, 204, 196, 104, 178, + 246, 103, 73, 249, 165, 108, 237, 234, 155, 133, 35, 139, 98, 136, 51, 220, 98, 181, + 40, 151, 80, 58, 242, 84, 206, 242, 199, 145, 156, 160, 61, 116, 100, 77, 89, 184, 213, + 119, 25, 4, 107, 26, 23, 87, 101, 85, 68, 115, 221, 171, 170, 123, 246, 26, 51, 108, + 13, 101, 217, 17, 0, 103, 110, 15, 136, 70, 255, 12, 197, 89, 225, 158, 25, 116, 32, + 53, 169, 122, 4, 32, 108, 30, 181, 180, 142, 250, 141, 217, 8, 33, 111, 89, 48, 196, + 90, 158, 207, 213, 165, 97, 196, 21, 193, 114, 199, 167, 123, 95, 118, 101, 107, 130, + 161, 186, 67, 27, 27, 161, 192, 157, 252, 98, 131, 186, 124, 64, 224, 199, 126, 47, + 230, 66, 92, 97, 38, 0, 35, 197, 178, 219, 121, 240, 149, 197, 18, 165, 39, 3, 137, 58, + 8, 227, 152, 124, 247, 103, 85, 150, 194, 140, 108, 96, 151, 57, 117, 182, 177, 47, + 168, 112, 184, 214, 42, 48, 65, 3, 30, 179, 187, 137, 179, 122, 98, 210, 3, 7, 102, + 235, 3, 145, 107, 200, 72, 4, 153, 135, 43, 202, 155, 170, 174, 148, 161, 37, 43, 219, + 16, 229, 65, 45, 225, 49, 126, 168, 215, 93, 153, 179, 49, 108, 51, 131, 227, 82, 134, + 77, 28, 175, 237, 136, 49, 137, 164, 197, 224, 42, 85, 250, 48, 109, 196, 70, 12, 236, + 65, 121, 254, 3, 37, 183, 201, 124, 246, 130, 174, 89, 7, 81, 235, 147, 148, 40, 147, + 174, 204, 99, 22, 150, 133, 195, 22, 108, 119, 59, 21, 104, 37, 251, 61, 92, 82, 9, + 122, 73, 48, 97, 82, 52, 170, 10, 79, 134, 17, 8, 247, 96, 108, 186, 230, 6, 138, 90, + 16, 36, 56, 45, 179, 121, 179, 17, 1, 174, 106, 4, 170, 81, 185, 60, 47, 201, 26, 67, + 199, 160, 52, 60, 63, 12, 82, 114, 122, 87, 166, 141, 168, 101, 177, 129, 197, 127, + 226, 166, 159, 168, 108, 218, 112, 31, 246, 4, 73, 134, 155, 197, 192, 8, 71, 135, 163, + 9, 122, 183, 127, 156, 60, 178, 124, 135, 129, 7, 185, 105, 232, 250, 199, 173, 80, 18, + 15, 118, 183, 3, 105, 56, 189, 107, 157, 44, 101, 103, 60, 99, 69, 187, 220, 146, 116, + 202, 85, 115, 219, 87, 241, 209, 39, 14, 184, 154, 236, 211, 31, 117, 106, 24, 68, 233, + 161, 49, 229, 105, 179, 58, 171, 215, 194, 178, 6, 243, 155, 2, 148, 202, 45, 145, 118, + 210, 233, 112, 208, 186, 157, 150, 68, 148, 94, 184, 49, 69, 36, 16, 33, 89, 36, 188, + 156, 122, 117, 124, 48, 83, 99, 153, 72, 148, 163, 14, 116, 15, 162, 169, 50, 10, 244, + 155, 136, 105, 76, 118, 209, 144, 130, 88, 102, 85, 234, 137, 222, 195, 37, 93, 169, + 179, 227, 195, 162, 203, 242, 19, 175, 252, 19, 197, 230, 97, 174, 167, 72, 10, 169, + 184, 26, 249, 41, 49, 199, 54, 205, 183, 178, 241, 5, 35, 142, 227, 196, 245, 2, 91, + 201, 6, 41, 62, 214, 158, 48, 39, 106, 189, 134, 27, 86, 2, 83, 27, 170, 30, 140, 103, + 7, 87, 65, 87, 163, 117, 188, 132, 38, 15, 116, 251, 23, 157, 113, 156, 165, 96, 41, + 112, 103, 3, 81, 200, 3, 166, 137, 98, 14, 86, 183, 200, 161, 100, 212, 199, 206, 167, + 114, 15, 235, 131, 103, 77, 103, 188, 254, 5, 30, 172, 183, 174, 176, 252, 9, 29, 90, + 78, 178, 188, 101, 158, 74, 200, 231, 75, 89, 233, 113, 206, 254, 122, 136, 41, 148, + 20, 10, 217, 144, 177, 108, 42, 124, 210, 7, 40, 138, 167, 223, 156, 151, 106, 236, 94, + 84, 183, 48, 39, 81, 77, 54, 129, 140, 133, 183, 107, 9, 8, 193, 99, 90, 185, 41, 229, + 43, 138, 11, 86, 161, 39, 124, 40, 114, 119, 122, 101, 124, 226, 227, 192, 241, 54, + 182, 89, 37, 186, 17, 90, 171, 245, 185, 119, 76, 21, 11, 170, 146, 46, 110, 228, 51, + 213, 162, 5, 193, 211, 153, 112, 148, 208, 43, 3, 254, 16, 173, 51, 88, 252, 219, 69, + 48, 242, 206, 198, 185, 105, 26, ]; assert!(expected_ssk == child_node.value.0.secret_spending_key); assert!(expected_ccc == child_node.ccc); - assert!(expected_nsk == child_node.value.0.private_key_holder.nullifier_secret_key); + assert!( + expected_ask + == child_node + .value + .0 + .private_key_holder + .authorization_secret_key + ); + assert!(expected_nsk == child_node.value.0.private_key_holder.nullifier_secret_key()); assert!(expected_npk == child_node.value.0.nullifier_public_key); assert!(expected_vsk == child_node.value.0.private_key_holder.viewing_secret_key); assert!(expected_vpk == child_node.value.0.viewing_public_key.to_bytes()); diff --git a/lee/key_protocol/src/key_management/secret_holders.rs b/lee/key_protocol/src/key_management/secret_holders.rs index b8225a4b..088b49ff 100644 --- a/lee/key_protocol/src/key_management/secret_holders.rs +++ b/lee/key_protocol/src/key_management/secret_holders.rs @@ -1,6 +1,8 @@ use bip39::Mnemonic; use common::HashType; -use lee_core::{NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey}; +use lee_core::{ + AuthorizationSecretKey, NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey, +}; use ml_kem; use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; @@ -36,7 +38,7 @@ impl ViewingSecretKey { /// for recepient. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PrivateKeyHolder { - pub nullifier_secret_key: NullifierSecretKey, + pub authorization_secret_key: AuthorizationSecretKey, pub viewing_secret_key: ViewingSecretKey, } @@ -87,41 +89,35 @@ impl SeedHolder { impl SecretSpendingKey { #[must_use] #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] - pub fn generate_nullifier_secret_key(&self, index: Option) -> NullifierSecretKey { - const PREFIX: &[u8; 8] = b"LEE/keys"; - const SUFFIX_1: &[u8; 1] = &[1]; - const SUFFIX_2: &[u8; 19] = &[0; 19]; + pub fn generate_authorization_secret_key(&self, index: Option) -> AuthorizationSecretKey { + const DOMAIN: &[u8; 35] = b"/LEE/v0.3/Keys/Authorization/Secret"; let index = index.unwrap_or(0); let mut hasher = sha2::Sha256::new(); - hasher.update(PREFIX); + hasher.update(DOMAIN); hasher.update(self.0); - hasher.update(SUFFIX_1); hasher.update(index.to_be_bytes()); - hasher.update(SUFFIX_2); - ::from(hasher.finalize_fixed()) + AuthorizationSecretKey(hasher.finalize_fixed().into()) + } + + #[must_use] + pub fn generate_nullifier_secret_key(&self, index: Option) -> NullifierSecretKey { + ::from(&self.generate_authorization_secret_key(index)) } #[must_use] #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] pub fn generate_viewing_secret_seed_key(&self, index: Option) -> ViewingSecretKey { - const PREFIX: &[u8; 8] = b"LEE/keys"; - const SUFFIX_1: &[u8; 1] = &[2]; - const SUFFIX_2: &[u8; 19] = &[0; 19]; + const DOMAIN: &[u8; 29] = b"/LEE/v0.3/Keys/Viewing/Secret"; let index = index.unwrap_or(0); - let mut bytes: Vec = Vec::with_capacity(64); - bytes.extend_from_slice(PREFIX); - bytes.extend_from_slice(&self.0); - bytes.extend_from_slice(SUFFIX_1); - bytes.extend_from_slice(&index.to_be_bytes()); - bytes.extend_from_slice(SUFFIX_2); - let bytes: [u8; 64] = bytes - .try_into() - .expect("`generate_viewing_secret_seed_key`: bytes must be exactly 64"); + let mut bytes = [0_u8; 29 + 32 + 4]; + bytes[..29].copy_from_slice(DOMAIN); + bytes[29..61].copy_from_slice(&self.0); + bytes[61..].copy_from_slice(&index.to_be_bytes()); let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed"); @@ -139,7 +135,7 @@ impl SecretSpendingKey { #[must_use] pub fn produce_private_key_holder(&self, index: Option) -> PrivateKeyHolder { PrivateKeyHolder { - nullifier_secret_key: self.generate_nullifier_secret_key(index), + authorization_secret_key: self.generate_authorization_secret_key(index), viewing_secret_key: self.generate_viewing_secret_seed_key(index), } } @@ -158,9 +154,14 @@ impl From<&ViewingSecretKey> for ViewingPublicKey { } impl PrivateKeyHolder { + #[must_use] + pub fn nullifier_secret_key(&self) -> NullifierSecretKey { + (&self.authorization_secret_key).into() + } + #[must_use] pub fn generate_nullifier_public_key(&self) -> NullifierPublicKey { - (&self.nullifier_secret_key).into() + NullifierPublicKey::from(&self.nullifier_secret_key()) } #[must_use] diff --git a/lee/privacy_preserving_circuit/src/output.rs b/lee/privacy_preserving_circuit/src/output.rs index 3ff6d9cb..a01deef2 100644 --- a/lee/privacy_preserving_circuit/src/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -1,8 +1,8 @@ use lee_core::{ Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme, - EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierSecretKey, - NullifierWitness, PrivacyPreservingCircuitOutput, PrivateAccountKind, PrivateAction, - PrivateWitness, PublicAction, SharedSecretKey, WitnessKind, + EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, + NullifierSecretKey, NullifierWitness, PrivacyPreservingCircuitOutput, PrivateAccountKind, + PrivateAction, PrivateWitness, PublicAction, SharedSecretKey, WitnessKind, account::{Account, AccountId, Nonce}, compute_digest_for_path, encryption::{ViewTag, ViewingPublicKey}, @@ -48,7 +48,7 @@ pub fn compute_circuit_output( nullifier, }) => { let account_id = match kind { - WitnessKind::Regular => { + WitnessKind::Regular { .. } => { let derived = AccountId::for_regular_private_account( &nullifier.npk(), vpk, @@ -68,12 +68,31 @@ pub fn compute_circuit_output( match (kind, nullifier) { ( - WitnessKind::Regular, + WitnessKind::Regular { ask }, NullifierWitness::Init { .. } | NullifierWitness::Update { .. }, - ) => assert!( - pre_state.is_authorized, - "Regular private account pre-state must be authorized" - ), + ) => { + if let Some(ask) = ask { + let derived = NullifierSecretKey::from(ask); + match nullifier { + // Check that the authorization key is actually bound to the + // account Id. + NullifierWitness::Update { nsk, .. } => assert_eq!( + derived, *nsk, + "Authorization secret key does not derive this account's nullifier secret key" + ), + NullifierWitness::Init { npk, .. } => assert_eq!( + NullifierPublicKey::from(&derived), + *npk, + "Authorization secret key does not derive this account's nullifier public key" + ), + } + } + assert_eq!( + pre_state.is_authorized, + ask.is_some(), + "Regular private account authorization must match the supplied credential" + ); + } (WitnessKind::Pda { .. }, NullifierWitness::Init { .. }) => assert!( !pre_state.is_authorized, "Private PDA init requires unauthorized pre_state" @@ -126,7 +145,7 @@ pub fn compute_circuit_output( }; let account_kind = match kind { - WitnessKind::Regular => PrivateAccountKind::Regular(*identifier), + WitnessKind::Regular { .. } => PrivateAccountKind::Regular(*identifier), WitnessKind::Pda { .. } => { let (authority_program_id, seed) = pda_seed_by_position .get(&pos) diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 1e3b3515..e3016290 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -2,8 +2,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use crate::{ - Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey, - NullifierSecretKey, + AuthorizationSecretKey, Commitment, CommitmentSetDigest, Identifier, MembershipProof, + Nullifier, NullifierPublicKey, NullifierSecretKey, account::{Account, AccountWithMetadata}, encryption::{EncryptedAccountData, ViewTag, ViewingPublicKey}, program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow}, @@ -48,8 +48,9 @@ pub struct PrivateWitness { pub enum WitnessKind { /// Standalone private account. The `account_id` is derived as /// `AccountId::for_regular_private_account(&npk, vpk, identifier)` and matched against - /// `pre_state.account_id`. - Regular, + /// `pre_state.account_id`. An honest authorized account's `npk` for Id computation gets + /// derived from the supplied `ask`. + Regular { ask: Option }, /// Private PDA. The npk-to-account_id binding is proven upstream via `Claim::Pda(seed)` or a /// caller's `pda_seeds` match. The identifier diversifies the PDA within the /// `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it as the 4th input. diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index f6944ec8..ab7b40f3 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -15,7 +15,9 @@ pub use encryption::{ EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey, ML_KEM_768_CIPHERTEXT_LEN, SharedSecretKey, ViewTag, }; -pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey}; +pub use nullifier::{ + AuthorizationSecretKey, Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey, +}; pub use program::PrivateAccountKind; pub mod account; diff --git a/lee/state_machine/core/src/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index 755a2adc..1c0c5e17 100644 --- a/lee/state_machine/core/src/nullifier.rs +++ b/lee/state_machine/core/src/nullifier.rs @@ -48,16 +48,29 @@ impl AsRef<[u8]> for NullifierPublicKey { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[cfg_attr(any(feature = "host", test), derive(Hash))] +pub struct AuthorizationSecretKey(pub [u8; 32]); + +impl From<&AuthorizationSecretKey> for NullifierSecretKey { + fn from(value: &AuthorizationSecretKey) -> Self { + const DOMAIN: &[u8; 31] = b"/LEE/v0.3/Keys/Nullifier/Secret"; + let mut bytes = [0_u8; 31 + 32]; + bytes[..31].copy_from_slice(DOMAIN); + bytes[31..].copy_from_slice(&value.0); + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("hash should be exactly 32 bytes long") + } +} + impl From<&NullifierSecretKey> for NullifierPublicKey { fn from(value: &NullifierSecretKey) -> Self { - const PREFIX: &[u8; 8] = b"LEE/keys"; - const SUFFIX_1: &[u8; 1] = &[7]; - const SUFFIX_2: &[u8; 23] = &[0; 23]; - let mut bytes = Vec::new(); - bytes.extend_from_slice(PREFIX); - bytes.extend_from_slice(value); - bytes.extend_from_slice(SUFFIX_1); - bytes.extend_from_slice(SUFFIX_2); + const DOMAIN: &[u8; 31] = b"/LEE/v0.3/Keys/Nullifier/Public"; + let mut bytes = [0_u8; 31 + 32]; + bytes[..31].copy_from_slice(DOMAIN); + bytes[31..].copy_from_slice(value); Self( Impl::hash_bytes(&bytes) .as_bytes() @@ -154,6 +167,17 @@ mod tests { assert_eq!(nullifier, expected_nullifier); } + #[test] + fn from_authorization_key() { + let ask = AuthorizationSecretKey([0; 32]); + let expected_nsk: NullifierSecretKey = [ + 31, 33, 90, 89, 193, 14, 149, 46, 107, 38, 51, 65, 178, 242, 118, 11, 235, 198, 242, + 144, 192, 64, 39, 205, 244, 122, 210, 55, 11, 245, 117, 29, + ]; + let nsk = NullifierSecretKey::from(&ask); + assert_eq!(nsk, expected_nsk); + } + #[test] fn from_secret_key() { let nsk = [ @@ -161,8 +185,8 @@ mod tests { 196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28, ]; let expected_npk = NullifierPublicKey([ - 78, 20, 20, 5, 177, 198, 233, 100, 175, 134, 174, 200, 24, 205, 68, 215, 130, 74, 35, - 54, 154, 184, 219, 42, 168, 106, 126, 147, 133, 244, 18, 218, + 58, 181, 207, 24, 227, 133, 192, 231, 242, 216, 230, 219, 31, 227, 236, 94, 99, 245, + 206, 251, 237, 189, 88, 218, 215, 106, 66, 227, 136, 152, 140, 218, ]); let npk = NullifierPublicKey::from(&nsk); assert_eq!(npk, expected_npk); @@ -177,8 +201,8 @@ mod tests { let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let expected_account_id = AccountId::new([ - 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, + 226, 149, 99, 147, 82, 211, 97, 152, 31, 46, 87, 113, 237, 244, 197, 108, 71, 191, 161, + 199, 140, 177, 247, 73, 95, 64, 202, 90, 8, 157, 188, 147, ]); let account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); @@ -195,8 +219,8 @@ mod tests { let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let expected_account_id = AccountId::new([ - 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, + 44, 36, 222, 50, 57, 159, 215, 6, 246, 54, 45, 150, 94, 148, 148, 71, 212, 113, 165, + 10, 187, 162, 184, 70, 96, 35, 42, 230, 251, 72, 237, 80, ]); let account_id = AccountId::for_regular_private_account(&npk, &vpk, 1); @@ -214,8 +238,8 @@ mod tests { let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); let expected_account_id = AccountId::new([ - 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, + 50, 122, 67, 122, 59, 36, 150, 204, 128, 54, 55, 152, 14, 220, 163, 211, 246, 221, 197, + 75, 12, 199, 163, 151, 234, 194, 188, 13, 27, 120, 249, 220, ]); let account_id = AccountId::for_regular_private_account(&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 9a0bb94f..299ebcd8 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs @@ -93,7 +93,9 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -151,7 +153,7 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { commitment_set.extend(std::slice::from_ref(&commitment_sender)); let expected_new_nullifiers = vec![ ( - Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk), + Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk()), commitment_set.digest(), ), ( @@ -165,7 +167,7 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { let expected_private_account_1 = Account { program_owner: program.id(), balance: 100 - balance_to_move, - nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()), ..Default::default() }; let expected_private_account_2 = Account { @@ -182,7 +184,7 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { let esk_1 = EphemeralSecretKey::new( &sender_account_id, &[0; 32], - &sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + &sender_nonce.private_account_nonce_increment(&sender_keys.nsk()), ); let shared_secret_1 = SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &esk_1).0; @@ -199,10 +201,12 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: commitment_set .get_proof_for(&commitment_sender) .expect("sender's commitment must be in the set"), @@ -212,7 +216,9 @@ fn prove_privacy_preserving_execution_circuit_fully_private() { vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -284,7 +290,9 @@ fn init_note_view_tag_is_derived_from_account_keys() { vpk: keys.vpk(), random_seed: [0; 32], identifier, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(keys.ask), + }, nullifier: NullifierWitness::Init { npk: keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -329,10 +337,12 @@ fn update_note_view_tag_is_the_supplied_value() { vpk: keys.vpk(), random_seed: [0; 32], identifier, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: fed_tag, - nsk: keys.nsk, + nsk: keys.nsk(), membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), }, })], @@ -381,7 +391,9 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { vpk: account_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(account_keys.ask), + }, nullifier: NullifierWitness::Init { npk: account_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -574,7 +586,9 @@ fn shared_account_receives_via_simple_transfer() { vpk: shared_keys.vpk(), random_seed: [0; 32], identifier: shared_identifier, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(shared_keys.ask), + }, nullifier: NullifierWitness::Init { npk: shared_npk, commitment_root: DUMMY_COMMITMENT_HASH, @@ -613,9 +627,11 @@ fn private_authorized_init_encrypts_regular_kind_with_identifier() { vpk: keys.vpk(), random_seed: [0; 32], identifier, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(keys.ask), + }, nullifier: NullifierWitness::Init { - npk: NullifierPublicKey::from(&keys.nsk), + npk: NullifierPublicKey::from(&keys.nsk()), commitment_root: DUMMY_COMMITMENT_HASH, }, })], @@ -653,7 +669,9 @@ fn private_foreign_init_encrypts_regular_kind_with_identifier() { vpk: keys.vpk(), random_seed: [0; 32], identifier, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(keys.ask), + }, nullifier: NullifierWitness::Init { npk: keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -680,7 +698,7 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() { let esk = EphemeralSecretKey::new( &account_id, &[0; 32], - &Nonce::default().private_account_nonce_increment(&keys.nsk), + &Nonce::default().private_account_nonce_increment(&keys.nsk()), ); let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; let account = Account { @@ -701,10 +719,12 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() { vpk: keys.vpk(), random_seed: [0; 32], identifier, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: keys.nsk, + nsk: keys.nsk(), membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), }, })], @@ -718,6 +738,215 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() { ); } +/// Builds an on-chain regular private account owned by `program`, returning its id, pre-state +/// and a membership proof for its commitment. +fn seeded_regular_account( + keys: &crate::state::tests::TestPrivateKeys, + program: &Program, + identifier: u128, +) -> (AccountId, AccountWithMetadata, lee_core::MembershipProof) { + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); + let account = Account { + program_owner: program.id(), + balance: 1, + ..Account::default() + }; + let commitment = Commitment::new(&account_id, &account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&commitment)); + let proof = commitment_set.get_proof_for(&commitment).unwrap(); + ( + account_id, + AccountWithMetadata::new(account, false, account_id), + proof, + ) +} + +/// Spending without consenting. The witness carries no `ask`, so the pre-state is unauthorized, +/// and the nullifier is still produced from the `nsk`. +#[test] +fn private_regular_update_without_ask_is_spendable() { + let program = crate::test_methods::noop(); + let keys = test_private_account_keys_1(); + let (_, pre, membership_proof) = seeded_regular_account(&keys, &program, 0); + assert!(!pre.is_authorized); + + execute_and_prove( + vec![pre], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier: 0, + kind: WitnessKind::Regular { ask: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk(), + membership_proof, + }, + })], + &program.into(), + ) + .unwrap(); +} + +/// Claiming authorization without supplying an `ask` is rejected. +#[test] +fn private_regular_witness_without_ask_cannot_assert_authorization() { + let program = crate::test_methods::noop(); + let keys = test_private_account_keys_1(); + let (account_id, pre, membership_proof) = seeded_regular_account(&keys, &program, 0); + let pre = AccountWithMetadata::new(pre.account, true, account_id); + + let result = execute_and_prove( + vec![pre], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier: 0, + kind: WitnessKind::Regular { ask: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk(), + membership_proof, + }, + })], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// An `ask` that does not derive this account's `nsk` is not a credential for it. +#[test] +fn regular_update_with_wrong_ask_nsk_is_rejected() { + let program = crate::test_methods::noop(); + let keys = test_private_account_keys_1(); + let foreign = test_private_account_keys_2(); + let (account_id, pre, membership_proof) = seeded_regular_account(&keys, &program, 0); + let pre = AccountWithMetadata::new(pre.account, true, account_id); + + let result = execute_and_prove( + vec![pre], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier: 0, + kind: WitnessKind::Regular { + ask: Some(foreign.ask), + }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk(), + membership_proof, + }, + })], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// An `ask` that does not derive this account's `npk` is not a credential for it. +#[test] +fn regular_init_with_non_chaining_ask_npk_is_rejected() { + let program = crate::test_methods::claimer(); + let keys = test_private_account_keys_1(); + let foreign = test_private_account_keys_2(); + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0); + let pre = AccountWithMetadata::new(Account::default(), true, account_id); + + let result = execute_and_prove( + vec![pre], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier: 0, + kind: WitnessKind::Regular { + ask: Some(foreign.ask), + }, + nullifier: NullifierWitness::Init { + npk: keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn unauthorized_private_init_can_be_claimed() { + let program = crate::test_methods::claimer(); + let program_id = program.id(); + let keys = test_private_account_keys_1(); + let recipient_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id); + 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 (output, _) = execute_and_prove( + vec![recipient], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier: 0, + kind: WitnessKind::Regular { ask: None }, + nullifier: NullifierWitness::Init { + npk: keys.npk(), + commitment_root: DUMMY_COMMITMENT_HASH, + }, + })], + &program.into(), + ) + .unwrap(); + + let (_, claimed) = EncryptionScheme::decrypt( + &output.private_actions[0].encrypted_post_state.ciphertext, + &ssk, + &output.private_actions[0].nullifier, + ) + .unwrap(); + assert_eq!(claimed.program_owner, program_id); +} + +/// A program that asserts authorization over its pre-states rejects a regular private account +/// whose witness supplied no `ask`. +#[test] +fn auth_asserting_program_rejects_unauthorized_regular_private_account() { + let program = crate::test_methods::auth_asserting_noop(); + let keys = test_private_account_keys_1(); + let (_, pre, membership_proof) = seeded_regular_account(&keys, &program, 0); + + let result = execute_and_prove( + vec![pre], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Private(PrivateWitness { + vpk: keys.vpk(), + random_seed: [0; 32], + identifier: 0, + kind: WitnessKind::Regular { ask: None }, + nullifier: NullifierWitness::Update { + view_tag: 0, + nsk: keys.nsk(), + membership_proof, + }, + })], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::ProgramProveFailed(_)))); +} + /// A private-PDA update with a non-default identifier produces a ciphertext that decrypts /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. #[test] @@ -733,7 +962,7 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() { let esk = EphemeralSecretKey::new( &pda_id, &[0; 32], - &Nonce::default().private_account_nonce_increment(&keys.nsk), + &Nonce::default().private_account_nonce_increment(&keys.nsk()), ); let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; let pda_account = Account { @@ -764,7 +993,7 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() { kind: WitnessKind::Pda { binding: None }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: keys.nsk, + nsk: keys.nsk(), membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), }, }), @@ -847,7 +1076,7 @@ fn private_pda_update_identifier_mismatch_fails() { kind: WitnessKind::Pda { binding: None }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: keys.nsk, + nsk: keys.nsk(), membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), }, }), diff --git a/lee/state_machine/src/state/tests/changer_claimer.rs b/lee/state_machine/src/state/tests/changer_claimer.rs index 16b3872e..b422d119 100644 --- a/lee/state_machine/src/state/tests/changer_claimer.rs +++ b/lee/state_machine/src/state/tests/changer_claimer.rs @@ -75,10 +75,12 @@ fn private_changer_claimer_no_data_change_no_claim_succeeds() { vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (0, vec![]), }, })], @@ -109,10 +111,12 @@ fn private_changer_claimer_data_change_no_claim_fails() { vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (0, vec![]), }, })], diff --git a/lee/state_machine/src/state/tests/circuit.rs b/lee/state_machine/src/state/tests/circuit.rs index f71f9e09..758a7c85 100644 --- a/lee/state_machine/src/state/tests/circuit.rs +++ b/lee/state_machine/src/state/tests/circuit.rs @@ -65,10 +65,12 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() { vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: recipient_keys.nsk, + nsk: recipient_keys.nsk(), membership_proof: (0, vec![]), }, }), @@ -76,7 +78,9 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() { vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -121,10 +125,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (0, vec![]), }, }), @@ -132,7 +138,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -177,10 +185,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_ vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (0, vec![]), }, }), @@ -188,7 +198,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_ vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -233,10 +245,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided( vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (0, vec![]), }, }), @@ -244,7 +258,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided( vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -289,10 +305,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (0, vec![]), }, }), @@ -300,7 +318,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -343,10 +363,12 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (0, vec![]), }, }), @@ -354,7 +376,9 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -749,10 +773,12 @@ fn circuit_should_fail_if_there_are_repeated_ids() { vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (1, vec![]), }, }), @@ -760,10 +786,12 @@ fn circuit_should_fail_if_there_are_repeated_ids() { vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: (1, vec![]), }, }), @@ -802,9 +830,11 @@ fn private_authorized_uninitialized_account() { vpk: private_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(private_keys.ask), + }, nullifier: NullifierWitness::Init { - npk: NullifierPublicKey::from(&private_keys.nsk), + npk: NullifierPublicKey::from(&private_keys.nsk()), commitment_root: DUMMY_COMMITMENT_HASH, }, })], @@ -851,7 +881,9 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() { vpk: private_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(private_keys.ask), + }, nullifier: NullifierWitness::Init { npk: private_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -904,9 +936,11 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { vpk: private_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(private_keys.ask), + }, nullifier: NullifierWitness::Init { - npk: NullifierPublicKey::from(&private_keys.nsk), + npk: NullifierPublicKey::from(&private_keys.nsk()), commitment_root: DUMMY_COMMITMENT_HASH, }, })], @@ -949,9 +983,11 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() { vpk: private_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(private_keys.ask), + }, nullifier: NullifierWitness::Init { - npk: NullifierPublicKey::from(&private_keys.nsk), + npk: NullifierPublicKey::from(&private_keys.nsk()), commitment_root: DUMMY_COMMITMENT_HASH, }, })], @@ -1104,7 +1140,7 @@ fn two_private_pda_family_members_receive_and_spend() { kind: WitnessKind::Pda { binding: None }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: alice_keys.nsk, + nsk: alice_keys.nsk(), membership_proof: state .get_proof_for_commitment(&commitment_pda_0) .expect("pda_0 must be in state"), @@ -1143,7 +1179,7 @@ fn two_private_pda_family_members_receive_and_spend() { kind: WitnessKind::Pda { binding: None }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: alice_keys.nsk, + nsk: alice_keys.nsk(), membership_proof: state .get_proof_for_commitment(&commitment_pda_1) .expect("pda_1 must be in state"), @@ -1174,7 +1210,7 @@ fn two_private_pda_family_members_receive_and_spend() { balance: 0, nonce: alice_pda_1_account .nonce - .private_account_nonce_increment(&alice_keys.nsk), + .private_account_nonce_increment(&alice_keys.nsk()), ..Account::default() }; let commitment_pda_1_after_spend = @@ -1199,7 +1235,7 @@ fn two_private_pda_family_members_receive_and_spend() { }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: alice_keys.nsk, + nsk: alice_keys.nsk(), membership_proof: state .get_proof_for_commitment(&commitment_pda_1_after_spend) .expect("pda_1 after spend must be in state"), diff --git a/lee/state_machine/src/state/tests/claiming.rs b/lee/state_machine/src/state/tests/claiming.rs index 68c3cf5e..1d63fdb3 100644 --- a/lee/state_machine/src/state/tests/claiming.rs +++ b/lee/state_machine/src/state/tests/claiming.rs @@ -329,10 +329,12 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() { vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: state .get_proof_for_commitment(&sender_commitment) .expect("sender's commitment must be in state"), @@ -353,7 +355,7 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() { .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); - let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk); + let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk()); assert!(state.private_state.1.contains(&nullifier)); assert_eq!( @@ -420,8 +422,8 @@ fn private_chained_call(number_of_calls: u32) { dependencies.insert(simple_transfers.id(), simple_transfers); let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies); - let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk); - let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk); + let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk()); + let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk()); let from_expected_post = Account { balance: initial_balance - u128::from(number_of_calls) * amount, @@ -446,10 +448,12 @@ fn private_chained_call(number_of_calls: u32) { vpk: from_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(from_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: from_keys.nsk, + nsk: from_keys.nsk(), membership_proof: state .get_proof_for_commitment(&from_commitment) .expect("from's commitment must be in state"), @@ -459,10 +463,12 @@ fn private_chained_call(number_of_calls: u32) { vpk: to_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(to_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: to_keys.nsk, + nsk: to_keys.nsk(), membership_proof: state .get_proof_for_commitment(&to_commitment) .expect("to's commitment must be in state"), diff --git a/lee/state_machine/src/state/tests/mod.rs b/lee/state_machine/src/state/tests/mod.rs index 878f722c..b1c67b9b 100644 --- a/lee/state_machine/src/state/tests/mod.rs +++ b/lee/state_machine/src/state/tests/mod.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; use lee_core::{ - BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, - NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, Timestamp, + AuthorizationSecretKey, BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, + Nullifier, NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, Timestamp, WitnessKind, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, @@ -138,14 +138,18 @@ impl TestPublicKeys { } pub struct TestPrivateKeys { - pub nsk: NullifierSecretKey, + pub ask: AuthorizationSecretKey, pub d: [u8; 32], pub z: [u8; 32], } impl TestPrivateKeys { + pub fn nsk(&self) -> NullifierSecretKey { + (&self.ask).into() + } + pub fn npk(&self) -> NullifierPublicKey { - NullifierPublicKey::from(&self.nsk) + NullifierPublicKey::from(&self.nsk()) } pub fn vpk(&self) -> ViewingPublicKey { @@ -241,7 +245,7 @@ fn test_public_account_keys_2() -> TestPublicKeys { pub fn test_private_account_keys_1() -> TestPrivateKeys { TestPrivateKeys { - nsk: [13; 32], + ask: AuthorizationSecretKey([13; 32]), d: [31; 32], z: [32; 32], } @@ -249,7 +253,7 @@ pub fn test_private_account_keys_1() -> TestPrivateKeys { pub fn test_private_account_keys_2() -> TestPrivateKeys { TestPrivateKeys { - nsk: [38; 32], + ask: AuthorizationSecretKey([38; 32]), d: [83; 32], z: [84; 32], } @@ -284,7 +288,9 @@ fn shielded_balance_transfer_for_tests( vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -331,10 +337,12 @@ fn private_balance_transfer_for_tests( vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: state .get_proof_for_commitment(&sender_commitment) .expect("sender's commitment must be in state"), @@ -344,7 +352,9 @@ fn private_balance_transfer_for_tests( vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Init { npk: recipient_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -392,10 +402,12 @@ fn deshielded_balance_transfer_for_tests( vpk: sender_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(sender_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: sender_keys.nsk, + nsk: sender_keys.nsk(), membership_proof: state .get_proof_for_commitment(&sender_commitment) .expect("sender's commitment must be in state"), diff --git a/lee/state_machine/src/state/tests/privacy_preserving.rs b/lee/state_machine/src/state/tests/privacy_preserving.rs index afc9d88a..d3262d9b 100644 --- a/lee/state_machine/src/state/tests/privacy_preserving.rs +++ b/lee/state_machine/src/state/tests/privacy_preserving.rs @@ -76,7 +76,7 @@ fn transition_from_privacy_preserving_transaction_private() { &sender_account_id, &Account { program_owner: crate::test_methods::simple_balance_transfer().id(), - nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()), balance: sender_private_account.balance - balance_to_move, data: Data::default(), }, @@ -84,7 +84,7 @@ fn transition_from_privacy_preserving_transaction_private() { let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); let expected_new_nullifier = - Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); + Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk()); let expected_new_commitment_2 = Commitment::new( &recipient_account_id, @@ -211,7 +211,7 @@ fn transition_from_privacy_preserving_transaction_deshielded() { &sender_account_id, &Account { program_owner: crate::test_methods::simple_balance_transfer().id(), - nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()), balance: sender_private_account.balance - balance_to_move, data: Data::default(), }, @@ -219,7 +219,7 @@ fn transition_from_privacy_preserving_transaction_deshielded() { let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); let expected_new_nullifier = - Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); + Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk()); assert!(state.private_state.0.contains(&sender_pre_commitment)); assert!(!state.private_state.0.contains(&expected_new_commitment)); @@ -525,10 +525,12 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { vpk: recipient_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(recipient_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: recipient_keys.nsk, + nsk: recipient_keys.nsk(), membership_proof: state .get_proof_for_commitment(&recipient_commitment) .expect("recipient's commitment must be in state"), diff --git a/lee/state_machine/src/state/tests/validity_window.rs b/lee/state_machine/src/state/tests/validity_window.rs index c39571ea..7953c671 100644 --- a/lee/state_machine/src/state/tests/validity_window.rs +++ b/lee/state_machine/src/state/tests/validity_window.rs @@ -142,7 +142,9 @@ fn validity_window_works_in_privacy_preserving_transactions( vpk: account_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(account_keys.ask), + }, nullifier: NullifierWitness::Init { npk: account_keys.npk(), commitment_root: DUMMY_COMMITMENT_HASH, @@ -210,7 +212,9 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions( vpk: account_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(account_keys.ask), + }, nullifier: NullifierWitness::Init { npk: account_keys.npk(), 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 b3db107a..003b2780 100644 --- a/lee/state_machine/src/validated_state_diff/tests.rs +++ b/lee/state_machine/src/validated_state_diff/tests.rs @@ -168,10 +168,12 @@ fn privacy_malicious_programs_cannot_drain_public_victim() { vpk: attacker_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(attacker_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: attacker_keys.nsk, + nsk: attacker_keys.nsk(), membership_proof, }, }), @@ -330,10 +332,12 @@ fn privacy_malicious_programs_cannot_drain_private_victim() { vpk: attacker_keys.vpk(), random_seed: [0; 32], identifier: 0, - kind: WitnessKind::Regular, + kind: WitnessKind::Regular { + ask: Some(attacker_keys.ask), + }, nullifier: NullifierWitness::Update { view_tag: 0, - nsk: attacker_keys.nsk, + nsk: attacker_keys.nsk(), membership_proof, }, }), diff --git a/lez/testnet_initial_state/src/lib.rs b/lez/testnet_initial_state/src/lib.rs index f77a083f..8ea71e22 100644 --- a/lez/testnet_initial_state/src/lib.rs +++ b/lez/testnet_initial_state/src/lib.rs @@ -1,12 +1,9 @@ use std::collections::HashMap; use key_protocol::key_management::{ - KeyChain, - key_tree::chain_index::ChainIndex, - secret_holders::{PrivateKeyHolder, SecretSpendingKey, ViewingSecretKey}, + KeyChain, key_tree::chain_index::ChainIndex, secret_holders::SecretSpendingKey, }; use lee::{Account, AccountId, Data, PrivateKey, PublicKey, V03State, program::Program}; -use lee_core::{NullifierPublicKey, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; const PRIVATE_KEY_PUB_ACC_A: [u8; 32] = [ @@ -29,46 +26,6 @@ const SSK_PRIV_ACC_B: [u8; 32] = [ 180, 43, 120, 55, 151, 50, 21, 113, 22, 254, 83, 148, 56, ]; -const NSK_PRIV_ACC_A: [u8; 32] = [ - 25, 21, 186, 59, 180, 224, 101, 64, 163, 208, 228, 43, 13, 185, 100, 123, 156, 47, 80, 179, 72, - 51, 115, 11, 180, 99, 21, 201, 48, 194, 118, 144, -]; - -const NSK_PRIV_ACC_B: [u8; 32] = [ - 99, 82, 190, 140, 234, 10, 61, 163, 15, 211, 179, 54, 70, 166, 87, 5, 182, 68, 117, 244, 217, - 23, 99, 9, 4, 177, 230, 125, 109, 91, 160, 30, -]; - -const VSK_D_PRIV_ACC_A: [u8; 32] = [ - 255, 250, 140, 26, 222, 223, 174, 95, 132, 108, 124, 88, 30, 247, 82, 72, 52, 70, 84, 139, 241, - 187, 41, 163, 19, 231, 232, 122, 225, 55, 134, 184, -]; - -const VSK_Z_PRIV_ACC_A: [u8; 32] = [ - 225, 24, 98, 78, 31, 203, 175, 248, 213, 17, 133, 207, 10, 135, 132, 151, 59, 184, 5, 81, 28, - 238, 137, 62, 233, 227, 99, 17, 236, 159, 244, 63, -]; - -const VSK_D_PRIV_ACC_B: [u8; 32] = [ - 128, 85, 85, 103, 226, 218, 119, 56, 60, 252, 31, 113, 232, 215, 156, 2, 159, 247, 156, 192, - 12, 178, 229, 236, 255, 120, 146, 211, 169, 117, 153, 180, -]; - -const VSK_Z_PRIV_ACC_B: [u8; 32] = [ - 165, 80, 169, 87, 248, 88, 167, 154, 27, 67, 131, 122, 50, 130, 111, 40, 164, 180, 204, 75, - 188, 140, 110, 132, 113, 133, 222, 8, 49, 123, 187, 18, -]; - -const NPK_PRIV_ACC_A: [u8; 32] = [ - 167, 108, 50, 153, 74, 47, 151, 188, 140, 79, 195, 31, 181, 9, 40, 167, 201, 32, 175, 129, 45, - 245, 223, 193, 210, 170, 247, 128, 167, 140, 155, 129, -]; - -const NPK_PRIV_ACC_B: [u8; 32] = [ - 32, 67, 72, 164, 106, 53, 66, 239, 141, 15, 52, 230, 136, 177, 2, 236, 207, 243, 134, 135, 210, - 143, 87, 232, 215, 128, 194, 120, 113, 224, 4, 165, -]; - const DEFAULT_PROGRAM_OWNER: [u32; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; const PUB_ACC_A_INITIAL_BALANCE: u128 = 10000; @@ -133,26 +90,23 @@ pub fn initial_pub_accounts_private_keys() -> Vec Vec { - let key_chain_1 = KeyChain { - secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_A), - private_key_holder: PrivateKeyHolder { - nullifier_secret_key: NSK_PRIV_ACC_A, - viewing_secret_key: ViewingSecretKey::new(VSK_D_PRIV_ACC_A, VSK_Z_PRIV_ACC_A), - }, - nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_A), - viewing_public_key: ViewingPublicKey::from_seed(&VSK_D_PRIV_ACC_A, &VSK_Z_PRIV_ACC_A), - }; +fn key_chain_from_ssk(ssk: [u8; 32]) -> KeyChain { + let secret_spending_key = SecretSpendingKey(ssk); + let private_key_holder = secret_spending_key.produce_private_key_holder(None); + let nullifier_public_key = private_key_holder.generate_nullifier_public_key(); + let viewing_public_key = private_key_holder.generate_viewing_public_key(); - let key_chain_2 = KeyChain { - secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_B), - private_key_holder: PrivateKeyHolder { - nullifier_secret_key: NSK_PRIV_ACC_B, - viewing_secret_key: ViewingSecretKey::new(VSK_D_PRIV_ACC_B, VSK_Z_PRIV_ACC_B), - }, - nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_B), - viewing_public_key: ViewingPublicKey::from_seed(&VSK_D_PRIV_ACC_B, &VSK_Z_PRIV_ACC_B), - }; + KeyChain { + secret_spending_key, + private_key_holder, + nullifier_public_key, + viewing_public_key, + } +} + +fn initial_priv_accounts_private_keys() -> Vec { + let key_chain_1 = key_chain_from_ssk(SSK_PRIV_ACC_A); + let key_chain_2 = key_chain_from_ssk(SSK_PRIV_ACC_B); vec![ PrivateAccountPrivateInitialData { @@ -313,13 +267,35 @@ pub fn initial_state_testnet() -> V03State { mod tests { use std::str::FromStr as _; + use key_protocol::key_management::secret_holders::ViewingSecretKey; + use super::*; + const VSK_D_PRIV_ACC_A: [u8; 32] = [ + 4, 118, 187, 42, 14, 254, 144, 150, 125, 176, 205, 240, 109, 81, 234, 177, 244, 236, 108, + 71, 107, 10, 107, 169, 95, 134, 75, 193, 213, 57, 81, 218, + ]; + + const VSK_Z_PRIV_ACC_A: [u8; 32] = [ + 117, 29, 113, 136, 175, 148, 38, 38, 110, 220, 157, 155, 245, 13, 239, 244, 106, 126, 188, + 90, 204, 28, 82, 70, 200, 16, 219, 33, 43, 210, 125, 239, + ]; + + const VSK_D_PRIV_ACC_B: [u8; 32] = [ + 100, 59, 111, 232, 245, 32, 102, 179, 205, 119, 145, 238, 9, 235, 62, 38, 55, 252, 179, + 217, 219, 211, 6, 188, 85, 160, 68, 54, 61, 114, 102, 81, + ]; + + const VSK_Z_PRIV_ACC_B: [u8; 32] = [ + 123, 246, 87, 46, 116, 95, 39, 122, 251, 71, 207, 144, 70, 227, 120, 27, 98, 59, 67, 247, + 209, 194, 110, 231, 250, 247, 205, 243, 31, 142, 104, 208, + ]; + const PUB_ACC_A_TEXT_ADDR: &str = "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV"; const PUB_ACC_B_TEXT_ADDR: &str = "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"; - const PRIV_ACC_A_TEXT_ADDR: &str = "EVesBKsYRVtkjnTcsbk8tWHkBn2xZmzAXzwgrP3ZaVoZ"; - const PRIV_ACC_B_TEXT_ADDR: &str = "94MXhZnueurjX6v37CYDKVEKYBiyhYArvtEdceq2XDQP"; + const PRIV_ACC_A_TEXT_ADDR: &str = "GSx3EttJzQqhFPibttxguyhKXkiD4DJmA2dMmuszEmFv"; + const PRIV_ACC_B_TEXT_ADDR: &str = "Dec1rT4DynCafh6k5pmywLGUU16RpxcxCdrSVYq8ukaN"; #[test] fn pub_state_consistency() { @@ -358,78 +334,24 @@ mod tests { let init_private_accs_keys = initial_priv_accounts_private_keys(); let init_comms = initial_commitments(); + // `nsk`/`npk` carry no constants of their own: the key chains derive from `SSK_*`, and the + // two address canaries below pin H(PREFIX || npk || vpk || identifier), so drift anywhere + // in ask -> nsk -> npk or in vsk -> vpk moves one of them. Nothing is left unpinned. + // `VSK_*` stays pinned separately because it is the last value on the vsk -> vpk leg that + // a test can compare directly. assert_eq!( - init_private_accs_keys[0] - .key_chain - .secret_spending_key - .produce_private_key_holder(None) - .nullifier_secret_key, init_private_accs_keys[0] .key_chain .private_key_holder - .nullifier_secret_key - ); - assert_eq!( - init_private_accs_keys[0] - .key_chain - .secret_spending_key - .produce_private_key_holder(None) .viewing_secret_key, - init_private_accs_keys[0] - .key_chain - .private_key_holder - .viewing_secret_key - ); - assert_eq!( - init_private_accs_keys[0] - .key_chain - .private_key_holder - .generate_nullifier_public_key(), - init_private_accs_keys[0].key_chain.nullifier_public_key - ); - assert_eq!( - init_private_accs_keys[0] - .key_chain - .private_key_holder - .generate_viewing_public_key(), - init_private_accs_keys[0].key_chain.viewing_public_key - ); - - assert_eq!( - init_private_accs_keys[1] - .key_chain - .secret_spending_key - .produce_private_key_holder(None) - .nullifier_secret_key, - init_private_accs_keys[1] - .key_chain - .private_key_holder - .nullifier_secret_key + ViewingSecretKey::new(VSK_D_PRIV_ACC_A, VSK_Z_PRIV_ACC_A) ); assert_eq!( init_private_accs_keys[1] .key_chain - .secret_spending_key - .produce_private_key_holder(None) + .private_key_holder .viewing_secret_key, - init_private_accs_keys[1] - .key_chain - .private_key_holder - .viewing_secret_key - ); - assert_eq!( - init_private_accs_keys[1] - .key_chain - .private_key_holder - .generate_nullifier_public_key(), - init_private_accs_keys[1].key_chain.nullifier_public_key - ); - assert_eq!( - init_private_accs_keys[1] - .key_chain - .private_key_holder - .generate_viewing_public_key(), - init_private_accs_keys[1].key_chain.viewing_public_key + ViewingSecretKey::new(VSK_D_PRIV_ACC_B, VSK_Z_PRIV_ACC_B) ); assert_eq!( @@ -453,7 +375,7 @@ mod tests { assert_eq!( init_comms[0], PrivateAccountPublicInitialData { - npk: NullifierPublicKey(NPK_PRIV_ACC_A), + npk: init_private_accs_keys[0].key_chain.nullifier_public_key, vpk: init_private_accs_keys[0] .key_chain .viewing_public_key @@ -470,7 +392,7 @@ mod tests { assert_eq!( init_comms[1], PrivateAccountPublicInitialData { - npk: NullifierPublicKey(NPK_PRIV_ACC_B), + npk: init_private_accs_keys[1].key_chain.nullifier_public_key, vpk: init_private_accs_keys[1] .key_chain .viewing_public_key diff --git a/lez/wallet-ffi/src/keys.rs b/lez/wallet-ffi/src/keys.rs index 6a2c4d0b..b3f52d22 100644 --- a/lez/wallet-ffi/src/keys.rs +++ b/lez/wallet-ffi/src/keys.rs @@ -361,6 +361,7 @@ pub unsafe extern "C" fn wallet_ffi_free_account_identity( kind: _, account_id: _, key_path, + authorization_secret_key: _, nullifier_secret_key: _, nullifier_public_key: _, viewing_public_key, diff --git a/lez/wallet-ffi/src/types.rs b/lez/wallet-ffi/src/types.rs index 154d87f8..de2d9496 100644 --- a/lez/wallet-ffi/src/types.rs +++ b/lez/wallet-ffi/src/types.rs @@ -9,7 +9,10 @@ use std::{ use common::HashType; use lee::{Data, ProgramId, SharedSecretKey}; -use lee_core::{encryption::MlKem768EncapsulationKey, program::PdaSeed, NullifierPublicKey}; +use lee_core::{ + encryption::MlKem768EncapsulationKey, program::PdaSeed, AuthorizationSecretKey, + NullifierPublicKey, NullifierSecretKey, +}; use wallet::{account::AccountIdWithPrivacy, AccountIdentity}; use crate::error::WalletFfiError; @@ -256,6 +259,7 @@ pub struct FfiAccountIdentity { pub account_id: FfiBytes32, /// C-compatible string. pub key_path: *mut c_char, + pub authorization_secret_key: FfiBytes32, pub nullifier_secret_key: FfiBytes32, pub nullifier_public_key: FfiBytes32, pub viewing_public_key: *const u8, @@ -269,6 +273,7 @@ impl Default for FfiAccountIdentity { kind: FfiAccountIdentityKind::Public, account_id: FfiBytes32::default(), key_path: std::ptr::null_mut(), + authorization_secret_key: FfiBytes32::default(), nullifier_secret_key: FfiBytes32::default(), nullifier_public_key: FfiBytes32::default(), viewing_public_key: std::ptr::null(), @@ -462,8 +467,7 @@ impl From for FfiAccountIdentity { } } AccountIdentity::PrivateShared { - nsk, - npk, + ask, vpk, identifier, } => { @@ -476,10 +480,13 @@ impl From for FfiAccountIdentity { ptr::null() }; + let nsk = NullifierSecretKey::from(&ask); + Self { kind: FfiAccountIdentityKind::PrivateShared, + authorization_secret_key: ask.0.into(), nullifier_secret_key: nsk.into(), - nullifier_public_key: npk.0.into(), + nullifier_public_key: NullifierPublicKey::from(&nsk).0.into(), viewing_public_key: vpk_data, viewing_public_key_len: vpk_len, identifier: identifier.into(), @@ -489,7 +496,6 @@ impl From for FfiAccountIdentity { AccountIdentity::PrivatePdaShared { account_id, nsk, - npk, vpk, identifier, } => { @@ -506,7 +512,7 @@ impl From for FfiAccountIdentity { kind: FfiAccountIdentityKind::PrivatePdaShared, account_id: account_id.into(), nullifier_secret_key: nsk.into(), - nullifier_public_key: npk.0.into(), + nullifier_public_key: NullifierPublicKey::from(&nsk).0.into(), viewing_public_key: vpk_data, viewing_public_key_len: vpk_len, identifier: identifier.into(), @@ -596,9 +602,16 @@ impl TryFrom<&FfiAccountIdentity> for AccountIdentity { Err(WalletFfiError::InvalidKeyValue) }?; + let ask = AuthorizationSecretKey(value.authorization_secret_key.data); + let nsk = NullifierSecretKey::from(&ask); + if value.nullifier_secret_key.data != nsk + || value.nullifier_public_key.data != NullifierPublicKey::from(&nsk).0 + { + return Err(WalletFfiError::InvalidKeyValue); + } + Ok(Self::PrivateShared { - nsk: value.nullifier_secret_key.data, - npk: NullifierPublicKey(value.nullifier_public_key.data), + ask, vpk, identifier: value.identifier.into(), }) @@ -617,10 +630,14 @@ impl TryFrom<&FfiAccountIdentity> for AccountIdentity { Err(WalletFfiError::InvalidKeyValue) }?; + let nsk = value.nullifier_secret_key.data; + if value.nullifier_public_key.data != NullifierPublicKey::from(&nsk).0 { + return Err(WalletFfiError::InvalidKeyValue); + } + Ok(Self::PrivatePdaShared { account_id: value.account_id.into(), - nsk: value.nullifier_secret_key.data, - npk: NullifierPublicKey(value.nullifier_public_key.data), + nsk, vpk, identifier: value.identifier.into(), }) @@ -676,10 +693,13 @@ impl From for AccountIdWithPrivacy { #[cfg(test)] mod tests { use lee::{AccountId, PrivateKey, PublicKey}; - use lee_core::{encryption::ViewingPublicKey, program::PdaSeed, PrivateAccountKind}; + use lee_core::{ + encryption::ViewingPublicKey, program::PdaSeed, AuthorizationSecretKey, NullifierSecretKey, + PrivateAccountKind, + }; use wallet::AccountIdentity; - use crate::{FfiAccountIdentity, FfiAccountIdentityKind}; + use crate::{error::WalletFfiError, FfiAccountIdentity, FfiAccountIdentityKind, FfiBytes32}; #[test] fn account_identity_roundtrip() { @@ -687,7 +707,8 @@ mod tests { let public_key = PublicKey::new_from_private_key(&private_key); let pub_acc_id = (&public_key).into(); - let nsk = [43; 32]; + let ask = AuthorizationSecretKey([43; 32]); + let nsk = NullifierSecretKey::from(&ask); let vpk = ViewingPublicKey::from_seed(&[44; 32], &[54; 32]); let npk = (&nsk).into(); let identifier = u128::from_le_bytes([45; 16]); @@ -726,15 +747,13 @@ mod tests { identifier, }; let acc_identity_7 = AccountIdentity::PrivateShared { - nsk, - npk, + ask, vpk: vpk.clone(), identifier, }; let acc_identity_8 = AccountIdentity::PrivatePdaShared { account_id: private_pda_acc_id, nsk, - npk, vpk, identifier, }; @@ -783,6 +802,10 @@ mod tests { FfiAccountIdentityKind::PrivatePdaShared ); + assert_eq!(ffi_acc_identity_7.nullifier_secret_key.data, nsk); + assert_eq!(ffi_acc_identity_7.nullifier_public_key.data, npk.0); + assert_eq!(ffi_acc_identity_8.nullifier_public_key.data, npk.0); + let acc_identity_res_1: AccountIdentity = (&ffi_acc_identity_1).try_into().unwrap(); let acc_identity_res_2: AccountIdentity = (&ffi_acc_identity_2).try_into().unwrap(); let acc_identity_res_2_5: AccountIdentity = (&ffi_acc_identity_2_5).try_into().unwrap(); @@ -803,4 +826,49 @@ mod tests { assert_eq!(acc_identity_res_7, acc_identity_7); assert_eq!(acc_identity_res_8, acc_identity_8); } + + #[test] + fn inconsistent_derived_keys_are_rejected() { + let ask = AuthorizationSecretKey([43; 32]); + let nsk = NullifierSecretKey::from(&ask); + let vpk = ViewingPublicKey::from_seed(&[44; 32], &[54; 32]); + let identifier = u128::from_le_bytes([45; 16]); + + let shared = AccountIdentity::PrivateShared { + ask, + vpk: vpk.clone(), + identifier, + }; + let pda_shared = AccountIdentity::PrivatePdaShared { + account_id: AccountId::new([46; 32]), + nsk, + vpk, + identifier, + }; + + let mut tampered_nsk: FfiAccountIdentity = shared.clone().into(); + tampered_nsk.nullifier_secret_key.data[0] ^= 1; + let mut tampered_npk: FfiAccountIdentity = shared.clone().into(); + tampered_npk.nullifier_public_key.data[0] ^= 1; + let mut zeroed: FfiAccountIdentity = shared.into(); + zeroed.nullifier_secret_key = FfiBytes32::default(); + zeroed.nullifier_public_key = FfiBytes32::default(); + let mut tampered_pda_npk: FfiAccountIdentity = pda_shared.clone().into(); + tampered_pda_npk.nullifier_public_key.data[0] ^= 1; + let mut zeroed_pda: FfiAccountIdentity = pda_shared.into(); + zeroed_pda.nullifier_public_key = FfiBytes32::default(); + + for inconsistent in [ + &tampered_nsk, + &tampered_npk, + &zeroed, + &tampered_pda_npk, + &zeroed_pda, + ] { + assert_eq!( + AccountIdentity::try_from(inconsistent).unwrap_err(), + WalletFfiError::InvalidKeyValue + ); + } + } } diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index b13b91d7..08bdecf3 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -249,6 +249,7 @@ typedef struct FfiAccountIdentity { * C-compatible string. */ char *key_path; + struct FfiBytes32 authorization_secret_key; struct FfiBytes32 nullifier_secret_key; struct FfiBytes32 nullifier_public_key; const uint8_t *viewing_public_key; diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index bad16212..3cf1cce4 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -4,9 +4,9 @@ use anyhow::Result; use keycard_wallet::KeycardWallet; use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ - Commitment, CommitmentSetDigest, DummyInput, Identifier, InputAccountIdentity, MembershipProof, - NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateAccountKind, PrivateWitness, - SharedSecretKey, WitnessKind, + AuthorizationSecretKey, Commitment, CommitmentSetDigest, DummyInput, Identifier, + InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, + NullifierWitness, PrivateAccountKind, PrivateWitness, SharedSecretKey, WitnessKind, account::{Account, AccountWithMetadata, Nonce}, compute_digest_for_path, encryption::{ @@ -45,20 +45,20 @@ pub enum AccountIdentity { identifier: Identifier, }, /// A shared regular private account with externally-provided keys (e.g. from GMS). - /// Uses standard `AccountId = from((&npk, identifier))` with authorized/unauthorized private - /// paths. Works with `authenticated_transfer` and all existing programs out of the box. + /// Carries the authorization secret key: the `nsk` and `npk` behind + /// `AccountId = from((&npk, &vpk, identifier))` are derived from it. + /// Works with `authenticated_transfer` and all existing programs out of the box. PrivateShared { - nsk: NullifierSecretKey, - npk: NullifierPublicKey, + ask: AuthorizationSecretKey, vpk: ViewingPublicKey, identifier: Identifier, }, /// A shared private PDA with externally-provided keys (e.g. from GMS). - /// `account_id` was derived via [`AccountId::for_private_pda`]. + /// `account_id` was derived via [`AccountId::for_private_pda`]; its `npk` is derived from + /// the `nsk` at use. PrivatePdaShared { account_id: AccountId, nsk: NullifierSecretKey, - npk: NullifierPublicKey, vpk: ViewingPublicKey, identifier: Identifier, }, @@ -102,20 +102,15 @@ impl fmt::Debug for AccountIdentity { .field("identifier", identifier) .finish(), Self::PrivateShared { - npk, - vpk, - identifier, - .. + vpk, identifier, .. } => f .debug_struct("PrivateShared") - .field("nsk", &"") - .field("npk", npk) + .field("ask", &"") .field("vpk", vpk) .field("identifier", identifier) .finish(), Self::PrivatePdaShared { account_id, - npk, vpk, identifier, .. @@ -123,7 +118,6 @@ impl fmt::Debug for AccountIdentity { .debug_struct("PrivatePdaShared") .field("account_id", account_id) .field("nsk", &"") - .field("npk", npk) .field("vpk", vpk) .field("identifier", identifier) .finish(), @@ -266,21 +260,10 @@ impl AccountManager { vpk, identifier, } => { - let acc = lee_core::account::Account::default(); - let auth_acc = AccountWithMetadata::new(acc, true, (&npk, &vpk, identifier)); - let random_seed = random_bytes(); - let pre = AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state: auth_acc, - proof: None, - random_seed, - is_pda: false, - }; - - State::Private(pre) + let account_id = lee::AccountId::from((&npk, &vpk, identifier)); + State::Private(private_foreign_acc_preparation( + account_id, npk, vpk, identifier, false, + )) } AccountIdentity::PrivatePdaOwned(account_id) => { let pre = private_key_tree_acc_preparation(wallet, account_id, true)?; @@ -291,31 +274,25 @@ impl AccountManager { npk, vpk, identifier, - } => { - let acc = lee_core::account::Account::default(); - let auth_acc = AccountWithMetadata::new(acc, false, account_id); - let random_seed = random_bytes(); - let pre = AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state: auth_acc, - proof: None, - random_seed, - is_pda: true, - }; - State::Private(pre) - } + } => State::Private(private_foreign_acc_preparation( + account_id, npk, vpk, identifier, true, + )), AccountIdentity::PrivateShared { - nsk, - npk, + ask, vpk, identifier, } => { + let nsk = NullifierSecretKey::from(&ask); + let npk = NullifierPublicKey::from(&nsk); let account_id = lee::AccountId::from((&npk, &vpk, identifier)); let pre = private_shared_acc_preparation( - wallet, account_id, nsk, npk, vpk, identifier, false, + wallet, + account_id, + nsk, + vpk, + identifier, + Some(ask), + false, ); State::Private(pre) @@ -323,12 +300,11 @@ impl AccountManager { AccountIdentity::PrivatePdaShared { account_id, nsk, - npk, vpk, identifier, } => { let pre = private_shared_acc_preparation( - wallet, account_id, nsk, npk, vpk, identifier, true, + wallet, account_id, nsk, vpk, identifier, None, true, ); State::Private(pre) @@ -448,7 +424,7 @@ impl AccountManager { kind: if pre.is_pda { WitnessKind::Pda { binding: None } } else { - WitnessKind::Regular + WitnessKind::Regular { ask: pre.ask } }, nullifier: match (pre.nsk, pre.proof.clone()) { (Some(nsk), Some(membership_proof)) => NullifierWitness::Update { @@ -527,6 +503,7 @@ impl AccountManager { } struct AccountPreparedData { + ask: Option, nsk: Option, npk: NullifierPublicKey, identifier: Identifier, @@ -550,7 +527,8 @@ fn private_key_tree_acc_preparation( let from_identifier = from_acc.kind.identifier(); let from_keys = &from_acc.key_chain; - let nsk = from_keys.private_key_holder.nullifier_secret_key; + let ask = from_keys.private_key_holder.authorization_secret_key; + let nsk = from_keys.private_key_holder.nullifier_secret_key(); let from_npk = from_keys.nullifier_public_key; let from_vpk = from_keys.viewing_public_key.clone(); @@ -561,6 +539,8 @@ fn private_key_tree_acc_preparation( let random_seed = random_bytes(); Ok(AccountPreparedData { + // A PDA is program-authorized and carries no credential of its own. + ask: (!is_pda).then_some(ask), nsk: Some(nsk), npk: from_npk, identifier: from_identifier, @@ -572,15 +552,40 @@ fn private_key_tree_acc_preparation( }) } -fn private_shared_acc_preparation( - wallet: &WalletCore, +/// Prepare a private account with no secret key knowledge, i.e. for inits. +fn private_foreign_acc_preparation( account_id: AccountId, - nsk: NullifierSecretKey, npk: NullifierPublicKey, vpk: ViewingPublicKey, identifier: Identifier, is_pda: bool, ) -> AccountPreparedData { + AccountPreparedData { + // The wallet holds no key for a recipient, so it can neither spend the account nor + // consent on its behalf. The program still claims it: a private claim never requires + // authorization. + ask: None, + nsk: None, + npk, + identifier, + vpk, + pre_state: AccountWithMetadata::new(Account::default(), false, account_id), + proof: None, + random_seed: random_bytes(), + is_pda, + } +} + +fn private_shared_acc_preparation( + wallet: &WalletCore, + account_id: AccountId, + nsk: NullifierSecretKey, + vpk: ViewingPublicKey, + identifier: Identifier, + ask: Option, + is_pda: bool, +) -> AccountPreparedData { + let npk = NullifierPublicKey::from(&nsk); let acc = wallet .storage() .key_chain() @@ -593,6 +598,7 @@ fn private_shared_acc_preparation( let random_seed = random_bytes(); AccountPreparedData { + ask, nsk: Some(nsk), npk, identifier, @@ -701,8 +707,7 @@ mod tests { #[test] fn private_shared_is_private() { let acc = AccountIdentity::PrivateShared { - nsk: [0; 32], - npk: NullifierPublicKey([1; 32]), + ask: AuthorizationSecretKey([0; 32]), vpk: ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]), identifier: 42, }; @@ -715,6 +720,7 @@ mod tests { let vpk = ViewingPublicKey::from_seed(&[0; 32], &[0; 32]); let pre_state = AccountWithMetadata::new(Account::default(), false, (&npk, &vpk, 0)); State::Private(AccountPreparedData { + ask: None, nsk: None, npk, identifier: 0, @@ -741,6 +747,23 @@ mod tests { } } + #[test] + fn foreign_private_init_is_unauthorized() { + let npk = NullifierPublicKey([7; 32]); + let vpk = ViewingPublicKey::from_seed(&[8; 32], &[9; 32]); + let account_id = lee::AccountId::from((&npk, &vpk, 0)); + let pre = private_foreign_acc_preparation(account_id, npk, vpk, 0, false); + + assert!(pre.ask.is_none()); + assert!(!pre.pre_state.is_authorized); + + let identities = manager(vec![State::Private(pre)]).account_identities(); + let InputAccountIdentity::Private(witness) = &identities[0] else { + panic!("expected a private witness"); + }; + assert!(matches!(witness.kind, WitnessKind::Regular { ask: None })); + } + #[test] fn dummy_inputs_default_pads_private_count_to_max() { let max = AccountManager::MAX_PRIVATE_ACCOUNTS; diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index d6d7d316..4eda2fc5 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -373,23 +373,19 @@ impl WalletCore { .key_chain() .shared_private_account(account_id)?; let keys = self.storage.key_chain().derive_shared_account_keys(entry)?; - let nsk = keys.nullifier_secret_key; - let npk = keys.generate_nullifier_public_key(); let vpk = keys.generate_viewing_public_key(); let identifier = entry.identifier; if entry.pda_seed.is_some() { Some(AccountIdentity::PrivatePdaShared { account_id, - nsk, - npk, + nsk: keys.nullifier_secret_key(), vpk, identifier, }) } else { Some(AccountIdentity::PrivateShared { - nsk, - npk, + ask: keys.authorization_secret_key, vpk, identifier, }) @@ -989,7 +985,7 @@ impl WalletCore { &key_chain.viewing_public_key, &kind, ); - let nsk = key_chain.private_key_holder.nullifier_secret_key; + let nsk = key_chain.private_key_holder.nullifier_secret_key(); (account_id, kind, res_acc, nsk) }) }) @@ -1028,7 +1024,7 @@ impl WalletCore { let keys = self.storage.key_chain().derive_shared_account_keys(entry)?; let npk = keys.generate_nullifier_public_key(); let vpk = keys.generate_viewing_public_key(); - let nsk = keys.nullifier_secret_key; + let nsk = keys.nullifier_secret_key(); let vsk = keys.viewing_secret_key; Some((account_id, npk, vpk, vsk, nsk)) }) diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 3f5eba05..3acd57ed 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -365,7 +365,7 @@ impl UserKeyChain { &found.key_chain.viewing_public_key, found.kind, ); - let nsk = found.key_chain.private_key_holder.nullifier_secret_key; + let nsk = found.key_chain.private_key_holder.nullifier_secret_key(); index.track(account_id, found.account, &nsk); } @@ -374,7 +374,7 @@ impl UserKeyChain { let Some(keys) = self.derive_shared_account_keys(entry) else { continue; }; - let nsk = keys.nullifier_secret_key; + let nsk = keys.nullifier_secret_key(); index.track(account_id, &entry.account, &nsk); } @@ -426,14 +426,14 @@ impl UserKeyChain { &keys.viewing_secret_key.d, &keys.viewing_secret_key.z, )?; - (keys.nullifier_secret_key, secret, true) + (keys.nullifier_secret_key(), secret, true) } else { let found = self.private_account(account_id)?; let secret = found .key_chain .calculate_shared_secret_receiver(&encrypted.epk)?; ( - found.key_chain.private_key_holder.nullifier_secret_key, + found.key_chain.private_key_holder.nullifier_secret_key(), secret, false, ) @@ -459,14 +459,14 @@ impl UserKeyChain { return Some(NullifierIndex::next_update_nullifier( account_id, &entry.account, - &keys.nullifier_secret_key, + &keys.nullifier_secret_key(), )); } let acc = self.private_account(account_id)?; Some(NullifierIndex::next_update_nullifier( account_id, acc.account, - &acc.key_chain.private_key_holder.nullifier_secret_key, + &acc.key_chain.private_key_holder.nullifier_secret_key(), )) } @@ -898,7 +898,7 @@ mod tests { let mut kc = UserKeyChain::default(); let key_chain = KeyChain::new_os_random(); - let nsk = key_chain.private_key_holder.nullifier_secret_key; + let nsk = key_chain.private_key_holder.nullifier_secret_key(); let identifier = 0; let account_id = AccountId::for_private_account( &key_chain.nullifier_public_key, @@ -966,7 +966,7 @@ mod tests { 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 nsk = keys.nullifier_secret_key; + let nsk = keys.nullifier_secret_key(); let account_id = AccountId::from((&npk, &vpk, identifier)); kc.insert_group_key_holder(label.clone(), holder); @@ -1036,7 +1036,7 @@ mod tests { 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 nsk = keys.nullifier_secret_key; + let nsk = keys.nullifier_secret_key(); let account_id = AccountId::from((&npk, &vpk, identifier)); kc.insert_group_key_holder(label.clone(), holder); diff --git a/test_fixtures/fixtures/prebuilt_sequencer_db.dump b/test_fixtures/fixtures/prebuilt_sequencer_db.dump index ec2cac38..ec3622e1 100644 Binary files a/test_fixtures/fixtures/prebuilt_sequencer_db.dump and b/test_fixtures/fixtures/prebuilt_sequencer_db.dump differ diff --git a/tools/crypto_primitives_bench/README.md b/tools/crypto_primitives_bench/README.md index eb2da149..8834d176 100644 --- a/tools/crypto_primitives_bench/README.md +++ b/tools/crypto_primitives_bench/README.md @@ -12,7 +12,7 @@ cargo bench -p crypto_primitives_bench --bench primitives Criterion's per-operation report (point estimate, 95% CI, outlier counts) for: -- `keychain/new_os_random`: full mnemonic → SSK → NSK/VSK + public-key derivation (HMAC-SHA512 PBKDF dominates). +- `keychain/new_os_random`: full mnemonic → SSK → ASK → NSK, plus SSK → VSK, and public-key derivation (HMAC-SHA512 PBKDF dominates). - `keychain/new_mnemonic`: same pipeline, mnemonic exposed. - `shared_secret_key/sender_dh`: secp256k1 ECDH per recipient (includes ephemeral key gen). - `encryption/encrypt` / `decrypt`: ChaCha20 over an Account note.