add test and other fixes

This commit is contained in:
jonesmarvin8 2026-05-20 16:00:57 -04:00
parent dc7378da1f
commit 8253002739
23 changed files with 163 additions and 616 deletions

View File

@ -65,7 +65,7 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
let from: AccountId = ctx.existing_private_accounts()[0];
let to_npk = NullifierPublicKey([42; 32]);
let to_npk_string = hex::encode(to_npk.0);
let to_vpk = ViewingPublicKey::from_seed(&to_npk.0, &[0u8; 32]);
let to_vpk = ViewingPublicKey::from_seed(&to_npk.0, &[0_u8; 32]);
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: private_mention(from),
@ -268,7 +268,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
let to_npk = NullifierPublicKey([42; 32]);
let to_npk_string = hex::encode(to_npk.0);
let to_vpk = ViewingPublicKey::from_seed(&to_npk.0, &[0u8; 32]);
let to_vpk = ViewingPublicKey::from_seed(&to_npk.0, &[0_u8; 32]);
let from: AccountId = ctx.existing_public_accounts()[0];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
@ -654,9 +654,9 @@ async fn ppt_that_chain_calls_faucet_is_dropped() -> Result<()> {
let auth_transfer_program_id = Program::authenticated_transfer_program().id();
let nsk: nssa_core::NullifierSecretKey = [3; 32];
let npk = NullifierPublicKey::from(&nsk);
let vpk = Secp256k1Point::from_scalar([4; 32]);
let ssk = SharedSecretKey::new([55; 32], &vpk);
let epk = EphemeralPublicKey::from_scalar([55; 32]);
let vpk = ViewingPublicKey(vec![4_u8; 1184]);
let ssk = SharedSecretKey([55_u8; 32]);
let epk = EphemeralPublicKey(vec![55_u8; 1088]);
let attacker_vault_id = {
let seed = vault_core::compute_vault_seed(attacker_id);
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)

View File

@ -227,10 +227,10 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
// Fresh recipients — hardcoded npks not in any wallet.
let recipient_npk_0 = NullifierPublicKey([0xAA; 32]);
let recipient_vpk_0 = ViewingPublicKey::from_seed(&recipient_npk_0.0, &[0u8; 32]);
let recipient_vpk_0 = ViewingPublicKey::from_seed(&recipient_npk_0.0, &[0_u8; 32]);
let recipient_npk_1 = NullifierPublicKey([0xBB; 32]);
let recipient_vpk_1 = ViewingPublicKey::from_seed(&recipient_npk_1.0, &[0u8; 32]);
let recipient_vpk_1 = ViewingPublicKey::from_seed(&recipient_npk_1.0, &[0_u8; 32]);
let amount_spend_0: u128 = 13;
let amount_spend_1: u128 = 37;

View File

@ -193,7 +193,7 @@ pub async fn tps_test() -> Result<()> {
fn build_privacy_transaction() -> PrivacyPreservingTransaction {
let program = Program::authenticated_transfer_program();
let sender_nsk = [1; 32];
let sender_vpk = ViewingPublicKey::from_seed(&[99u8; 32], &[100u8; 32]);
let sender_vpk = ViewingPublicKey::from_seed(&[99_u8; 32], &[100_u8; 32]);
let sender_npk = NullifierPublicKey::from(&sender_nsk);
let sender_pre = AccountWithMetadata::new(
Account {
@ -206,7 +206,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
AccountId::for_regular_private_account(&sender_npk, 0),
);
let recipient_nsk = [2; 32];
let recipient_vpk = ViewingPublicKey::from_seed(&[99u8; 32], &[100u8; 32]);
let recipient_vpk = ViewingPublicKey::from_seed(&[99_u8; 32], &[100_u8; 32]);
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
let recipient_pre = AccountWithMetadata::new(
Account::default(),

View File

@ -6,7 +6,7 @@ use nssa_core::{
/// Ephemeral key holder for the sender side of a KEM-based shared-secret exchange.
///
/// Non-clonable as intended for one-time use: construction encapsulates once and
/// stores both the shared secret and the ciphertext (EphemeralPublicKey) that must
/// stores both the shared secret and the ciphertext (`EphemeralPublicKey`) that must
/// be sent to the receiver.
pub struct EphemeralKeyHolder {
shared_secret: SharedSecretKey,
@ -36,15 +36,15 @@ impl EphemeralKeyHolder {
}
}
/// Returns the KEM ciphertext to be transmitted to the receiver as the EphemeralPublicKey.
/// Returns the KEM ciphertext to be transmitted to the receiver as the `EphemeralPublicKey`.
#[must_use]
pub fn ephemeral_public_key(&self) -> &EphemeralPublicKey {
pub const fn ephemeral_public_key(&self) -> &EphemeralPublicKey {
&self.ephemeral_public_key
}
/// Returns the sender-side shared secret (established at construction time).
#[must_use]
pub fn calculate_shared_secret_sender(&self) -> SharedSecretKey {
pub const fn calculate_shared_secret_sender(&self) -> SharedSecretKey {
self.shared_secret
}
}

View File

@ -399,10 +399,10 @@ mod tests {
let recipient_ssk = SecretSpendingKey([7_u8; 32]);
let recipient_keys = recipient_ssk.produce_private_key_holder(None);
let recipient_vpk = recipient_keys.generate_viewing_public_key();
let recipient_vsk = recipient_keys.viewing_secret_key.clone();
let recipient_vsk = recipient_keys.viewing_secret_key;
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
let restored = GroupKeyHolder::unseal(&sealed, recipient_vsk).expect("unseal");
let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal");
assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms());
@ -429,11 +429,10 @@ mod tests {
let wrong_vsk = SecretSpendingKey([99_u8; 32])
.produce_private_key_holder(None)
.viewing_secret_key
.clone();
.viewing_secret_key;
let sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
let result = GroupKeyHolder::unseal(&sealed, wrong_vsk);
let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk);
assert!(matches!(result, Err(super::SealError::DecryptionFailed)));
}
@ -445,14 +444,14 @@ mod tests {
let recipient_ssk = SecretSpendingKey([7_u8; 32]);
let recipient_keys = recipient_ssk.produce_private_key_holder(None);
let recipient_vpk = recipient_keys.generate_viewing_public_key();
let recipient_vsk = recipient_keys.viewing_secret_key.clone();
let recipient_vsk = recipient_keys.viewing_secret_key;
let mut sealed = holder.seal_for(&SealingPublicKey::from_bytes(recipient_vpk.0));
// Flip a byte in the AES-GCM ciphertext portion (after KEM ciphertext + nonce).
let last = sealed.len() - 1;
sealed[last] ^= 0xFF;
let result = GroupKeyHolder::unseal(&sealed, recipient_vsk);
let result = GroupKeyHolder::unseal(&sealed, &recipient_vsk);
assert!(matches!(result, Err(super::SealError::DecryptionFailed)));
}
@ -527,11 +526,11 @@ mod tests {
let bob_ssk = SecretSpendingKey([77_u8; 32]);
let bob_keys = bob_ssk.produce_private_key_holder(None);
let bob_vpk = bob_keys.generate_viewing_public_key();
let bob_vsk = bob_keys.viewing_secret_key.clone();
let bob_vsk = bob_keys.viewing_secret_key;
let sealed = alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.0));
let bob_holder =
GroupKeyHolder::unseal(&sealed, bob_vsk).expect("Bob should unseal the GMS");
GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS");
let bob_npk = bob_holder
.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed)

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey};
use serde::{Deserialize, Serialize};
use sha2::Digest as _;
@ -59,7 +61,7 @@ impl ChildKeysPrivate {
pub fn nth_child(&self, cci: u32) -> Self {
let mut parent_hash = sha2::Sha256::new();
parent_hash.update(b"LEE/keys");
parent_hash.update([0u8; 16]);
parent_hash.update([0_u8; 16]);
parent_hash.update([9_u8]);
parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key);
parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.d);
@ -128,7 +130,7 @@ impl KeyTreeNode for ChildKeysPrivate {
#[cfg(test)]
mod tests {
use nssa_core::{NullifierPublicKey, NullifierSecretKey};
use nssa_core::NullifierSecretKey;
use super::*;
use crate::key_management::{self, secret_holders::ViewingSecretKey};
@ -144,7 +146,7 @@ mod tests {
let keys = ChildKeysPrivate::root(seed);
let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139,
222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191,
]);
@ -159,7 +161,7 @@ mod tests {
34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163,
];
let expected_npk: NullifierPublicKey = nssa_core::NullifierPublicKey([
let expected_npk = nssa_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,
]);
@ -257,11 +259,10 @@ mod tests {
114, 39, 38, 118, 197, 205, 225,
];
// Marvin-pq this test currently fails
let root_node = ChildKeysPrivate::root(seed);
let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32);
let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
215, 207, 70, 52, 161, 220, 88, 88, 241, 149, 81, 130, 217, 214, 252, 170, 51, 232,
230, 158, 195, 173, 174, 37, 27, 101, 49, 35, 79, 13, 44, 225,
]);

View File

@ -19,7 +19,7 @@ pub struct SeedHolder {
pub struct SecretSpendingKey(pub [u8; 32]);
/// Viewing secret key: the KEM seed split into its two 32-byte halves `d` and `r` (= z in
/// FIPS 203), from which the ML-KEM 768 decapsulation key is derived deterministically.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ViewingSecretKey {
pub d: [u8; 32],
pub r: [u8; 32],
@ -27,7 +27,7 @@ pub struct ViewingSecretKey {
/// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret
/// for recepient.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PrivateKeyHolder {
pub nullifier_secret_key: NullifierSecretKey,
pub viewing_secret_key: ViewingSecretKey,
@ -150,7 +150,7 @@ impl SecretSpendingKey {
}
#[must_use]
pub fn generate_viewing_secret_key(seed: [u8; 64]) -> ViewingSecretKey {
pub const fn generate_viewing_secret_key(seed: [u8; 64]) -> ViewingSecretKey {
ViewingSecretKey {
d: *seed.first_chunk::<32>().expect("seed is 64 bytes"),
r: *seed.last_chunk::<32>().expect("seed is 64 bytes"),
@ -169,11 +169,11 @@ impl SecretSpendingKey {
impl From<&ViewingSecretKey> for ViewingPublicKey {
fn from(sk: &ViewingSecretKey) -> Self {
use ml_kem::{Kem, KeyExport as _, MlKem768, Seed};
let mut seed_bytes = [0u8; 64];
let mut seed_bytes = [0_u8; 64];
seed_bytes[..32].copy_from_slice(&sk.d);
seed_bytes[32..].copy_from_slice(&sk.r);
let dk = <MlKem768 as Kem>::DecapsulationKey::from_seed(Seed::from(seed_bytes));
ViewingPublicKey(dk.encapsulation_key().to_bytes().to_vec())
Self(dk.encapsulation_key().to_bytes().to_vec())
}
}

View File

@ -1,421 +0,0 @@
use std::collections::BTreeMap;
use anyhow::Result;
use k256::AffinePoint;
use nssa::{Account, AccountId};
use nssa_core::{Identifier, PrivateAccountKind};
use serde::{Deserialize, Serialize};
use crate::key_management::{
KeyChain,
group_key_holder::GroupKeyHolder,
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
secret_holders::SeedHolder,
};
pub type PublicKey = AffinePoint;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserPrivateAccountData {
pub key_chain: KeyChain,
pub accounts: Vec<(PrivateAccountKind, Account)>,
}
/// Metadata for a shared account (GMS-derived), stored alongside the cached plaintext state.
/// The group label and identifier (or PDA seed) are needed to re-derive keys during sync.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SharedAccountEntry {
pub group_label: String,
pub identifier: Identifier,
/// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`.
/// `None` for regular shared accounts (keys derived from identifier via derivation seed).
#[serde(default)]
pub pda_seed: Option<nssa_core::program::PdaSeed>,
#[serde(default)]
pub pda_program_id: Option<nssa_core::program::ProgramId>,
pub account: Account,
}
#[derive(Clone, Debug)]
pub struct NSSAUserData {
/// Default public accounts.
pub default_pub_account_signing_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
/// Default private accounts.
pub default_user_private_accounts: BTreeMap<AccountId, UserPrivateAccountData>,
/// Tree of public keys.
pub public_key_tree: KeyTreePublic,
/// Tree of private keys.
pub private_key_tree: KeyTreePrivate,
/// Group key holders for shared account management, keyed by a human-readable label.
pub group_key_holders: BTreeMap<String, GroupKeyHolder>,
/// Cached plaintext state of shared private accounts (PDAs and regular shared accounts),
/// keyed by `AccountId`. Each entry stores the group label and identifier needed
/// to re-derive keys during sync.
pub shared_private_accounts: BTreeMap<nssa::AccountId, SharedAccountEntry>,
/// Dedicated sealing secret key for GMS distribution. Generated once via
/// `wallet group new-sealing-key`. The corresponding public key is shared with
/// group members so they can seal GMS for this wallet.
pub sealing_secret_key: Option<crate::key_management::secret_holders::ViewingSecretKey>,
}
impl NSSAUserData {
fn valid_public_key_transaction_pairing_check(
accounts_keys_map: &BTreeMap<nssa::AccountId, nssa::PrivateKey>,
) -> bool {
let mut check_res = true;
for (account_id, key) in accounts_keys_map {
let expected_account_id =
nssa::AccountId::from(&nssa::PublicKey::new_from_private_key(key));
if &expected_account_id != account_id {
println!("{expected_account_id}, {account_id}");
check_res = false;
}
}
check_res
}
fn valid_private_key_transaction_pairing_check(
accounts_keys_map: &BTreeMap<AccountId, UserPrivateAccountData>,
) -> bool {
let mut check_res = true;
for (account_id, entry) in accounts_keys_map {
let npk = &entry.key_chain.nullifier_public_key;
let any_match = entry
.accounts
.iter()
.any(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == *account_id);
if !any_match {
println!("No matching entry found for account_id {account_id}");
check_res = false;
}
}
check_res
}
pub fn new_with_accounts(
default_accounts_keys: BTreeMap<nssa::AccountId, nssa::PrivateKey>,
default_accounts_key_chains: BTreeMap<AccountId, UserPrivateAccountData>,
public_key_tree: KeyTreePublic,
private_key_tree: KeyTreePrivate,
) -> Result<Self> {
if !Self::valid_public_key_transaction_pairing_check(&default_accounts_keys) {
anyhow::bail!(
"Key transaction pairing check not satisfied, there are public account_ids, which are not derived from keys"
);
}
if !Self::valid_private_key_transaction_pairing_check(&default_accounts_key_chains) {
anyhow::bail!(
"Key transaction pairing check not satisfied, there are private account_ids, which are not derived from keys"
);
}
Ok(Self {
default_pub_account_signing_keys: default_accounts_keys,
default_user_private_accounts: default_accounts_key_chains,
public_key_tree,
private_key_tree,
group_key_holders: BTreeMap::new(),
shared_private_accounts: BTreeMap::new(),
sealing_secret_key: None,
})
}
/// Generated new private key for public transaction signatures.
///
/// Returns the `account_id` of new account.
pub fn generate_new_public_transaction_private_key(
&mut self,
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
match parent_cci {
Some(parent_cci) => self
.public_key_tree
.generate_new_public_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.public_key_tree
.generate_new_public_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures.
#[must_use]
pub fn get_pub_account_signing_key(
&self,
account_id: nssa::AccountId,
) -> Option<&nssa::PrivateKey> {
self.default_pub_account_signing_keys
.get(&account_id)
.or_else(|| self.public_key_tree.get_node(account_id).map(Into::into))
}
/// Creates a new receiving key node and returns its `ChainIndex`.
pub fn create_private_accounts_key(&mut self, parent_cci: Option<ChainIndex>) -> ChainIndex {
match parent_cci {
Some(parent_cci) => self
.private_key_tree
.create_private_accounts_key_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.private_key_tree
.create_private_accounts_key_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Registers an additional identifier on an existing private key node, deriving and recording
/// the corresponding `AccountId`. Returns `None` if the node does not exist or the identifier
/// is already registered.
pub fn register_identifier_on_private_key_chain(
&mut self,
cci: &ChainIndex,
identifier: Identifier,
) -> Option<nssa::AccountId> {
self.private_key_tree
.register_identifier_on_node(cci, identifier)
}
/// Returns the key chain and account data for the given private account ID.
#[must_use]
pub fn get_private_account(
&self,
account_id: nssa::AccountId,
) -> Option<(KeyChain, nssa_core::account::Account, Identifier)> {
// Check default accounts
if let Some(entry) = self.default_user_private_accounts.get(&account_id) {
let npk = &entry.key_chain.nullifier_public_key;
if let Some((kind, account)) = entry
.accounts
.iter()
.find(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == account_id)
{
return Some((entry.key_chain.clone(), account.clone(), kind.identifier()));
}
return None;
}
// Check tree
if let Some(node) = self.private_key_tree.get_node(account_id) {
let key_chain = &node.value.0;
let npk = &key_chain.nullifier_public_key;
if let Some((kind, account)) = node
.value
.1
.iter()
.find(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == account_id)
{
return Some((key_chain.clone(), account.clone(), kind.identifier()));
}
}
None
}
pub fn account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.public_account_ids().chain(self.private_account_ids())
}
pub fn public_account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.default_pub_account_signing_keys
.keys()
.copied()
.chain(self.public_key_tree.account_id_map.keys().copied())
}
pub fn private_account_ids(&self) -> impl Iterator<Item = nssa::AccountId> {
self.default_user_private_accounts
.keys()
.copied()
.chain(self.private_key_tree.account_id_map.keys().copied())
}
/// Returns the `GroupKeyHolder` for the given label, if it exists.
#[must_use]
pub fn group_key_holder(&self, label: &str) -> Option<&GroupKeyHolder> {
self.group_key_holders.get(label)
}
/// Inserts or replaces a `GroupKeyHolder` under the given label.
///
/// If a holder already exists under this label, it is silently replaced and the old
/// GMS is lost. Callers must ensure label uniqueness across groups.
pub fn insert_group_key_holder(&mut self, label: String, holder: GroupKeyHolder) {
self.group_key_holders.insert(label, holder);
}
/// Returns the cached account for a shared private account, if it exists.
#[must_use]
pub fn shared_private_account(
&self,
account_id: &nssa::AccountId,
) -> Option<&SharedAccountEntry> {
self.shared_private_accounts.get(account_id)
}
/// Inserts or replaces a shared private account entry.
pub fn insert_shared_private_account(
&mut self,
account_id: nssa::AccountId,
entry: SharedAccountEntry,
) {
self.shared_private_accounts.insert(account_id, entry);
}
/// Updates the cached account state for a shared private account.
pub fn update_shared_private_account_state(
&mut self,
account_id: &nssa::AccountId,
account: nssa_core::account::Account,
) {
if let Some(entry) = self.shared_private_accounts.get_mut(account_id) {
entry.account = account;
}
}
/// Iterates over all shared private accounts.
pub fn shared_private_accounts_iter(
&self,
) -> impl Iterator<Item = (&nssa::AccountId, &SharedAccountEntry)> {
self.shared_private_accounts.iter()
}
}
impl Default for NSSAUserData {
fn default() -> Self {
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
Self::new_with_accounts(
BTreeMap::new(),
BTreeMap::new(),
KeyTreePublic::new(&seed_holder),
KeyTreePrivate::new(&seed_holder),
)
.unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn group_key_holder_storage_round_trip() {
let mut user_data = NSSAUserData::default();
assert!(user_data.group_key_holder("test-group").is_none());
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
user_data.insert_group_key_holder(String::from("test-group"), holder.clone());
let retrieved = user_data
.group_key_holder("test-group")
.expect("should exist");
assert_eq!(retrieved.dangerous_raw_gms(), holder.dangerous_raw_gms());
}
#[test]
fn group_key_holders_default_empty() {
let user_data = NSSAUserData::default();
assert!(user_data.group_key_holders.is_empty());
assert!(user_data.shared_private_accounts.is_empty());
}
#[test]
fn shared_account_entry_serde_round_trip() {
use nssa_core::program::PdaSeed;
let entry = SharedAccountEntry {
group_label: String::from("test-group"),
identifier: 42,
pda_seed: None,
pda_program_id: None,
account: nssa_core::account::Account::default(),
};
let encoded = bincode::serialize(&entry).expect("serialize");
let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize");
assert_eq!(decoded.group_label, "test-group");
assert_eq!(decoded.identifier, 42);
assert!(decoded.pda_seed.is_none());
let pda_entry = SharedAccountEntry {
group_label: String::from("pda-group"),
identifier: u128::MAX,
pda_seed: Some(PdaSeed::new([7_u8; 32])),
pda_program_id: Some([9; 8]),
account: nssa_core::account::Account::default(),
};
let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda");
let pda_decoded: SharedAccountEntry =
bincode::deserialize(&pda_encoded).expect("deserialize pda");
assert_eq!(pda_decoded.group_label, "pda-group");
assert_eq!(pda_decoded.identifier, u128::MAX);
assert_eq!(pda_decoded.pda_seed.unwrap(), PdaSeed::new([7_u8; 32]));
}
#[test]
fn shared_account_entry_none_pda_seed_round_trips() {
// Verify that an entry with pda_seed=None serializes and deserializes correctly,
// confirming the #[serde(default)] attribute works for backward compatibility.
let entry = SharedAccountEntry {
group_label: String::from("old"),
identifier: 1,
pda_seed: None,
pda_program_id: None,
account: nssa_core::account::Account::default(),
};
let encoded = bincode::serialize(&entry).expect("serialize");
let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize");
assert_eq!(decoded.group_label, "old");
assert_eq!(decoded.identifier, 1);
assert!(decoded.pda_seed.is_none());
}
#[test]
fn shared_account_derives_consistent_keys_from_group() {
use nssa_core::program::PdaSeed;
let mut user_data = NSSAUserData::default();
let gms_holder = GroupKeyHolder::from_gms([42_u8; 32]);
user_data.insert_group_key_holder(String::from("my-group"), gms_holder);
let holder = user_data.group_key_holder("my-group").unwrap();
// Regular shared account: derive via tag
let tag = [1_u8; 32];
let keys_a = holder.derive_keys_for_shared_account(&tag);
let keys_b = holder.derive_keys_for_shared_account(&tag);
assert_eq!(
keys_a.generate_nullifier_public_key(),
keys_b.generate_nullifier_public_key(),
);
// PDA shared account: derive via seed
let seed = PdaSeed::new([2_u8; 32]);
let pda_keys_a = holder.derive_keys_for_pda(&[9; 8], &seed);
let pda_keys_b = holder.derive_keys_for_pda(&[9; 8], &seed);
assert_eq!(
pda_keys_a.generate_nullifier_public_key(),
pda_keys_b.generate_nullifier_public_key(),
);
// PDA and shared derivations don't collide
assert_ne!(
keys_a.generate_nullifier_public_key(),
pda_keys_a.generate_nullifier_public_key(),
);
}
#[test]
fn new_account() {
let mut user_data = NSSAUserData::default();
let chain_index = user_data.create_private_accounts_key(Some(ChainIndex::root()));
let is_key_chain_generated = user_data
.private_key_tree
.key_map
.contains_key(&chain_index);
assert!(is_key_chain_generated);
let key_chain = &user_data.private_key_tree.key_map[&chain_index].value.0;
println!("{key_chain:#?}");
}
}

View File

@ -7,7 +7,7 @@ use std::io::Read as _;
#[cfg(feature = "host")]
use crate::Nullifier;
#[cfg(feature = "host")]
use crate::encryption::{EphemeralPublicKey, shared_key_derivation::Secp256k1Point};
use crate::encryption::EphemeralPublicKey;
#[cfg(feature = "host")]
use crate::error::NssaCoreError;
use crate::{
@ -157,25 +157,6 @@ impl Ciphertext {
}
}
#[cfg(feature = "host")]
impl Secp256k1Point {
/// Converts the point to bytes.
#[must_use]
pub fn to_bytes(&self) -> [u8; 33] {
self.0.clone().try_into().unwrap()
}
/// Deserializes a secp256k1 point from a cursor.
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaCoreError> {
let mut value = vec![0; 33];
cursor.read_exact(&mut value)?;
Ok(Self(value))
}
}
// Marvin-pq: EphemeralPublicKey is now the ML-KEM-768 ciphertext (1088 bytes) produced by
// SharedSecretKey::encapsulate. It replaces the old Secp256k1Point (33 bytes) on the wire.
// Fixed size: 1088 bytes for ML-KEM-768 (EncodedUSize + EncodedVSize per FIPS 203 §7.2).
#[cfg(feature = "host")]
impl EphemeralPublicKey {
/// Serializes the ML-KEM-768 ciphertext to bytes (always 1088 bytes).
@ -187,7 +168,7 @@ impl EphemeralPublicKey {
/// Deserializes an ML-KEM-768 ciphertext from a cursor.
/// Reads exactly 1088 bytes — the fixed ciphertext size for ML-KEM-768.
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaCoreError> {
let mut value = vec![0u8; 1088];
let mut value = vec![0_u8; 1088];
cursor.read_exact(&mut value)?;
Ok(Self(value))
}

View File

@ -154,4 +154,41 @@ mod tests {
assert_eq!(account_ct.0.len(), pda_ct.0.len());
}
/// Verifies the full account-note pipeline: ML-KEM-768 encapsulation/decapsulation
/// feeds the correct shared secret into the SHA-256 KDF and ChaCha20 round-trip.
#[cfg(feature = "host")]
#[test]
fn kem_to_chacha20_round_trip() {
let d = [1_u8; 32];
let r = [2_u8; 32];
let vpk = shared_key_derivation::ViewingPublicKey::from_seed(&d, &r);
let (sender_ss, epk) = SharedSecretKey::encapsulate(&vpk);
let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &r);
let account = Account {
program_owner: [12_u32; 8],
balance: 999,
..Account::default()
};
let kind = PrivateAccountKind::Regular(0);
let commitment = crate::Commitment::new(&AccountId::new([7_u8; 32]), &account);
let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &commitment, 0);
let (decoded_kind, decoded_account) =
EncryptionScheme::decrypt(&ct, &receiver_ss, &commitment, 0)
.expect("decryption must succeed with correct shared secret");
assert_eq!(decoded_account, account);
assert_eq!(decoded_kind, kind);
// Wrong shared secret must not decrypt correctly.
let wrong_ss = SharedSecretKey([0_u8; 32]);
let bad = EncryptionScheme::decrypt(&ct, &wrong_ss, &commitment, 0);
assert!(
bad.is_none() || bad.is_some_and(|(_, a)| a.balance != 999),
"wrong shared secret must not produce the correct plaintext"
);
}
}

View File

@ -1,48 +1,8 @@
#![expect(
clippy::arithmetic_side_effects,
reason = "Multiplication of finite field elements can't overflow"
)]
use std::fmt::Write as _;
use borsh::{BorshDeserialize, BorshSerialize};
use k256::{
AffinePoint, FieldBytes, ProjectivePoint,
elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _},
};
use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed};
use serde::{Deserialize, Serialize};
use crate::{SharedSecretKey, encryption::Scalar};
/// Marvin-pq check this
/// A compressed secp256k1 point (33 bytes).
/// Kept for backward compatibility with Phase-2+ callers; no longer used as `EphemeralPublicKey`.
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct Secp256k1Point(pub Vec<u8>);
impl std::fmt::Debug for Secp256k1Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let hex: String = self.0.iter().fold(String::new(), |mut acc, b| {
write!(acc, "{b:02x}").expect("writing to string should not fail");
acc
});
write!(f, "Secp256k1Point({hex})")
}
}
impl Secp256k1Point {
#[must_use]
pub fn from_scalar(value: Scalar) -> Self {
let x_bytes: FieldBytes = value.into();
let x = k256::Scalar::from_repr(x_bytes).unwrap();
let p = ProjectivePoint::GENERATOR * x;
let q = AffinePoint::from(p);
let enc = q.to_encoded_point(true);
Self(enc.as_bytes().to_vec())
}
}
use crate::SharedSecretKey;
/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the
/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768.
@ -50,7 +10,7 @@ impl Secp256k1Point {
pub struct EphemeralPublicKey(pub Vec<u8>);
/// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate).
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize)]
pub struct ViewingPublicKey(pub Vec<u8>);
impl ViewingPublicKey {
@ -113,7 +73,7 @@ impl SharedSecretKey {
input.extend_from_slice(message_hash);
input.extend_from_slice(&output_index.to_le_bytes());
let hash = Impl::hash_bytes(&input);
let m: ml_kem::B32 = ml_kem::array::Array::try_from(hash.as_bytes() as &[u8])
let m: ml_kem::B32 = ml_kem::array::Array::try_from(hash.as_bytes())
.expect("SHA-256 output is 32 bytes");
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> = vpk
@ -159,8 +119,8 @@ mod tests {
#[test]
fn encapsulate_decapsulate_round_trip() {
let d = [1u8; 32];
let r = [2u8; 32];
let d = [1_u8; 32];
let r = [2_u8; 32];
let mut seed = Seed::default();
seed[..32].copy_from_slice(&d);
@ -184,8 +144,8 @@ mod tests {
#[test]
fn different_vpks_produce_different_shared_secrets() {
let (d1, r1) = ([1u8; 32], [2u8; 32]);
let (d2, r2) = ([3u8; 32], [4u8; 32]);
let (d1, r1) = ([1_u8; 32], [2_u8; 32]);
let (d2, r2) = ([3_u8; 32], [4_u8; 32]);
let vpk1 = {
let mut seed = Seed::default();

View File

@ -244,7 +244,7 @@ mod tests {
let expected_sender_pre = sender.clone();
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
let (output, proof) = execute_and_prove(
vec![sender, recipient],
@ -341,10 +341,10 @@ mod tests {
];
let shared_secret_1 =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0;
let shared_secret_2 =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 1).0;
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0;
let (output, proof) = execute_and_prove(
vec![sender_pre, recipient],
@ -419,7 +419,7 @@ mod tests {
.unwrap();
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0;
let program_with_deps = ProgramWithDependencies::new(
validity_window_chain_caller,
@ -450,7 +450,7 @@ mod tests {
let seed = PdaSeed::new([42; 32]);
let identifier: u128 = 99;
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
@ -488,7 +488,7 @@ mod tests {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret_pda =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
// PDA (new, mask 3)
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
@ -527,7 +527,7 @@ mod tests {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret_pda =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
// PDA (new, private PDA)
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
@ -582,7 +582,7 @@ mod tests {
let shared_npk = shared_keys.npk();
let shared_identifier: u128 = 42;
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0;
// Sender: public account with balance, owned by auth-transfer
let sender_id = AccountId::new([99; 32]);
@ -633,7 +633,7 @@ mod tests {
let program = Program::authenticated_transfer_program();
let keys = test_private_account_keys_1();
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
@ -663,7 +663,7 @@ mod tests {
let program = Program::authenticated_transfer_program();
let keys = test_private_account_keys_1();
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let sender = AccountWithMetadata::new(
Account {
@ -708,7 +708,7 @@ mod tests {
let program = Program::authenticated_transfer_program();
let keys = test_private_account_keys_1();
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
let account = Account {
program_owner: program.id(),
@ -757,7 +757,7 @@ mod tests {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let identifier: u128 = 99;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let auth_transfer_id = auth_transfer.id();
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
@ -812,7 +812,7 @@ mod tests {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
@ -838,7 +838,7 @@ mod tests {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let auth_transfer_id = auth_transfer.id();
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);

View File

@ -208,7 +208,7 @@ pub mod tests {
let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
// all remaining vec fields are empty: u32 len=0
let empty_vec_bytes: &[u8] = &[0_u8; 4];
// validity windows: unbounded = {from: None (0u8), to: None (0u8)}
// validity windows: unbounded = {from: None (0_u8), to: None (0_u8)}
let unbounded_window_bytes: &[u8] = &[0_u8; 2];
let expected_borsh_vec: Vec<u8> = [
@ -246,11 +246,11 @@ pub mod tests {
#[test]
fn encrypted_account_data_constructor() {
let npk = NullifierPublicKey::from(&[1; 32]);
let vpk = ViewingPublicKey::from_seed(&[2u8; 32], &[3u8; 32]);
let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]);
let account = Account::default();
let account_id = nssa_core::account::AccountId::for_regular_private_account(&npk, 0);
let commitment = Commitment::new(&account_id, &account);
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0u8; 32], 0);
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0_u8; 32], 0);
let ciphertext = EncryptionScheme::encrypt(
&account,
&PrivateAccountKind::Regular(0),

View File

@ -1345,7 +1345,7 @@ pub mod tests {
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0);
let (output, proof) = circuit::execute_and_prove(
vec![sender, recipient],
@ -1396,10 +1396,10 @@ pub mod tests {
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
let (shared_secret_1, epk_1) =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
let (shared_secret_2, epk_2) =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 1);
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1);
let (output, proof) = circuit::execute_and_prove(
vec![sender_pre, recipient_pre],
@ -1464,7 +1464,7 @@ pub mod tests {
);
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
let (output, proof) = circuit::execute_and_prove(
vec![sender_pre, recipient_pre],
@ -1974,7 +1974,7 @@ pub mod tests {
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -1986,7 +1986,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2030,7 +2030,7 @@ pub mod tests {
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2042,7 +2042,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2086,7 +2086,7 @@ pub mod tests {
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2098,7 +2098,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2142,7 +2142,7 @@ pub mod tests {
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2154,7 +2154,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2198,7 +2198,7 @@ pub mod tests {
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2210,7 +2210,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2252,7 +2252,7 @@ pub mod tests {
InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2264,7 +2264,7 @@ pub mod tests {
npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(),
&[0u8; 32],
&[0_u8; 32],
0,
)
.0,
@ -2287,7 +2287,7 @@ pub mod tests {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let public_account_1 = AccountWithMetadata::new(
Account {
program_owner: program.id(),
@ -2329,7 +2329,7 @@ pub mod tests {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
@ -2366,7 +2366,7 @@ pub mod tests {
let npk_b = keys_b.npk();
let seed = PdaSeed::new([42; 32]);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0;
// `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state.
// `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in
@ -2401,7 +2401,7 @@ pub mod tests {
let npk = keys.npk();
let seed = PdaSeed::new([77; 32]);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
@ -2440,7 +2440,7 @@ pub mod tests {
let claim_seed = PdaSeed::new([77; 32]);
let wrong_delegated_seed = PdaSeed::new([88; 32]);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
@ -2477,8 +2477,8 @@ pub mod tests {
let keys_a = test_private_account_keys_1();
let keys_b = test_private_account_keys_2();
let seed = PdaSeed::new([55; 32]);
let shared_a = SharedSecretKey::encapsulate_deterministic(&keys_a.vpk(), &[0u8; 32], 0).0;
let shared_b = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0u8; 32], 0).0;
let shared_a = SharedSecretKey::encapsulate_deterministic(&keys_a.vpk(), &[0_u8; 32], 0).0;
let shared_b = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0;
let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk(), u128::MAX);
let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk(), u128::MAX);
@ -2524,7 +2524,7 @@ pub mod tests {
let keys = test_private_account_keys_1();
let npk = keys.npk();
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
let seed = PdaSeed::new([99; 32]);
// Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized =
@ -2624,7 +2624,7 @@ pub mod tests {
);
let shared_secret =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0;
let result = execute_and_prove(
vec![private_account_1.clone(), private_account_1],
Program::serialize_instruction(100_u128).unwrap(),
@ -2969,7 +2969,7 @@ pub mod tests {
let recipient_pre =
AccountWithMetadata::new(Account::default(), true, recipient_account_id);
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
let balance = 37;
@ -3074,10 +3074,10 @@ pub mod tests {
);
let (from_ss, from_epk) =
SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0_u8; 32], 0);
let (to_ss, to_epk) =
SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0u8; 32], 1);
SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0_u8; 32], 1);
let mut dependencies = HashMap::new();
@ -3375,7 +3375,7 @@ pub mod tests {
// Set up parameters for the new account
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0);
let instruction = authenticated_transfer_core::Instruction::Initialize;
@ -3426,7 +3426,7 @@ pub mod tests {
let program = Program::claimer();
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0);
let (output, proof) = execute_and_prove(
vec![unauthorized_account],
@ -3475,7 +3475,7 @@ pub mod tests {
// Set up parameters for claiming the new account
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0);
let instruction = authenticated_transfer_core::Instruction::Initialize;
@ -3524,7 +3524,7 @@ pub mod tests {
let noop_program = Program::noop();
let shared_secret2 =
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0).0;
// Step 3: Try to execute noop program with authentication but without initialization
let res = execute_and_prove(
@ -3608,7 +3608,7 @@ pub mod tests {
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0)
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
@ -3635,7 +3635,7 @@ pub mod tests {
vec![private_account],
Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0u8; 32], 0)
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0,
nsk: sender_keys.nsk,
membership_proof: (0, vec![]),
@ -3683,7 +3683,7 @@ pub mod tests {
let instruction = (balance_to_transfer, auth_transfers.id());
let recipient =
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0u8; 32], 0).0;
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
let mut dependencies = HashMap::new();
dependencies.insert(auth_transfers.id(), auth_transfers);
@ -3840,7 +3840,7 @@ pub mod tests {
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs();
let tx = {
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0);
let instruction = (
block_validity_window,
@ -3909,7 +3909,7 @@ pub mod tests {
let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs();
let tx = {
let (shared_secret, epk) =
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0);
let instruction = (
BlockValidityWindow::new_unbounded(),
@ -4464,9 +4464,9 @@ pub mod tests {
};
let (alice_shared_0, alice_epk_0) =
SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0u8; 32], 0);
SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 0);
let (alice_shared_1, alice_epk_1) =
SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0u8; 32], 1);
SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 1);
// Fund alice_pda_0
{
@ -4576,7 +4576,7 @@ pub mod tests {
let message = Message::try_from_circuit_output(
vec![recipient_id],
vec![Nonce(0)],
vec![(alice_npk, alice_keys.vpk(), alice_epk_0.clone())],
vec![(alice_npk, alice_keys.vpk(), alice_epk_0)],
output,
)
.unwrap();
@ -4616,7 +4616,7 @@ pub mod tests {
let message = Message::try_from_circuit_output(
vec![recipient_id],
vec![],
vec![(alice_npk, alice_keys.vpk(), alice_epk_1.clone())],
vec![(alice_npk, alice_keys.vpk(), alice_epk_1)],
output,
)
.unwrap();

View File

@ -129,20 +129,20 @@ pub fn initial_priv_accounts_private_keys() -> Vec<PrivateAccountPrivateInitialD
secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_A),
private_key_holder: PrivateKeyHolder {
nullifier_secret_key: NSK_PRIV_ACC_A,
viewing_secret_key: ViewingSecretKey { d: VSK_PRIV_ACC_A, r: [0u8; 32] },
viewing_secret_key: ViewingSecretKey { d: VSK_PRIV_ACC_A, r: [0_u8; 32] },
},
nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_A),
viewing_public_key: ViewingPublicKey::from_seed(&VSK_PRIV_ACC_A, &[0u8; 32]),
viewing_public_key: ViewingPublicKey::from_seed(&VSK_PRIV_ACC_A, &[0_u8; 32]),
};
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 { d: VSK_PRIV_ACC_B, r: [0u8; 32] },
viewing_secret_key: ViewingSecretKey { d: VSK_PRIV_ACC_B, r: [0_u8; 32] },
},
nullifier_public_key: NullifierPublicKey(NPK_PRIV_ACC_B),
viewing_public_key: ViewingPublicKey::from_seed(&VSK_PRIV_ACC_B, &[0u8; 32]),
viewing_public_key: ViewingPublicKey::from_seed(&VSK_PRIV_ACC_B, &[0_u8; 32]),
};
vec![

View File

@ -3,7 +3,7 @@
//! Measures:
//! - `KeyChain::new_os_random` (mnemonic → SSK → NSK/VSK + public keys)
//! - `KeyChain::new_mnemonic` (same, but mnemonic exposed)
//! - `SharedSecretKey::new` (Diffie-Hellman shared key derivation, the per-recipient cost)
//! - `SharedSecretKey::encapsulate` (ML-KEM-768 encapsulation, the per-recipient cost)
//! - `EncryptionScheme::encrypt` / `decrypt` (Account note encryption)
//!
//! Reports best-of-N wall time per operation. No live stack required.
@ -24,10 +24,8 @@ use key_protocol::key_management::KeyChain;
use nssa_core::{
Commitment, EncryptionScheme, SharedSecretKey,
account::{Account, AccountId},
encryption::{EphemeralPublicKey, EphemeralSecretKey},
program::PrivateAccountKind,
};
use rand::{RngCore as _, rngs::OsRng};
use serde::Serialize;
const ITERS: usize = 100;
@ -84,28 +82,19 @@ fn main() -> Result<()> {
let (_kc, _mnemonic) = KeyChain::new_mnemonic("");
}));
// SharedSecretKey: caller has ephemeral secret, recipient has VSK→VPK.
// We bench the SENDER side: derive ephemeral pubkey, then SharedSecretKey::new(scalar, point).
// SharedSecretKey: caller has recipient VPK; we bench the SENDER side —
// ML-KEM-768 encapsulation (replaces the old ECDH scalar multiplication).
let recipient_kc = KeyChain::new_os_random();
let vpk = recipient_kc.viewing_public_key;
results.push(time("SharedSecretKey::new (sender DH)", ITERS, || {
let mut bytes = [0_u8; 32];
OsRng.fill_bytes(&mut bytes);
let esk: EphemeralSecretKey = bytes;
let _epk = EphemeralPublicKey::from(&esk);
let _ssk = SharedSecretKey::new(esk, &vpk);
results.push(time("SharedSecretKey::encapsulate (sender KEM)", ITERS, || {
let (_ssk, _epk) = SharedSecretKey::encapsulate(&vpk);
}));
// EncryptionScheme::encrypt / decrypt over a small Account note.
let account = Account::default();
let account_id = AccountId::new([7; 32]);
let commitment = Commitment::new(&account_id, &account);
let shared = {
let mut bytes = [0_u8; 32];
OsRng.fill_bytes(&mut bytes);
let esk: EphemeralSecretKey = bytes;
SharedSecretKey::new(esk, &vpk)
};
let (shared, _epk) = SharedSecretKey::encapsulate(&vpk);
let kind = PrivateAccountKind::Regular(0_u128);
let output_index: u32 = 0;

View File

@ -125,7 +125,7 @@ pub unsafe extern "C" fn wallet_ffi_get_private_account_keys(
// NPK is a 32-byte array
let npk_bytes = key_chain.nullifier_public_key.0;
// VPK is a compressed secp256k1 point (33 bytes)
// VPK is an ML-KEM-768 encapsulation key (1184 bytes)
let vpk_bytes = key_chain.viewing_public_key.to_bytes();
let vpk_len = vpk_bytes.len();
let vpk_vec = vpk_bytes.to_vec();

View File

@ -71,9 +71,9 @@ impl Default for FfiAccount {
pub struct FfiPrivateAccountKeys {
/// Nullifier public key (32 bytes).
pub nullifier_public_key: FfiBytes32,
/// viewing public key (compressed secp256k1 point).
/// Viewing public key (ML-KEM-768 encapsulation key, 1184 bytes).
pub viewing_public_key: *const u8,
/// Length of viewing public key (typically 33 bytes).
/// Length of viewing public key (always 1184 bytes for ML-KEM-768).
pub viewing_public_key_len: usize,
}

View File

@ -135,11 +135,11 @@ typedef struct FfiPrivateAccountKeys {
*/
struct FfiBytes32 nullifier_public_key;
/**
* viewing public key (compressed secp256k1 point).
* Viewing public key (ML-KEM-768 encapsulation key, 1184 bytes).
*/
const uint8_t *viewing_public_key;
/**
* Length of viewing public key (typically 33 bytes).
* Length of viewing public key (always 1184 bytes for ML-KEM-768).
*/
uintptr_t viewing_public_key_len;
} FfiPrivateAccountKeys;

View File

@ -266,8 +266,8 @@ impl WalletCore {
}
/// Set the wallet's dedicated sealing secret key.
pub fn set_sealing_secret_key(&mut self, key: key_protocol::key_management::secret_holders::ViewingSecretKey) {
self.storage.user_data.sealing_secret_key = Some(key);
pub const fn set_sealing_secret_key(&mut self, key: key_protocol::key_management::secret_holders::ViewingSecretKey) {
self.storage.key_chain_mut().set_sealing_secret_key(key);
}
/// Resolve an `AccountId` to the appropriate `PrivacyPreservingAccount` variant.

View File

@ -414,7 +414,7 @@ mod tests {
let acc = PrivacyPreservingAccount::PrivateShared {
nsk: [0; 32],
npk: NullifierPublicKey([1; 32]),
vpk: ViewingPublicKey::from_seed(&[2u8; 32], &[3u8; 32]),
vpk: ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]),
identifier: 42,
};
assert!(acc.is_private());

View File

@ -6,7 +6,7 @@ use key_protocol::key_management::{
KeyChain,
group_key_holder::GroupKeyHolder,
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex, traits::KeyTreeNode as _},
secret_holders::SeedHolder,
secret_holders::{SeedHolder, ViewingSecretKey},
};
use log::{debug, warn};
use nssa::{Account, AccountId};
@ -79,7 +79,7 @@ pub struct UserKeyChain {
/// Dedicated sealing secret key for GMS distribution. Generated once via
/// `wallet group new-sealing-key`. The corresponding public key is shared with
/// group members so they can seal GMS for this wallet.
sealing_secret_key: Option<nssa_core::encryption::Scalar>,
sealing_secret_key: Option<ViewingSecretKey>,
}
impl UserKeyChain {
@ -509,12 +509,12 @@ impl UserKeyChain {
/// Returns the sealing secret key for GMS distribution, if it exists.
#[must_use]
pub const fn sealing_secret_key(&self) -> Option<nssa_core::encryption::Scalar> {
self.sealing_secret_key
pub const fn sealing_secret_key(&self) -> Option<&ViewingSecretKey> {
self.sealing_secret_key.as_ref()
}
/// Sets the sealing secret key for GMS distribution.
pub const fn set_sealing_secret_key(&mut self, key: nssa_core::encryption::Scalar) {
pub const fn set_sealing_secret_key(&mut self, key: ViewingSecretKey) {
self.sealing_secret_key = Some(key);
}
@ -584,7 +584,7 @@ impl UserKeyChain {
KeyChainPersistentData {
accounts,
sealing_secret_key: *sealing_secret_key,
sealing_secret_key: sealing_secret_key.clone(),
group_key_holders: group_key_holders.clone(),
shared_private_accounts: shared_private_accounts.clone(),
}

View File

@ -5,6 +5,7 @@ use key_protocol::key_management::{
key_tree::{
chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic,
},
secret_holders::ViewingSecretKey,
};
use serde::{Deserialize, Serialize};
use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData};
@ -26,7 +27,7 @@ pub struct PersistentStorage {
pub struct KeyChainPersistentData {
pub accounts: Vec<PersistentAccountData>,
#[serde(default)]
pub sealing_secret_key: Option<nssa_core::encryption::Scalar>,
pub sealing_secret_key: Option<ViewingSecretKey>,
#[serde(default)]
pub group_key_holders: BTreeMap<Label, GroupKeyHolder>,
#[serde(default)]