mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-06-02 07:09:29 +00:00
BREAKING CHANGE: add identity variants to the circuit and change semantics for `Claim::Authorized` for private PDAs
938 lines
35 KiB
Rust
938 lines
35 KiB
Rust
use core::panic;
|
|
use std::collections::{BTreeMap, btree_map::Entry};
|
|
|
|
use anyhow::{Context as _, Result, anyhow};
|
|
use key_protocol::key_management::{
|
|
KeyChain,
|
|
group_key_holder::GroupKeyHolder,
|
|
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex, traits::KeyTreeNode as _},
|
|
secret_holders::SeedHolder,
|
|
};
|
|
use log::{debug, warn};
|
|
use nssa::{Account, AccountId};
|
|
use nssa_core::{Identifier, PrivateAccountKind};
|
|
use serde::{Deserialize, Serialize};
|
|
use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData};
|
|
|
|
use crate::{
|
|
account::{AccountIdWithPrivacy, Label},
|
|
storage::persistent::{
|
|
KeyChainPersistentData, PersistentAccountData, PersistentAccountDataPrivate,
|
|
PersistentAccountDataPublic,
|
|
},
|
|
};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct ImportedPrivateAccountKey {
|
|
pub key_chain: KeyChain,
|
|
/// We need to keep chain index even though it's not a generated account, because
|
|
/// it may have been generated in another wallet with some chain index and we need it for
|
|
/// decoding cyphertexts.
|
|
pub chain_index: Option<ChainIndex>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
|
pub struct ImportedPrivateAccountData {
|
|
pub accounts: BTreeMap<PrivateAccountKind, Account>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FoundPrivateAccount<'acc> {
|
|
pub account: &'acc Account,
|
|
pub key_chain: &'acc KeyChain,
|
|
pub kind: &'acc PrivateAccountKind,
|
|
pub chain_index: Option<ChainIndex>,
|
|
}
|
|
|
|
/// 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)]
|
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
|
pub struct SharedAccountEntry {
|
|
pub group_label: Label,
|
|
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).
|
|
pub pda_seed: Option<nssa_core::program::PdaSeed>,
|
|
pub authority_program_id: Option<nssa_core::program::ProgramId>,
|
|
pub account: Account,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
|
pub struct UserKeyChain {
|
|
/// Imported public accounts.
|
|
imported_public_accounts: BTreeMap<AccountId, nssa::PrivateKey>,
|
|
/// Imported private accounts.
|
|
imported_private_accounts: BTreeMap<ImportedPrivateAccountKey, ImportedPrivateAccountData>,
|
|
/// Tree of public account keys.
|
|
public_key_tree: KeyTreePublic,
|
|
/// Tree of private account keys.
|
|
private_key_tree: KeyTreePrivate,
|
|
/// 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.
|
|
shared_private_accounts: BTreeMap<nssa::AccountId, SharedAccountEntry>,
|
|
/// Group key holders for shared account management, keyed by a human-readable label.
|
|
group_key_holders: BTreeMap<Label, GroupKeyHolder>,
|
|
/// 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>,
|
|
}
|
|
|
|
impl UserKeyChain {
|
|
#[must_use]
|
|
pub const fn new_with_accounts(
|
|
public_key_tree: KeyTreePublic,
|
|
private_key_tree: KeyTreePrivate,
|
|
) -> Self {
|
|
Self {
|
|
imported_public_accounts: BTreeMap::new(),
|
|
imported_private_accounts: BTreeMap::new(),
|
|
public_key_tree,
|
|
private_key_tree,
|
|
group_key_holders: BTreeMap::new(),
|
|
shared_private_accounts: BTreeMap::new(),
|
|
sealing_secret_key: None,
|
|
}
|
|
}
|
|
|
|
/// Generate new trees for public and private keys up to given depth.
|
|
///
|
|
/// See [`key_protocol::key_management::key_tree::KeyTree::generate_tree_for_depth()`] for more
|
|
/// details.
|
|
pub fn generate_trees_for_depth(&mut self, depth: u32) {
|
|
self.public_key_tree.generate_tree_for_depth(depth);
|
|
self.private_key_tree.generate_tree_for_depth(depth);
|
|
}
|
|
|
|
/// Cleanup non-initialized accounts from the trees up to given depth.
|
|
///
|
|
/// For more details see
|
|
/// [`key_protocol::key_management::key_tree::KeyTreePublic::cleanup_tree_remove_uninit_layered()`]
|
|
/// and [`key_protocol::key_management::key_tree::KeyTreePrivate::cleanup_tree_remove_uninit_layered()`].
|
|
pub async fn cleanup_trees_remove_uninit_layered<F: Future<Output = Result<nssa::Account>>>(
|
|
&mut self,
|
|
depth: u32,
|
|
get_account: impl Fn(AccountId) -> F,
|
|
) -> Result<()> {
|
|
self.public_key_tree
|
|
.cleanup_tree_remove_uninit_layered(depth, get_account)
|
|
.await?;
|
|
self.private_key_tree
|
|
.cleanup_tree_remove_uninit_layered(depth);
|
|
Ok(())
|
|
}
|
|
|
|
/// 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>,
|
|
) -> (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 pub_account_signing_key(&self, account_id: AccountId) -> Option<&nssa::PrivateKey> {
|
|
self.imported_public_accounts
|
|
.get(&account_id)
|
|
.or_else(|| self.public_key_tree.get_node(account_id).map(Into::into))
|
|
}
|
|
|
|
/// Generated new private key for privacy preserving transactions.
|
|
///
|
|
/// Returns the `account_id` of new account.
|
|
pub fn generate_new_privacy_preserving_transaction_key_chain(
|
|
&mut self,
|
|
parent_cci: Option<ChainIndex>,
|
|
) -> (AccountId, ChainIndex) {
|
|
let chain_index = self.create_private_accounts_key(parent_cci);
|
|
let entry = self.private_key_tree.key_map.entry(chain_index.clone());
|
|
|
|
let Entry::Occupied(occupied) = entry else {
|
|
panic!("Newly created chain index must be present in a tree");
|
|
};
|
|
let node = occupied.get();
|
|
|
|
let npk = node.value.0.nullifier_public_key;
|
|
let (kind, _) = node
|
|
.value
|
|
.1
|
|
.first_key_value()
|
|
.expect("Newly created key chain node must have at least one account");
|
|
let account_id = AccountId::for_private_account(&npk, kind);
|
|
(account_id, chain_index)
|
|
}
|
|
|
|
/// 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 private account for given `account_id`. Doesn't search in pda accounts cache.
|
|
/// Does not cover shared private accounts — use [`UserKeyChain::shared_private_account()`] for
|
|
/// those.
|
|
#[must_use]
|
|
pub fn private_account(&self, account_id: AccountId) -> Option<FoundPrivateAccount<'_>> {
|
|
self.imported_private_accounts
|
|
.iter()
|
|
.flat_map(|(key, data)| {
|
|
data.accounts
|
|
.iter()
|
|
.map(|(kind, account)| FoundPrivateAccount {
|
|
account,
|
|
key_chain: &key.key_chain,
|
|
kind,
|
|
chain_index: key.chain_index.clone(),
|
|
})
|
|
})
|
|
.chain(
|
|
self.private_key_tree
|
|
.key_map
|
|
.iter()
|
|
.flat_map(|(chain_index, data)| {
|
|
data.value
|
|
.1
|
|
.iter()
|
|
.map(|(kind, account)| FoundPrivateAccount {
|
|
account,
|
|
key_chain: &data.value.0,
|
|
kind,
|
|
chain_index: Some(chain_index.clone()),
|
|
})
|
|
}),
|
|
)
|
|
.find_map(|found| {
|
|
let expected_id = AccountId::for_private_account(
|
|
&found.key_chain.nullifier_public_key,
|
|
found.kind,
|
|
);
|
|
(expected_id == account_id).then_some(found)
|
|
})
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn private_account_key_chain_by_index(
|
|
&self,
|
|
chain_index: &ChainIndex,
|
|
) -> Option<&KeyChain> {
|
|
self.private_key_tree
|
|
.key_map
|
|
.get(chain_index)
|
|
.map(|data| &data.value.0)
|
|
}
|
|
|
|
pub fn private_account_key_chains(
|
|
&self,
|
|
) -> impl Iterator<Item = (AccountId, &KeyChain, Option<&ChainIndex>)> {
|
|
self.imported_private_accounts
|
|
.iter()
|
|
.flat_map(|(key, data)| {
|
|
data.accounts.keys().map(|kind| {
|
|
let account_id =
|
|
AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind);
|
|
(account_id, &key.key_chain, key.chain_index.as_ref())
|
|
})
|
|
})
|
|
.chain(
|
|
self.private_key_tree
|
|
.key_map
|
|
.iter()
|
|
.flat_map(|(chain_index, keys_node)| {
|
|
keys_node.account_ids().map(move |account_id| {
|
|
(account_id, &keys_node.value.0, Some(chain_index))
|
|
})
|
|
}),
|
|
)
|
|
}
|
|
|
|
pub fn add_imported_public_account(&mut self, private_key: nssa::PrivateKey) {
|
|
let account_id = AccountId::from(&nssa::PublicKey::new_from_private_key(&private_key));
|
|
|
|
self.imported_public_accounts
|
|
.insert(account_id, private_key);
|
|
}
|
|
|
|
pub fn add_imported_private_account(
|
|
&mut self,
|
|
key_chain: KeyChain,
|
|
chain_index: Option<ChainIndex>,
|
|
identifier: Identifier,
|
|
account: Account,
|
|
) {
|
|
let key = ImportedPrivateAccountKey {
|
|
key_chain,
|
|
chain_index,
|
|
};
|
|
let kind = PrivateAccountKind::Regular(identifier);
|
|
let entry = self.imported_private_accounts.entry(key.clone());
|
|
match entry {
|
|
Entry::Occupied(mut occupied) => {
|
|
let data = occupied.get_mut();
|
|
let per_id_entry = data.accounts.entry(kind);
|
|
if let Entry::Occupied(per_id_occupied) = &per_id_entry {
|
|
let existing_account = per_id_occupied.get();
|
|
if existing_account != &account {
|
|
warn!(
|
|
"Overwriting existing imported private account for key {key:?}. \
|
|
Existing account: {existing_account:?}, new account: {account:?}",
|
|
);
|
|
}
|
|
}
|
|
per_id_entry.insert_entry(account);
|
|
}
|
|
Entry::Vacant(vacant) => {
|
|
vacant.insert_entry(ImportedPrivateAccountData {
|
|
accounts: BTreeMap::from_iter([(kind, account)]),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn insert_private_account(
|
|
&mut self,
|
|
account_id: AccountId,
|
|
kind: PrivateAccountKind,
|
|
account: nssa_core::account::Account,
|
|
) -> Result<()> {
|
|
// Try to find in shared accounts
|
|
if let Some(entry) = self.shared_private_accounts.get_mut(&account_id) {
|
|
debug!("Updating shared private account {account_id}");
|
|
entry.account = account;
|
|
return Ok(());
|
|
}
|
|
|
|
// Then try to update imported account
|
|
for (key, data) in &mut self.imported_private_accounts {
|
|
for (kind, imported_account) in &mut data.accounts {
|
|
let expected_id =
|
|
AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind);
|
|
if expected_id == account_id {
|
|
debug!("Updating imported private account {account_id}");
|
|
*imported_account = account;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Otherwise update the private key tree
|
|
|
|
let chain_index = self.private_key_tree.account_id_map.get(&account_id);
|
|
|
|
if let Some(chain_index) = chain_index {
|
|
// Node already in account_id_map — update its entry
|
|
let node = self
|
|
.private_key_tree
|
|
.key_map
|
|
.get_mut(chain_index)
|
|
.expect("Node must be present in a tree");
|
|
|
|
match node.value.1.entry(kind) {
|
|
Entry::Occupied(mut occupied) => {
|
|
debug!("Updating generated private account {account_id}");
|
|
occupied.insert(account);
|
|
}
|
|
Entry::Vacant(vacant) => {
|
|
debug!("Inserting new private account identity {account_id}");
|
|
vacant.insert(account);
|
|
}
|
|
}
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
// Node not yet in account_id_map — find it by checking all nodes
|
|
for (ci, node) in &mut self.private_key_tree.key_map {
|
|
let expected_id =
|
|
nssa::AccountId::for_private_account(&node.value.0.nullifier_public_key, &kind);
|
|
if expected_id == account_id {
|
|
match node.value.1.entry(kind) {
|
|
Entry::Occupied(mut occupied) => {
|
|
debug!("Updating generated private account {account_id}");
|
|
occupied.insert(account);
|
|
}
|
|
Entry::Vacant(vacant) => {
|
|
debug!("Inserting new private account identity {account_id}");
|
|
vacant.insert(account);
|
|
}
|
|
}
|
|
// Register in account_id_map
|
|
self.private_key_tree
|
|
.account_id_map
|
|
.insert(account_id, ci.clone());
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
Err(anyhow!("Account ID {account_id} not found in key chain"))
|
|
}
|
|
|
|
pub fn account_ids(&self) -> impl Iterator<Item = (AccountIdWithPrivacy, Option<&ChainIndex>)> {
|
|
self.public_account_ids()
|
|
.map(|(account_id, chain_index)| {
|
|
(AccountIdWithPrivacy::Public(account_id), chain_index)
|
|
})
|
|
.chain(self.private_account_ids().map(|(account_id, chain_index)| {
|
|
(AccountIdWithPrivacy::Private(account_id), chain_index)
|
|
}))
|
|
}
|
|
|
|
pub fn public_account_ids(&self) -> impl Iterator<Item = (AccountId, Option<&ChainIndex>)> {
|
|
self.imported_public_accounts
|
|
.keys()
|
|
.map(|account_id| (*account_id, None))
|
|
.chain(
|
|
self.public_key_tree
|
|
.account_id_map
|
|
.iter()
|
|
.map(|(account_id, chain_index)| (*account_id, Some(chain_index))),
|
|
)
|
|
}
|
|
|
|
pub fn private_account_ids(&self) -> impl Iterator<Item = (AccountId, Option<&ChainIndex>)> {
|
|
self.imported_private_accounts
|
|
.iter()
|
|
.flat_map(|(key, data)| {
|
|
data.accounts.keys().map(|kind| {
|
|
let account_id =
|
|
AccountId::for_private_account(&key.key_chain.nullifier_public_key, kind);
|
|
(account_id, key.chain_index.as_ref())
|
|
})
|
|
})
|
|
.chain(
|
|
self.private_key_tree
|
|
.key_map
|
|
.iter()
|
|
.flat_map(|(chain_index, keys_node)| {
|
|
keys_node
|
|
.account_ids()
|
|
.map(move |account_id| (account_id, Some(chain_index)))
|
|
}),
|
|
)
|
|
.chain(self.shared_private_accounts.keys().map(|id| (*id, None)))
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
/// 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: Label, holder: GroupKeyHolder) {
|
|
self.group_key_holders.insert(label, holder);
|
|
}
|
|
|
|
/// Removes the `GroupKeyHolder` under the given label, if it exists.
|
|
pub fn remove_group_key_holder(&mut self, label: &Label) -> Option<GroupKeyHolder> {
|
|
self.group_key_holders.remove(label)
|
|
}
|
|
|
|
/// Returns the `GroupKeyHolder` for the given label, if it exists.
|
|
#[must_use]
|
|
pub fn group_key_holder(&self, label: &Label) -> Option<&GroupKeyHolder> {
|
|
self.group_key_holders.get(label)
|
|
}
|
|
|
|
/// Iterates over all group key holders.
|
|
pub fn group_key_holders_iter(&self) -> impl Iterator<Item = (&Label, &GroupKeyHolder)> {
|
|
self.group_key_holders.iter()
|
|
}
|
|
|
|
/// Iterates over all shared private accounts.
|
|
pub fn shared_private_accounts_iter(
|
|
&self,
|
|
) -> impl Iterator<Item = (&nssa::AccountId, &SharedAccountEntry)> {
|
|
self.shared_private_accounts.iter()
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
/// Sets the sealing secret key for GMS distribution.
|
|
pub const fn set_sealing_secret_key(&mut self, key: nssa_core::encryption::Scalar) {
|
|
self.sealing_secret_key = Some(key);
|
|
}
|
|
|
|
pub(super) fn to_persistent(&self) -> KeyChainPersistentData {
|
|
let Self {
|
|
imported_public_accounts,
|
|
imported_private_accounts,
|
|
public_key_tree,
|
|
private_key_tree,
|
|
shared_private_accounts,
|
|
group_key_holders,
|
|
sealing_secret_key,
|
|
} = self;
|
|
|
|
let mut accounts = vec![];
|
|
|
|
for (account_id, chain_index) in &public_key_tree.account_id_map {
|
|
if let Some(data) = public_key_tree.key_map.get(chain_index) {
|
|
accounts.push(PersistentAccountData::Public(PersistentAccountDataPublic {
|
|
account_id: *account_id,
|
|
chain_index: chain_index.clone(),
|
|
data: data.clone(),
|
|
}));
|
|
}
|
|
}
|
|
|
|
for (account_id, key) in &private_key_tree.account_id_map {
|
|
if let Some(data) = private_key_tree.key_map.get(key) {
|
|
accounts.push(PersistentAccountData::Private(Box::new(
|
|
PersistentAccountDataPrivate {
|
|
account_id: *account_id,
|
|
chain_index: key.clone(),
|
|
data: data.clone().into(),
|
|
},
|
|
)));
|
|
}
|
|
}
|
|
|
|
for (account_id, key) in imported_public_accounts {
|
|
accounts.push(PersistentAccountData::ImportedPublic(
|
|
PublicAccountPrivateInitialData {
|
|
account_id: *account_id,
|
|
pub_sign_key: key.clone(),
|
|
},
|
|
));
|
|
}
|
|
|
|
for (key, data) in imported_private_accounts {
|
|
let ImportedPrivateAccountKey {
|
|
key_chain,
|
|
chain_index,
|
|
} = key;
|
|
let ImportedPrivateAccountData {
|
|
accounts: imported_accounts,
|
|
} = data;
|
|
for (kind, account) in imported_accounts {
|
|
accounts.push(PersistentAccountData::ImportedPrivate(Box::new(
|
|
PrivateAccountPrivateInitialData {
|
|
account: account.clone(),
|
|
key_chain: key_chain.clone(),
|
|
chain_index: chain_index.clone(),
|
|
identifier: kind.identifier(),
|
|
},
|
|
)));
|
|
}
|
|
}
|
|
|
|
KeyChainPersistentData {
|
|
accounts,
|
|
sealing_secret_key: *sealing_secret_key,
|
|
group_key_holders: group_key_holders.clone(),
|
|
shared_private_accounts: shared_private_accounts.clone(),
|
|
}
|
|
}
|
|
|
|
#[expect(
|
|
clippy::wildcard_enum_match_arm,
|
|
reason = "We perform search for specific variants only"
|
|
)]
|
|
pub(super) fn from_persistent(key_chain_data: KeyChainPersistentData) -> Result<Self> {
|
|
let KeyChainPersistentData {
|
|
accounts: persistent_accounts,
|
|
sealing_secret_key,
|
|
group_key_holders,
|
|
shared_private_accounts,
|
|
} = key_chain_data;
|
|
|
|
let mut imported_public_accounts = BTreeMap::new();
|
|
let mut imported_private_accounts = BTreeMap::new();
|
|
|
|
let public_root = persistent_accounts
|
|
.iter()
|
|
.find(|data| match data {
|
|
&PersistentAccountData::Public(data) => data.chain_index == ChainIndex::root(),
|
|
_ => false,
|
|
})
|
|
.cloned()
|
|
.context("Malformed persistent account data, must have public root")?;
|
|
|
|
let private_root = persistent_accounts
|
|
.iter()
|
|
.find(|data| match data {
|
|
&PersistentAccountData::Private(data) => data.chain_index == ChainIndex::root(),
|
|
_ => false,
|
|
})
|
|
.cloned()
|
|
.context("Malformed persistent account data, must have private root")?;
|
|
|
|
let mut public_key_tree = KeyTreePublic::new_from_root(match public_root {
|
|
PersistentAccountData::Public(data) => data.data,
|
|
_ => unreachable!(),
|
|
});
|
|
let mut private_key_tree = KeyTreePrivate::new_from_root(match private_root {
|
|
PersistentAccountData::Private(data) => data.data.into(),
|
|
_ => unreachable!(),
|
|
});
|
|
|
|
for pers_acc_data in persistent_accounts {
|
|
match pers_acc_data {
|
|
PersistentAccountData::Public(data) => {
|
|
public_key_tree.insert(data.account_id, data.chain_index, data.data);
|
|
}
|
|
PersistentAccountData::Private(data) => {
|
|
private_key_tree.insert(data.account_id, data.chain_index, data.data.into());
|
|
}
|
|
PersistentAccountData::ImportedPublic(data) => {
|
|
imported_public_accounts.insert(data.account_id, data.pub_sign_key);
|
|
}
|
|
PersistentAccountData::ImportedPrivate(data) => {
|
|
imported_private_accounts
|
|
.entry(ImportedPrivateAccountKey {
|
|
key_chain: data.key_chain,
|
|
chain_index: data.chain_index,
|
|
})
|
|
.or_insert_with(|| ImportedPrivateAccountData {
|
|
accounts: BTreeMap::new(),
|
|
})
|
|
.accounts
|
|
.insert(PrivateAccountKind::Regular(data.identifier), data.account);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Self {
|
|
imported_public_accounts,
|
|
imported_private_accounts,
|
|
public_key_tree,
|
|
private_key_tree,
|
|
shared_private_accounts,
|
|
group_key_holders,
|
|
sealing_secret_key,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Default for UserKeyChain {
|
|
fn default() -> Self {
|
|
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
|
|
Self::new_with_accounts(
|
|
KeyTreePublic::new(&seed_holder),
|
|
KeyTreePrivate::new(&seed_holder),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn new_account() {
|
|
let mut user_data = UserKeyChain::default();
|
|
|
|
let (account_id_private, _) = user_data
|
|
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
|
|
|
|
let is_key_chain_generated = user_data.private_account(account_id_private).is_some();
|
|
|
|
assert!(is_key_chain_generated);
|
|
|
|
let account_id_private_str = account_id_private.to_string();
|
|
println!("{account_id_private_str:#?}");
|
|
let account = &user_data.private_account(account_id_private).unwrap();
|
|
println!("{account:#?}");
|
|
}
|
|
|
|
#[test]
|
|
fn add_imported_public_account() {
|
|
let mut user_data = UserKeyChain::default();
|
|
|
|
let private_key = nssa::PrivateKey::new_os_random();
|
|
let account_id = AccountId::from(&nssa::PublicKey::new_from_private_key(&private_key));
|
|
|
|
user_data.add_imported_public_account(private_key);
|
|
|
|
let is_account_added = user_data.pub_account_signing_key(account_id).is_some();
|
|
|
|
assert!(is_account_added);
|
|
}
|
|
|
|
#[test]
|
|
fn add_imported_private_account() {
|
|
let mut user_data = UserKeyChain::default();
|
|
|
|
let key_chain = KeyChain::new_os_random();
|
|
let account_id = AccountId::from((&key_chain.nullifier_public_key, 0));
|
|
let account = nssa_core::account::Account::default();
|
|
|
|
user_data.add_imported_private_account(key_chain, None, 0, account);
|
|
|
|
let is_account_added = user_data.private_account(account_id).is_some();
|
|
|
|
assert!(is_account_added);
|
|
}
|
|
|
|
#[test]
|
|
fn insert_private_imported_account() {
|
|
let mut user_data = UserKeyChain::default();
|
|
|
|
let key_chain = KeyChain::new_os_random();
|
|
let account_id = AccountId::from((&key_chain.nullifier_public_key, 0));
|
|
let account = nssa_core::account::Account::default();
|
|
|
|
user_data.add_imported_private_account(key_chain, None, 0, account.clone());
|
|
|
|
let new_account = nssa_core::account::Account {
|
|
balance: 100,
|
|
..account
|
|
};
|
|
|
|
user_data
|
|
.insert_private_account(account_id, PrivateAccountKind::Regular(0), new_account)
|
|
.unwrap();
|
|
|
|
let retrieved_account = &user_data.private_account(account_id).unwrap();
|
|
|
|
assert_eq!(retrieved_account.account.balance, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn insert_private_non_imported_account() {
|
|
let mut user_data = UserKeyChain::default();
|
|
|
|
let (account_id, _chain_index) = user_data
|
|
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
|
|
|
|
let new_account = nssa_core::account::Account {
|
|
balance: 100,
|
|
..nssa_core::account::Account::default()
|
|
};
|
|
|
|
user_data
|
|
.insert_private_account(account_id, PrivateAccountKind::Regular(0), new_account)
|
|
.unwrap();
|
|
|
|
let retrieved_account = &user_data.private_account(account_id).unwrap();
|
|
|
|
assert_eq!(retrieved_account.account.balance, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn insert_private_non_existent_account() {
|
|
let mut user_data = UserKeyChain::default();
|
|
|
|
let key_chain = KeyChain::new_os_random();
|
|
let account_id = AccountId::from((&key_chain.nullifier_public_key, 0));
|
|
|
|
let new_account = nssa_core::account::Account {
|
|
balance: 100,
|
|
..nssa_core::account::Account::default()
|
|
};
|
|
|
|
let result = user_data.insert_private_account(
|
|
account_id,
|
|
PrivateAccountKind::Regular(0),
|
|
new_account,
|
|
);
|
|
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn private_key_chain_iteration() {
|
|
let mut user_data = UserKeyChain::default();
|
|
|
|
let key_chain = KeyChain::new_os_random();
|
|
let account_id1 = AccountId::from((&key_chain.nullifier_public_key, 0));
|
|
let account = nssa_core::account::Account::default();
|
|
user_data.add_imported_private_account(key_chain, None, 0, account);
|
|
|
|
let (account_id2, chain_index2) = user_data
|
|
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
|
|
let (account_id3, chain_index3) = user_data
|
|
.generate_new_privacy_preserving_transaction_key_chain(Some(chain_index2.clone()));
|
|
|
|
let key_chains: Vec<(AccountId, &KeyChain, Option<&ChainIndex>)> =
|
|
user_data.private_account_key_chains().collect();
|
|
|
|
assert_eq!(key_chains.len(), 4); // 1 default + 1 imported + 2 generated accounts
|
|
// Imported account first
|
|
assert_eq!(key_chains[0].0, account_id1);
|
|
assert_eq!(key_chains[0].2, None);
|
|
// Skip key_chains[1] as it's default root account
|
|
// Then goes generated accounts
|
|
assert_eq!(key_chains[2].0, account_id2);
|
|
assert_eq!(key_chains[2].2, Some(&chain_index2));
|
|
assert_eq!(key_chains[3].0, account_id3);
|
|
assert_eq!(key_chains[3].2, Some(&chain_index3));
|
|
}
|
|
|
|
#[test]
|
|
fn group_key_holder_storage_round_trip() {
|
|
let mut user_data = UserKeyChain::default();
|
|
assert!(
|
|
user_data
|
|
.group_key_holder(&Label::new("test-group"))
|
|
.is_none()
|
|
);
|
|
|
|
let holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
|
user_data.insert_group_key_holder(Label::new("test-group"), holder.clone());
|
|
|
|
let retrieved = user_data
|
|
.group_key_holder(&Label::new("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 = UserKeyChain::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: Label::new("test-group"),
|
|
identifier: 42,
|
|
pda_seed: None,
|
|
authority_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, Label::new("test-group"));
|
|
assert_eq!(decoded.identifier, 42);
|
|
assert!(decoded.pda_seed.is_none());
|
|
|
|
let pda_entry = SharedAccountEntry {
|
|
group_label: Label::new("pda-group"),
|
|
identifier: u128::MAX,
|
|
pda_seed: Some(PdaSeed::new([7_u8; 32])),
|
|
authority_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, Label::new("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: Label::new("old"),
|
|
identifier: 1,
|
|
pda_seed: None,
|
|
authority_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, Label::new("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 = UserKeyChain::default();
|
|
let gms_holder = GroupKeyHolder::from_gms([42_u8; 32]);
|
|
user_data.insert_group_key_holder(Label::new("my-group"), gms_holder);
|
|
|
|
let holder = user_data.group_key_holder(&Label::new("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(),
|
|
);
|
|
}
|
|
}
|