mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-11 00:09:39 +00:00
Merge origin/artem/change-wallet-balance-fetch into artem/user-view-tag
Brings the nullifier-based wallet balance fetch (NullifierIndex, sync_updates_via_nullifiers) onto the vpk-binding base, so account updates are found by nullifier rather than only by view tag. Conflict resolutions (semantic): - wallet/lib.rs: keep vpk-binding's for_private_account(npk, vpk, kind); add theirs' nsk into the 4-tuple consumed by index.track(). - wallet/storage/key_chain.rs: keep theirs' private_account/private_accounts split (the iterator is reused by the nullifier index) and restore the vpk in the derivation; adapt 4 pre-vpk call sites (for_private_account 2->3 arg, AccountId::from((npk,id)) -> (npk,vpk,id)). - artifacts/*: took ours; regenerate with `just build-artifacts`.
This commit is contained in:
commit
859ffd3900
@ -1,6 +1,6 @@
|
||||
use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _};
|
||||
use lee_core::{
|
||||
SharedSecretKey,
|
||||
Identifier, SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey},
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
@ -146,6 +146,23 @@ impl GroupKeyHolder {
|
||||
SecretSpendingKey(hasher.finalize_fixed().into()).produce_private_key_holder(None)
|
||||
}
|
||||
|
||||
/// Derive keys for a shared regular account from its `identifier`.
|
||||
///
|
||||
/// Computes the derivation seed via the `SharedAccountTag` domain separator, then delegates
|
||||
/// to [`Self::derive_keys_for_shared_account`].
|
||||
#[must_use]
|
||||
pub fn derive_regular_shared_account_keys_from_identifier(
|
||||
&self,
|
||||
identifier: Identifier,
|
||||
) -> PrivateKeyHolder {
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00";
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PREFIX);
|
||||
hasher.update(identifier.to_le_bytes());
|
||||
let derivation_seed: [u8; 32] = hasher.finalize().into();
|
||||
self.derive_keys_for_shared_account(&derivation_seed)
|
||||
}
|
||||
|
||||
/// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`].
|
||||
///
|
||||
/// Uses ML-KEM-768 encapsulation to derive a shared secret, then AES-256-GCM to encrypt
|
||||
|
||||
@ -324,7 +324,7 @@ impl AccountPostState {
|
||||
pub type BlockValidityWindow = ValidityWindow<BlockId>;
|
||||
pub type TimestampValidityWindow = ValidityWindow<Timestamp>;
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)
|
||||
|
||||
@ -11,7 +11,7 @@ use crate::{AccountId, error::LeeError};
|
||||
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
#[derive(Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub public_account_ids: Vec<AccountId>,
|
||||
pub nonces: Vec<Nonce>,
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
reason = "Most of the shadows come from args parsing which is ok"
|
||||
)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
|
||||
pub use account_manager::AccountIdentity;
|
||||
use anyhow::{Context as _, Result};
|
||||
@ -18,7 +18,8 @@ use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use lee::{
|
||||
Account, AccountId, PrivacyPreservingTransaction, ProgramId,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::ProgramWithDependencies, message::EncryptedAccountData,
|
||||
circuit::ProgramWithDependencies,
|
||||
message::{EncryptedAccountData, Message},
|
||||
},
|
||||
};
|
||||
use lee_core::{
|
||||
@ -34,7 +35,7 @@ use crate::{
|
||||
account::{AccountIdWithPrivacy, Label},
|
||||
config::WalletConfigOverrides,
|
||||
poller::TxPoller,
|
||||
storage::key_chain::SharedAccountEntry,
|
||||
storage::key_chain::{NullifierIndex, SharedAccountEntry},
|
||||
};
|
||||
|
||||
pub mod account;
|
||||
@ -298,35 +299,26 @@ impl WalletCore {
|
||||
.storage
|
||||
.key_chain()
|
||||
.shared_private_account(account_id)?;
|
||||
let holder = self
|
||||
.storage
|
||||
.key_chain()
|
||||
.group_key_holder(&entry.group_label)?;
|
||||
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 let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.authority_program_id) {
|
||||
let keys = holder.derive_keys_for_pda(&program_id, &pda_seed);
|
||||
if entry.pda_seed.is_some() {
|
||||
Some(AccountIdentity::PrivatePdaShared {
|
||||
account_id,
|
||||
nsk: keys.nullifier_secret_key,
|
||||
npk: keys.generate_nullifier_public_key(),
|
||||
vpk: keys.generate_viewing_public_key(),
|
||||
identifier: entry.identifier,
|
||||
nsk,
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
})
|
||||
} else {
|
||||
let derivation_seed = {
|
||||
use sha2::Digest as _;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
|
||||
hasher.update(entry.identifier.to_le_bytes());
|
||||
let result: [u8; 32] = hasher.finalize().into();
|
||||
result
|
||||
};
|
||||
let keys = holder.derive_keys_for_shared_account(&derivation_seed);
|
||||
Some(AccountIdentity::PrivateShared {
|
||||
nsk: keys.nullifier_secret_key,
|
||||
npk: keys.generate_nullifier_public_key(),
|
||||
vpk: keys.generate_viewing_public_key(),
|
||||
identifier: entry.identifier,
|
||||
nsk,
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -401,14 +393,6 @@ impl WalletCore {
|
||||
group_name: Label,
|
||||
) -> Result<SharedAccountInfo> {
|
||||
let identifier: lee_core::Identifier = rand::random();
|
||||
let derivation_seed = {
|
||||
use sha2::Digest as _;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
|
||||
hasher.update(identifier.to_le_bytes());
|
||||
let result: [u8; 32] = hasher.finalize().into();
|
||||
result
|
||||
};
|
||||
|
||||
let holder = self
|
||||
.storage
|
||||
@ -416,7 +400,7 @@ impl WalletCore {
|
||||
.group_key_holder(&group_name)
|
||||
.context(format!("Group '{group_name}' not found"))?;
|
||||
|
||||
let keys = holder.derive_keys_for_shared_account(&derivation_seed);
|
||||
let keys = holder.derive_regular_shared_account_keys_from_identifier(identifier);
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let account_id = AccountId::from((&npk, &vpk, identifier));
|
||||
@ -739,10 +723,20 @@ impl WalletCore {
|
||||
let mut blocks =
|
||||
std::pin::pin!(poller.poll_block_range(last_synced_block.saturating_add(1)..=block_id));
|
||||
|
||||
// Get the latest nullifiers for all owned accounts.
|
||||
let mut index = self.storage.key_chain().build_latest_nullifier_index();
|
||||
let bar = indicatif::ProgressBar::new(num_of_blocks);
|
||||
while let Some(block) = blocks.try_next().await? {
|
||||
for tx in block.body.transactions {
|
||||
self.sync_private_accounts_with_tx(tx);
|
||||
let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else {
|
||||
continue;
|
||||
};
|
||||
// Eagerly decrypt note updates using expected nullifiers.
|
||||
let handled = self
|
||||
.storage
|
||||
.key_chain_mut()
|
||||
.sync_updates_via_nullifiers(&pp_tx.message, &mut index);
|
||||
self.sync_private_accounts_with_tx(&pp_tx.message, &mut index, &handled);
|
||||
}
|
||||
|
||||
self.storage.set_last_synced_block(block.header.block_id);
|
||||
@ -759,11 +753,12 @@ impl WalletCore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_private_accounts_with_tx(&mut self, tx: LeeTransaction) {
|
||||
let LeeTransaction::PrivacyPreserving(tx) = tx else {
|
||||
return;
|
||||
};
|
||||
|
||||
fn sync_private_accounts_with_tx(
|
||||
&mut self,
|
||||
message: &Message,
|
||||
index: &mut NullifierIndex,
|
||||
handled: &HashSet<usize>,
|
||||
) {
|
||||
let affected_accounts = self
|
||||
.storage
|
||||
.key_chain()
|
||||
@ -773,13 +768,18 @@ impl WalletCore {
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
);
|
||||
let new_commitments = &tx.message.new_commitments;
|
||||
let new_commitments = &message.new_commitments;
|
||||
|
||||
tx.message()
|
||||
message
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(move |(_, encrypted_data)| encrypted_data.view_tag == view_tag)
|
||||
.filter(move |(ciph_id, encrypted_data)| {
|
||||
// If we have not decrypted the update using the nullifiers,
|
||||
// the note may be an initialized one, for which we should
|
||||
// scan.
|
||||
!handled.contains(ciph_id) && encrypted_data.view_tag == view_tag
|
||||
})
|
||||
.filter_map(move |(ciph_id, encrypted_data)| {
|
||||
let ciphertext = &encrypted_data.ciphertext;
|
||||
let commitment = &new_commitments[ciph_id];
|
||||
@ -801,17 +801,21 @@ impl WalletCore {
|
||||
&key_chain.viewing_public_key,
|
||||
&kind,
|
||||
);
|
||||
(account_id, kind, res_acc)
|
||||
let nsk = key_chain.private_key_holder.nullifier_secret_key;
|
||||
(account_id, kind, res_acc, nsk)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (affected_account_id, kind, new_acc) in affected_accounts {
|
||||
for (affected_account_id, kind, new_acc, nsk) in affected_accounts {
|
||||
info!(
|
||||
"Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}"
|
||||
);
|
||||
// Await the account's next update by its nullifier, so later updates
|
||||
// to it are caught without tag matching.
|
||||
index.track(affected_account_id, &new_acc, &nsk);
|
||||
self.storage
|
||||
.key_chain_mut()
|
||||
.insert_private_account(affected_account_id, kind, new_acc)
|
||||
@ -819,54 +823,37 @@ impl WalletCore {
|
||||
}
|
||||
|
||||
// Scan for updates to shared accounts (GMS-derived).
|
||||
self.sync_shared_private_accounts_with_tx(&tx);
|
||||
self.sync_shared_private_accounts_with_tx(message, index, handled);
|
||||
}
|
||||
|
||||
fn sync_shared_private_accounts_with_tx(&mut self, tx: &PrivacyPreservingTransaction) {
|
||||
fn sync_shared_private_accounts_with_tx(
|
||||
&mut self,
|
||||
message: &Message,
|
||||
index: &mut NullifierIndex,
|
||||
handled: &HashSet<usize>,
|
||||
) {
|
||||
let shared_keys: Vec<_> = self
|
||||
.storage
|
||||
.key_chain()
|
||||
.shared_private_accounts_iter()
|
||||
.filter_map(|(&account_id, entry)| {
|
||||
let holder = self
|
||||
.storage
|
||||
.key_chain()
|
||||
.group_key_holder(&entry.group_label)?;
|
||||
|
||||
let keys = match (&entry.pda_seed, &entry.authority_program_id) {
|
||||
(Some(pda_seed), Some(program_id)) => {
|
||||
holder.derive_keys_for_pda(program_id, pda_seed)
|
||||
}
|
||||
(Some(_), None) => return None, // PDA without program_id, skip
|
||||
_ => {
|
||||
let derivation_seed = {
|
||||
use sha2::Digest as _;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00");
|
||||
hasher.update(entry.identifier.to_le_bytes());
|
||||
let result: [u8; 32] = hasher.finalize().into();
|
||||
result
|
||||
};
|
||||
holder.derive_keys_for_shared_account(&derivation_seed)
|
||||
}
|
||||
};
|
||||
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 vsk = keys.viewing_secret_key;
|
||||
Some((account_id, npk, vpk, vsk))
|
||||
Some((account_id, npk, vpk, vsk, nsk))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (account_id, npk, vpk, vsk) in shared_keys {
|
||||
for (account_id, npk, vpk, vsk, nsk) in shared_keys {
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk);
|
||||
|
||||
for (ciph_id, encrypted_data) in tx
|
||||
.message()
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.enumerate()
|
||||
for (ciph_id, encrypted_data) in
|
||||
message.encrypted_private_post_states.iter().enumerate()
|
||||
{
|
||||
if encrypted_data.view_tag != view_tag {
|
||||
// If already decrypted or the tag does nto match, skip.
|
||||
if handled.contains(&ciph_id) || encrypted_data.view_tag != view_tag {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -875,7 +862,7 @@ impl WalletCore {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let commitment = &tx.message.new_commitments[ciph_id];
|
||||
let commitment = &message.new_commitments[ciph_id];
|
||||
|
||||
if let Some((_kind, new_acc)) = lee_core::EncryptionScheme::decrypt(
|
||||
&encrypted_data.ciphertext,
|
||||
@ -886,6 +873,7 @@ impl WalletCore {
|
||||
.expect("Ciphertext ID is expected to fit in u32"),
|
||||
) {
|
||||
info!("Synced shared account {account_id:#?} with new state {new_acc:#?}");
|
||||
index.track(account_id, &new_acc, &nsk);
|
||||
self.storage
|
||||
.key_chain_mut()
|
||||
.update_shared_private_account_state(&account_id, new_acc);
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
use core::panic;
|
||||
use std::collections::{BTreeMap, btree_map::Entry};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet, 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, ViewingSecretKey},
|
||||
secret_holders::{PrivateKeyHolder, SeedHolder, ViewingSecretKey},
|
||||
};
|
||||
use lee::{Account, AccountId, privacy_preserving_transaction::message::Message};
|
||||
use lee_core::{
|
||||
Commitment, EncryptionScheme, Identifier, Nullifier, NullifierSecretKey, PrivateAccountKind,
|
||||
SharedSecretKey,
|
||||
};
|
||||
use lee::{Account, AccountId};
|
||||
use lee_core::{Identifier, PrivateAccountKind};
|
||||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData};
|
||||
@ -59,6 +62,41 @@ pub struct SharedAccountEntry {
|
||||
pub account: Account,
|
||||
}
|
||||
|
||||
/// Maps each owned or shared private account to the nullifier its next update will publish,
|
||||
/// so sync can spot updates by nullifier rather than view tag.
|
||||
#[derive(Default)]
|
||||
pub struct NullifierIndex(HashMap<Nullifier, AccountId>);
|
||||
|
||||
impl NullifierIndex {
|
||||
fn next_update_nullifier(
|
||||
account_id: AccountId,
|
||||
account: &Account,
|
||||
nsk: &NullifierSecretKey,
|
||||
) -> Nullifier {
|
||||
Nullifier::for_account_update(&Commitment::new(&account_id, account), nsk)
|
||||
}
|
||||
|
||||
/// Returns the account whose next update would publish `nullifier`.
|
||||
#[must_use]
|
||||
pub fn account_for(&self, nullifier: &Nullifier) -> Option<AccountId> {
|
||||
self.0.get(nullifier).copied()
|
||||
}
|
||||
|
||||
/// Indexes `account_id` by the nullifier its next update will publish.
|
||||
pub fn track(&mut self, account_id: AccountId, account: &Account, nsk: &NullifierSecretKey) {
|
||||
self.0.insert(
|
||||
Self::next_update_nullifier(account_id, account, nsk),
|
||||
account_id,
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces a spent nullifier with the account's `next` one.
|
||||
pub fn update(&mut self, spent: &Nullifier, next: Nullifier, account_id: AccountId) {
|
||||
self.0.remove(spent);
|
||||
self.0.insert(next, account_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct UserKeyChain {
|
||||
@ -210,6 +248,19 @@ impl UserKeyChain {
|
||||
/// those.
|
||||
#[must_use]
|
||||
pub fn private_account(&self, account_id: AccountId) -> Option<FoundPrivateAccount<'_>> {
|
||||
self.private_accounts().find_map(|found| {
|
||||
let expected_id = AccountId::for_private_account(
|
||||
&found.key_chain.nullifier_public_key,
|
||||
&found.key_chain.viewing_public_key,
|
||||
found.kind,
|
||||
);
|
||||
(expected_id == account_id).then_some(found)
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterates every owned private account (imported and generated), one
|
||||
/// [`FoundPrivateAccount`] per identity. Excludes shared accounts.
|
||||
pub fn private_accounts(&self) -> impl Iterator<Item = FoundPrivateAccount<'_>> {
|
||||
self.imported_private_accounts
|
||||
.iter()
|
||||
.flat_map(|(key, data)| {
|
||||
@ -238,14 +289,6 @@ impl UserKeyChain {
|
||||
})
|
||||
}),
|
||||
)
|
||||
.find_map(|found| {
|
||||
let expected_id = AccountId::for_private_account(
|
||||
&found.key_chain.nullifier_public_key,
|
||||
&found.key_chain.viewing_public_key,
|
||||
found.kind,
|
||||
);
|
||||
(expected_id == account_id).then_some(found)
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -286,6 +329,124 @@ impl UserKeyChain {
|
||||
)
|
||||
}
|
||||
|
||||
/// Re-derives the [`PrivateKeyHolder`] for a shared account `entry`, dispatching on PDA vs
|
||||
/// regular. `None` if the group key holder is absent or a PDA entry lacks its program id.
|
||||
#[must_use]
|
||||
pub fn derive_shared_account_keys(
|
||||
&self,
|
||||
entry: &SharedAccountEntry,
|
||||
) -> Option<PrivateKeyHolder> {
|
||||
let holder = self.group_key_holder(&entry.group_label)?;
|
||||
Some(match (&entry.pda_seed, &entry.authority_program_id) {
|
||||
(Some(pda_seed), Some(program_id)) => holder.derive_keys_for_pda(program_id, pda_seed),
|
||||
(Some(_), None) => return None,
|
||||
_ => holder.derive_regular_shared_account_keys_from_identifier(entry.identifier),
|
||||
})
|
||||
}
|
||||
|
||||
/// Maps each owned and shared account's current-state update nullifier to its `account_id`,
|
||||
/// so co-owner updates are found during sync by nullifier rather than view tag.
|
||||
#[must_use]
|
||||
pub fn build_latest_nullifier_index(&self) -> NullifierIndex {
|
||||
let mut index = NullifierIndex::default();
|
||||
|
||||
// For each (regular) found account the user owns, compute its nullifier and put
|
||||
// into the map. This is the next nullifier it will look for.
|
||||
for found in self.private_accounts() {
|
||||
let account_id = AccountId::for_private_account(
|
||||
&found.key_chain.nullifier_public_key,
|
||||
&found.key_chain.viewing_public_key,
|
||||
found.kind,
|
||||
);
|
||||
let nsk = found.key_chain.private_key_holder.nullifier_secret_key;
|
||||
index.track(account_id, found.account, &nsk);
|
||||
}
|
||||
|
||||
// Same for the shared accounts.
|
||||
for (&account_id, entry) in self.shared_private_accounts_iter() {
|
||||
let Some(keys) = self.derive_shared_account_keys(entry) else {
|
||||
continue;
|
||||
};
|
||||
let nsk = keys.nullifier_secret_key;
|
||||
index.track(account_id, &entry.account, &nsk);
|
||||
}
|
||||
|
||||
index
|
||||
}
|
||||
|
||||
/// Applies every watched nullifier the `message` publishes: decrypts the position-aligned
|
||||
/// note, stores the new state, and rolls the index to the account's next nullifier. Returns
|
||||
/// the output slots handled, so the view-tag pass can skip them.
|
||||
pub fn sync_updates_via_nullifiers(
|
||||
&mut self,
|
||||
message: &Message,
|
||||
index: &mut NullifierIndex,
|
||||
) -> HashSet<usize> {
|
||||
let mut handled = HashSet::new();
|
||||
for (i, (old_nullifier, _)) in message.new_nullifiers.iter().enumerate() {
|
||||
// Get the nullifier information if awaiting the nullifier.
|
||||
let Some(account_id) = index.account_for(old_nullifier) else {
|
||||
continue;
|
||||
};
|
||||
// Try decrypting the commitment connectted to the nullifier and get the next
|
||||
// nullifier to await.
|
||||
if let Some(new_nullifier) = self.apply_nullifier_update(account_id, message, i) {
|
||||
// Update the index to await for the new state of the account, i.e.
|
||||
// the new nullifier.
|
||||
index.update(old_nullifier, new_nullifier, account_id);
|
||||
// Record that this nullifier's position can be skipped for scanning.
|
||||
handled.insert(i);
|
||||
}
|
||||
}
|
||||
handled
|
||||
}
|
||||
|
||||
/// Decrypts the note at slot `i` for `account_id`, stores the new state, and returns the
|
||||
/// account's next update nullifier. `None` if keys or decryption fail.
|
||||
fn apply_nullifier_update(
|
||||
&mut self,
|
||||
account_id: AccountId,
|
||||
message: &Message,
|
||||
i: usize,
|
||||
) -> Option<Nullifier> {
|
||||
let encrypted = &message.encrypted_private_post_states[i];
|
||||
let commitment = &message.new_commitments[i];
|
||||
let ciph_id = u32::try_from(i).ok()?;
|
||||
|
||||
let (nsk, secret, is_shared) = if let Some(entry) = self.shared_private_account(account_id)
|
||||
{
|
||||
let keys = self.derive_shared_account_keys(entry)?;
|
||||
let secret = SharedSecretKey::decapsulate(
|
||||
&encrypted.epk,
|
||||
&keys.viewing_secret_key.d,
|
||||
&keys.viewing_secret_key.z,
|
||||
)?;
|
||||
(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,
|
||||
secret,
|
||||
false,
|
||||
)
|
||||
};
|
||||
|
||||
let (kind, new_account) =
|
||||
EncryptionScheme::decrypt(&encrypted.ciphertext, &secret, commitment, ciph_id)?;
|
||||
let new_nullifier = NullifierIndex::next_update_nullifier(account_id, &new_account, &nsk);
|
||||
|
||||
if is_shared {
|
||||
self.update_shared_private_account_state(&account_id, new_account);
|
||||
} else {
|
||||
self.insert_private_account(account_id, kind, new_account)
|
||||
.ok()?;
|
||||
}
|
||||
Some(new_nullifier)
|
||||
}
|
||||
|
||||
pub fn add_imported_public_account(&mut self, private_key: lee::PrivateKey) {
|
||||
let account_id = AccountId::from(&lee::PublicKey::new_from_private_key(&private_key));
|
||||
|
||||
@ -696,8 +857,166 @@ impl Default for UserKeyChain {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee_core::encryption::EncryptedAccountData;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn nullifier_sync_updates_sole_owned_account() {
|
||||
let mut kc = UserKeyChain::default();
|
||||
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
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,
|
||||
&key_chain.viewing_public_key,
|
||||
&PrivateAccountKind::Regular(identifier),
|
||||
);
|
||||
|
||||
let old_account = Account::default();
|
||||
kc.add_imported_private_account(key_chain.clone(), None, identifier, old_account.clone());
|
||||
|
||||
let old_nullifier =
|
||||
Nullifier::for_account_update(&Commitment::new(&account_id, &old_account), &nsk);
|
||||
let mut index = kc.build_latest_nullifier_index();
|
||||
assert_eq!(index.account_for(&old_nullifier), Some(account_id));
|
||||
|
||||
let new_account = Account {
|
||||
balance: 150,
|
||||
..Account::default()
|
||||
};
|
||||
let new_commitment = Commitment::new(&account_id, &new_account);
|
||||
let (sender_ss, epk) = SharedSecretKey::encapsulate(&key_chain.viewing_public_key);
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
&new_account,
|
||||
&PrivateAccountKind::Regular(identifier),
|
||||
&sender_ss,
|
||||
&new_commitment,
|
||||
0,
|
||||
);
|
||||
let note = EncryptedAccountData::new(
|
||||
ciphertext,
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
epk,
|
||||
);
|
||||
|
||||
let message = Message {
|
||||
encrypted_private_post_states: vec![note],
|
||||
new_commitments: vec![new_commitment],
|
||||
new_nullifiers: vec![(old_nullifier, [0; 32])],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let handled = kc.sync_updates_via_nullifiers(&message, &mut index);
|
||||
|
||||
assert_eq!(handled, HashSet::from([0]));
|
||||
assert_eq!(
|
||||
kc.private_account(account_id).unwrap().account,
|
||||
&new_account
|
||||
);
|
||||
let new_nullifier =
|
||||
Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk);
|
||||
assert_eq!(index.account_for(&new_nullifier), Some(account_id));
|
||||
assert!(index.account_for(&old_nullifier).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nullifier_sync_updates_shared_account() {
|
||||
let mut kc = UserKeyChain::default();
|
||||
|
||||
let label = Label::new("group");
|
||||
let holder = GroupKeyHolder::new();
|
||||
let identifier = 0;
|
||||
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 account_id = AccountId::from((&npk, &vpk, identifier));
|
||||
|
||||
kc.insert_group_key_holder(label.clone(), holder);
|
||||
let old_account = Account::default();
|
||||
kc.insert_shared_private_account(
|
||||
account_id,
|
||||
SharedAccountEntry {
|
||||
group_label: label,
|
||||
identifier,
|
||||
pda_seed: None,
|
||||
authority_program_id: None,
|
||||
account: old_account.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
let old_nullifier =
|
||||
Nullifier::for_account_update(&Commitment::new(&account_id, &old_account), &nsk);
|
||||
let mut index = kc.build_latest_nullifier_index();
|
||||
assert_eq!(index.account_for(&old_nullifier), Some(account_id));
|
||||
|
||||
let new_account = Account {
|
||||
balance: 250,
|
||||
..Account::default()
|
||||
};
|
||||
let new_commitment = Commitment::new(&account_id, &new_account);
|
||||
let (sender_ss, epk) = SharedSecretKey::encapsulate(&vpk);
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
&new_account,
|
||||
&PrivateAccountKind::Regular(identifier),
|
||||
&sender_ss,
|
||||
&new_commitment,
|
||||
0,
|
||||
);
|
||||
let note = EncryptedAccountData::new(ciphertext, &npk, &vpk, epk);
|
||||
let message = Message {
|
||||
encrypted_private_post_states: vec![note],
|
||||
new_commitments: vec![new_commitment],
|
||||
new_nullifiers: vec![(old_nullifier, [0; 32])],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let handled = kc.sync_updates_via_nullifiers(&message, &mut index);
|
||||
|
||||
assert_eq!(handled, HashSet::from([0]));
|
||||
assert_eq!(
|
||||
kc.shared_private_account(account_id).unwrap().account,
|
||||
new_account
|
||||
);
|
||||
let new_nullifier =
|
||||
Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk);
|
||||
assert_eq!(index.account_for(&new_nullifier), Some(account_id));
|
||||
assert!(index.account_for(&old_nullifier).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nullifier_sync_ignores_unindexed_nullifier() {
|
||||
let mut kc = UserKeyChain::default();
|
||||
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
let identifier = 0;
|
||||
let account_id = AccountId::for_private_account(
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
&PrivateAccountKind::Regular(identifier),
|
||||
);
|
||||
let account = Account::default();
|
||||
kc.add_imported_private_account(key_chain, None, identifier, account.clone());
|
||||
|
||||
let mut index = kc.build_latest_nullifier_index();
|
||||
let unindexed = Nullifier::for_account_update(
|
||||
&Commitment::new(&AccountId::new([9; 32]), &Account::default()),
|
||||
&[9; 32],
|
||||
);
|
||||
let message = Message {
|
||||
new_nullifiers: vec![(unindexed, [0; 32])],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let handled = kc.sync_updates_via_nullifiers(&message, &mut index);
|
||||
|
||||
assert!(handled.is_empty());
|
||||
assert_eq!(kc.private_account(account_id).unwrap().account, &account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_account() {
|
||||
let mut user_data = UserKeyChain::default();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user