From eea069c663bec9736e834033c4540e5a952159c9 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 11:37:52 +0000 Subject: [PATCH 01/22] feat(wallet): nullifier-watch for updated private accounts --- lez/wallet/src/lib.rs | 53 ++++++++-------- lez/wallet/src/storage/key_chain.rs | 96 ++++++++++++++++++----------- 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 80b42e17..60dcc228 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -7,7 +7,7 @@ reason = "Most of the shadows come from args parsing which is ok" )] -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; pub use account_manager::AccountIdentity; use anyhow::{Context as _, Result}; @@ -22,7 +22,8 @@ use lee::{ }, }; use lee_core::{ - Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData, + Commitment, MembershipProof, Nullifier, SharedSecretKey, account::Nonce, + program::InstructionData, }; use log::info; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; @@ -789,28 +790,7 @@ impl WalletCore { .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 vsk = keys.viewing_secret_key; @@ -855,6 +835,31 @@ impl WalletCore { } } + #[expect(dead_code, reason = "wired into the sync loop in the update-scan unit")] + fn build_nullifier_watch(&self) -> HashMap { + let key_chain = self.storage.key_chain(); + let mut watch = HashMap::new(); + + for found in key_chain.private_accounts() { + let account_id = + AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind); + let nsk = found.key_chain.private_key_holder.nullifier_secret_key; + let commitment = Commitment::new(&account_id, found.account); + watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + } + + for (&account_id, entry) in key_chain.shared_private_accounts_iter() { + let Some(keys) = key_chain.derive_shared_account_keys(entry) else { + continue; + }; + let nsk = keys.nullifier_secret_key; + let commitment = Commitment::new(&account_id, &entry.account); + watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + } + + watch + } + #[must_use] pub const fn config_path(&self) -> &PathBuf { &self.config_path diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index f73833eb..bf4d8ab2 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -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, ViewingSecretKey}, + secret_holders::{PrivateKeyHolder, SeedHolder, ViewingSecretKey}, }; use lee::{Account, AccountId}; use lee_core::{Identifier, PrivateAccountKind}; @@ -209,41 +209,11 @@ impl UserKeyChain { /// those. #[must_use] pub fn private_account(&self, account_id: AccountId) -> Option> { - 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) - }) + self.private_accounts().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] @@ -281,6 +251,60 @@ impl UserKeyChain { ) } + pub fn private_accounts(&self) -> impl Iterator> { + 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()), + }) + }), + ) + } + + #[must_use] + pub fn derive_shared_account_keys( + &self, + entry: &SharedAccountEntry, + ) -> Option { + 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, + _ => { + 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) + } + }) + } + 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)); From d09a4215d802a112d412822cb659f452f6316d0c Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 11:39:42 +0000 Subject: [PATCH 02/22] refactor(wallet): use shared-key derivation helper --- lez/wallet/src/lib.rs | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 60dcc228..bac34ad9 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -298,35 +298,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, }) } } From 6025cb53ad18a35021f93df80ff97bd25ec419e9 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 11:52:26 +0000 Subject: [PATCH 03/22] feat(wallet): detect private-account updates via nullifier scanning --- lez/wallet/src/lib.rs | 80 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index bac34ad9..fc80ee63 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -696,9 +696,13 @@ impl WalletCore { let mut blocks = std::pin::pin!(poller.poll_block_range(last_synced_block.saturating_add(1)..=block_id)); + let mut watch = self.build_nullifier_watch(); let bar = indicatif::ProgressBar::new(num_of_blocks); while let Some(block) = blocks.try_next().await? { for tx in block.body.transactions { + if let LeeTransaction::PrivacyPreserving(pp_tx) = &tx { + self.sync_updates_via_nullifiers(pp_tx, &mut watch); + } self.sync_private_accounts_with_tx(tx); } @@ -826,7 +830,6 @@ impl WalletCore { } } - #[expect(dead_code, reason = "wired into the sync loop in the update-scan unit")] fn build_nullifier_watch(&self) -> HashMap { let key_chain = self.storage.key_chain(); let mut watch = HashMap::new(); @@ -851,6 +854,81 @@ impl WalletCore { watch } + fn sync_updates_via_nullifiers( + &mut self, + tx: &PrivacyPreservingTransaction, + watch: &mut HashMap, + ) { + let hits: Vec<(usize, Nullifier, AccountId)> = tx + .message + .new_nullifiers + .iter() + .enumerate() + .filter_map(|(i, (nullifier, _))| watch.get(nullifier).map(|&id| (i, *nullifier, id))) + .collect(); + + for (i, old_nullifier, account_id) in hits { + if let Some(new_nullifier) = self.apply_nullifier_update(account_id, tx, i) { + watch.remove(&old_nullifier); + watch.insert(new_nullifier, account_id); + } + } + } + + fn apply_nullifier_update( + &mut self, + account_id: AccountId, + tx: &PrivacyPreservingTransaction, + i: usize, + ) -> Option { + let encrypted = &tx.message.encrypted_private_post_states[i]; + let commitment = &tx.message.new_commitments[i]; + let ciph_id = u32::try_from(i).ok()?; + + let key_chain = self.storage.key_chain(); + let (nsk, secret, is_shared) = + if let Some(entry) = key_chain.shared_private_account(account_id) { + let keys = key_chain.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 = key_chain.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) = lee_core::EncryptionScheme::decrypt( + &encrypted.ciphertext, + &secret, + commitment, + ciph_id, + )?; + let new_nullifier = + Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk); + + if is_shared { + self.storage + .key_chain_mut() + .update_shared_private_account_state(&account_id, new_account); + } else { + self.storage + .key_chain_mut() + .insert_private_account(account_id, kind, new_account) + .ok()?; + } + Some(new_nullifier) + } + #[must_use] pub const fn config_path(&self) -> &PathBuf { &self.config_path From a24d2ca4ec8fdcb615ebd88dda2b8a493d5a2d65 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 12:20:45 +0000 Subject: [PATCH 04/22] fix(wallet): make nullifier watch updatable during sync --- lez/wallet/src/lib.rs | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index fc80ee63..baffc706 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -703,7 +703,7 @@ impl WalletCore { if let LeeTransaction::PrivacyPreserving(pp_tx) = &tx { self.sync_updates_via_nullifiers(pp_tx, &mut watch); } - self.sync_private_accounts_with_tx(tx); + self.sync_private_accounts_with_tx(tx, &mut watch); } self.storage.set_last_synced_block(block.header.block_id); @@ -720,7 +720,11 @@ impl WalletCore { Ok(()) } - fn sync_private_accounts_with_tx(&mut self, tx: LeeTransaction) { + fn sync_private_accounts_with_tx( + &mut self, + tx: LeeTransaction, + watch: &mut HashMap, + ) { let LeeTransaction::PrivacyPreserving(tx) = tx else { return; }; @@ -773,13 +777,18 @@ impl WalletCore { .key_chain_mut() .insert_private_account(affected_account_id, kind, new_acc) .expect("Account Id should exist"); + self.refresh_watch_entry(watch, affected_account_id); } // Scan for updates to shared accounts (GMS-derived). - self.sync_shared_private_accounts_with_tx(&tx); + self.sync_shared_private_accounts_with_tx(&tx, watch); } - fn sync_shared_private_accounts_with_tx(&mut self, tx: &PrivacyPreservingTransaction) { + fn sync_shared_private_accounts_with_tx( + &mut self, + tx: &PrivacyPreservingTransaction, + watch: &mut HashMap, + ) { let shared_keys: Vec<_> = self .storage .key_chain() @@ -825,6 +834,7 @@ impl WalletCore { self.storage .key_chain_mut() .update_shared_private_account_state(&account_id, new_acc); + self.refresh_watch_entry(watch, account_id); } } } @@ -929,6 +939,29 @@ impl WalletCore { Some(new_nullifier) } + fn refresh_watch_entry( + &self, + watch: &mut HashMap, + account_id: AccountId, + ) { + let key_chain = self.storage.key_chain(); + let (account, nsk) = if let Some(entry) = key_chain.shared_private_account(account_id) { + let Some(keys) = key_chain.derive_shared_account_keys(entry) else { + return; + }; + (&entry.account, keys.nullifier_secret_key) + } else if let Some(found) = key_chain.private_account(account_id) { + ( + found.account, + found.key_chain.private_key_holder.nullifier_secret_key, + ) + } else { + return; + }; + let nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, account), &nsk); + watch.insert(nullifier, account_id); + } + #[must_use] pub const fn config_path(&self) -> &PathBuf { &self.config_path From 5fda1608a3c06eacf85f279918ce97e61e5b5bb8 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 12:28:23 +0000 Subject: [PATCH 05/22] feat(wallet): skip nullifier-handled slots in the view-tag pass --- lez/wallet/src/lib.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index baffc706..e635f062 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -7,7 +7,10 @@ reason = "Most of the shadows come from args parsing which is ok" )] -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; pub use account_manager::AccountIdentity; use anyhow::{Context as _, Result}; @@ -700,10 +703,12 @@ impl WalletCore { let bar = indicatif::ProgressBar::new(num_of_blocks); while let Some(block) = blocks.try_next().await? { for tx in block.body.transactions { - if let LeeTransaction::PrivacyPreserving(pp_tx) = &tx { - self.sync_updates_via_nullifiers(pp_tx, &mut watch); - } - self.sync_private_accounts_with_tx(tx, &mut watch); + let handled = if let LeeTransaction::PrivacyPreserving(pp_tx) = &tx { + self.sync_updates_via_nullifiers(pp_tx, &mut watch) + } else { + HashSet::new() + }; + self.sync_private_accounts_with_tx(tx, &mut watch, &handled); } self.storage.set_last_synced_block(block.header.block_id); @@ -724,6 +729,7 @@ impl WalletCore { &mut self, tx: LeeTransaction, watch: &mut HashMap, + handled: &HashSet, ) { let LeeTransaction::PrivacyPreserving(tx) = tx else { return; @@ -744,7 +750,9 @@ impl WalletCore { .encrypted_private_post_states .iter() .enumerate() - .filter(move |(_, encrypted_data)| encrypted_data.view_tag == view_tag) + .filter(move |(ciph_id, encrypted_data)| { + !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]; @@ -781,13 +789,14 @@ impl WalletCore { } // Scan for updates to shared accounts (GMS-derived). - self.sync_shared_private_accounts_with_tx(&tx, watch); + self.sync_shared_private_accounts_with_tx(&tx, watch, handled); } fn sync_shared_private_accounts_with_tx( &mut self, tx: &PrivacyPreservingTransaction, watch: &mut HashMap, + handled: &HashSet, ) { let shared_keys: Vec<_> = self .storage @@ -811,7 +820,7 @@ impl WalletCore { .iter() .enumerate() { - if encrypted_data.view_tag != view_tag { + if handled.contains(&ciph_id) || encrypted_data.view_tag != view_tag { continue; } @@ -868,7 +877,7 @@ impl WalletCore { &mut self, tx: &PrivacyPreservingTransaction, watch: &mut HashMap, - ) { + ) -> HashSet { let hits: Vec<(usize, Nullifier, AccountId)> = tx .message .new_nullifiers @@ -877,12 +886,15 @@ impl WalletCore { .filter_map(|(i, (nullifier, _))| watch.get(nullifier).map(|&id| (i, *nullifier, id))) .collect(); + let mut handled = HashSet::new(); for (i, old_nullifier, account_id) in hits { if let Some(new_nullifier) = self.apply_nullifier_update(account_id, tx, i) { watch.remove(&old_nullifier); watch.insert(new_nullifier, account_id); + handled.insert(i); } } + handled } fn apply_nullifier_update( From 6bb2d7ffc35f424cf30ade467a5cd130399b938a Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 13:34:50 +0000 Subject: [PATCH 06/22] refactor(wallet): move nullifier-watch sync onto UserKeyChain --- lez/wallet/src/lib.rs | 139 ++-------------------------- lez/wallet/src/storage/key_chain.rs | 122 +++++++++++++++++++++++- 2 files changed, 129 insertions(+), 132 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index e635f062..acea846d 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -699,12 +699,14 @@ impl WalletCore { let mut blocks = std::pin::pin!(poller.poll_block_range(last_synced_block.saturating_add(1)..=block_id)); - let mut watch = self.build_nullifier_watch(); + let mut watch = self.storage.key_chain().build_nullifier_watch(); let bar = indicatif::ProgressBar::new(num_of_blocks); while let Some(block) = blocks.try_next().await? { for tx in block.body.transactions { let handled = if let LeeTransaction::PrivacyPreserving(pp_tx) = &tx { - self.sync_updates_via_nullifiers(pp_tx, &mut watch) + self.storage + .key_chain_mut() + .sync_updates_via_nullifiers(&pp_tx.message, &mut watch) } else { HashSet::new() }; @@ -785,7 +787,9 @@ impl WalletCore { .key_chain_mut() .insert_private_account(affected_account_id, kind, new_acc) .expect("Account Id should exist"); - self.refresh_watch_entry(watch, affected_account_id); + self.storage + .key_chain() + .refresh_watch_entry(watch, affected_account_id); } // Scan for updates to shared accounts (GMS-derived). @@ -843,137 +847,14 @@ impl WalletCore { self.storage .key_chain_mut() .update_shared_private_account_state(&account_id, new_acc); - self.refresh_watch_entry(watch, account_id); + self.storage + .key_chain() + .refresh_watch_entry(watch, account_id); } } } } - fn build_nullifier_watch(&self) -> HashMap { - let key_chain = self.storage.key_chain(); - let mut watch = HashMap::new(); - - for found in key_chain.private_accounts() { - let account_id = - AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind); - let nsk = found.key_chain.private_key_holder.nullifier_secret_key; - let commitment = Commitment::new(&account_id, found.account); - watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); - } - - for (&account_id, entry) in key_chain.shared_private_accounts_iter() { - let Some(keys) = key_chain.derive_shared_account_keys(entry) else { - continue; - }; - let nsk = keys.nullifier_secret_key; - let commitment = Commitment::new(&account_id, &entry.account); - watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); - } - - watch - } - - fn sync_updates_via_nullifiers( - &mut self, - tx: &PrivacyPreservingTransaction, - watch: &mut HashMap, - ) -> HashSet { - let hits: Vec<(usize, Nullifier, AccountId)> = tx - .message - .new_nullifiers - .iter() - .enumerate() - .filter_map(|(i, (nullifier, _))| watch.get(nullifier).map(|&id| (i, *nullifier, id))) - .collect(); - - let mut handled = HashSet::new(); - for (i, old_nullifier, account_id) in hits { - if let Some(new_nullifier) = self.apply_nullifier_update(account_id, tx, i) { - watch.remove(&old_nullifier); - watch.insert(new_nullifier, account_id); - handled.insert(i); - } - } - handled - } - - fn apply_nullifier_update( - &mut self, - account_id: AccountId, - tx: &PrivacyPreservingTransaction, - i: usize, - ) -> Option { - let encrypted = &tx.message.encrypted_private_post_states[i]; - let commitment = &tx.message.new_commitments[i]; - let ciph_id = u32::try_from(i).ok()?; - - let key_chain = self.storage.key_chain(); - let (nsk, secret, is_shared) = - if let Some(entry) = key_chain.shared_private_account(account_id) { - let keys = key_chain.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 = key_chain.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) = lee_core::EncryptionScheme::decrypt( - &encrypted.ciphertext, - &secret, - commitment, - ciph_id, - )?; - let new_nullifier = - Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk); - - if is_shared { - self.storage - .key_chain_mut() - .update_shared_private_account_state(&account_id, new_account); - } else { - self.storage - .key_chain_mut() - .insert_private_account(account_id, kind, new_account) - .ok()?; - } - Some(new_nullifier) - } - - fn refresh_watch_entry( - &self, - watch: &mut HashMap, - account_id: AccountId, - ) { - let key_chain = self.storage.key_chain(); - let (account, nsk) = if let Some(entry) = key_chain.shared_private_account(account_id) { - let Some(keys) = key_chain.derive_shared_account_keys(entry) else { - return; - }; - (&entry.account, keys.nullifier_secret_key) - } else if let Some(found) = key_chain.private_account(account_id) { - ( - found.account, - found.key_chain.private_key_holder.nullifier_secret_key, - ) - } else { - return; - }; - let nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, account), &nsk); - watch.insert(nullifier, account_id); - } - #[must_use] pub const fn config_path(&self) -> &PathBuf { &self.config_path diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index bf4d8ab2..2694f574 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -1,5 +1,5 @@ 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::{ @@ -8,8 +8,10 @@ use key_protocol::key_management::{ key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex, traits::KeyTreeNode as _}, secret_holders::{PrivateKeyHolder, SeedHolder, ViewingSecretKey}, }; -use lee::{Account, AccountId}; -use lee_core::{Identifier, PrivateAccountKind}; +use lee::{Account, AccountId, privacy_preserving_transaction::message::Message}; +use lee_core::{ + Commitment, EncryptionScheme, Identifier, Nullifier, PrivateAccountKind, SharedSecretKey, +}; use log::{debug, warn}; use serde::{Deserialize, Serialize}; use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData}; @@ -305,6 +307,120 @@ impl UserKeyChain { }) } + #[must_use] + pub fn build_nullifier_watch(&self) -> HashMap { + let mut watch = HashMap::new(); + + for found in self.private_accounts() { + let account_id = + AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind); + let nsk = found.key_chain.private_key_holder.nullifier_secret_key; + let commitment = Commitment::new(&account_id, found.account); + watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + } + + 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; + let commitment = Commitment::new(&account_id, &entry.account); + watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + } + + watch + } + + pub fn sync_updates_via_nullifiers( + &mut self, + message: &Message, + watch: &mut HashMap, + ) -> HashSet { + let hits: Vec<(usize, Nullifier, AccountId)> = message + .new_nullifiers + .iter() + .enumerate() + .filter_map(|(i, (nullifier, _))| watch.get(nullifier).map(|&id| (i, *nullifier, id))) + .collect(); + + let mut handled = HashSet::new(); + for (i, old_nullifier, account_id) in hits { + if let Some(new_nullifier) = self.apply_nullifier_update(account_id, message, i) { + watch.remove(&old_nullifier); + watch.insert(new_nullifier, account_id); + handled.insert(i); + } + } + handled + } + + fn apply_nullifier_update( + &mut self, + account_id: AccountId, + message: &Message, + i: usize, + ) -> Option { + 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 = + Nullifier::for_account_update(&Commitment::new(&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 refresh_watch_entry( + &self, + watch: &mut HashMap, + account_id: AccountId, + ) { + let (account, nsk) = if let Some(entry) = self.shared_private_account(account_id) { + let Some(keys) = self.derive_shared_account_keys(entry) else { + return; + }; + (&entry.account, keys.nullifier_secret_key) + } else if let Some(found) = self.private_account(account_id) { + ( + found.account, + found.key_chain.private_key_holder.nullifier_secret_key, + ) + } else { + return; + }; + let nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, account), &nsk); + watch.insert(nullifier, account_id); + } + 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)); From 6283f5e3ee0fcde45575244d336baa91c249e1a1 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 14:23:16 +0000 Subject: [PATCH 07/22] refactor(state_machine): derive Default for ValidityWindow --- lee/state_machine/core/src/program.rs | 2 +- lee/state_machine/src/privacy_preserving_transaction/message.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lee/state_machine/core/src/program.rs b/lee/state_machine/core/src/program.rs index c5949dcf..f7791e85 100644 --- a/lee/state_machine/core/src/program.rs +++ b/lee/state_machine/core/src/program.rs @@ -317,7 +317,7 @@ impl AccountPostState { pub type BlockValidityWindow = ValidityWindow; pub type TimestampValidityWindow = ValidityWindow; -#[derive(Clone, Copy, Serialize, Deserialize)] +#[derive(Clone, Copy, Default, Serialize, Deserialize)] #[cfg_attr( any(feature = "host", test), derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize) diff --git a/lee/state_machine/src/privacy_preserving_transaction/message.rs b/lee/state_machine/src/privacy_preserving_transaction/message.rs index b2594912..9fafad9e 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/message.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/message.rs @@ -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, pub nonces: Vec, From d8e990a68ffa146d6ac0d35dd47696237b532949 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 14:23:16 +0000 Subject: [PATCH 08/22] test(wallet): cover nullifier-based update sync --- lez/wallet/src/storage/key_chain.rs | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 2694f574..1240b0ba 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -822,8 +822,99 @@ 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, + &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 watch = kc.build_nullifier_watch(); + assert_eq!(watch.get(&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 watch); + + 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!(watch.get(&new_nullifier), Some(&account_id)); + assert!(!watch.contains_key(&old_nullifier)); + } + + #[test] + fn nullifier_sync_ignores_unwatched_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, + &PrivateAccountKind::Regular(identifier), + ); + let account = Account::default(); + kc.add_imported_private_account(key_chain, None, identifier, account.clone()); + + let mut watch = kc.build_nullifier_watch(); + let unwatched = Nullifier::for_account_update( + &Commitment::new(&AccountId::new([9; 32]), &Account::default()), + &[9; 32], + ); + let message = Message { + new_nullifiers: vec![(unwatched, [0; 32])], + ..Default::default() + }; + + let handled = kc.sync_updates_via_nullifiers(&message, &mut watch); + + assert!(handled.is_empty()); + assert_eq!(kc.private_account(account_id).unwrap().account, &account); + } + #[test] fn new_account() { let mut user_data = UserKeyChain::default(); From f32e6bb178d7102b01996f0ade5841d1b273bd3f Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 14:41:48 +0000 Subject: [PATCH 09/22] refactor(key_protocol): use SharedAccountTag seed in GroupKeyHolder --- .../src/key_management/group_key_holder.rs | 15 ++++++++++++++- lez/wallet/src/lib.rs | 10 +--------- lez/wallet/src/storage/key_chain.rs | 12 +----------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/lee/key_protocol/src/key_management/group_key_holder.rs b/lee/key_protocol/src/key_management/group_key_holder.rs index 7fb24713..fbfd2550 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -1,6 +1,6 @@ use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; use lee_core::{ - SharedSecretKey, + Identifier, SharedSecretKey, encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{PdaSeed, ProgramId}, }; @@ -146,6 +146,19 @@ impl GroupKeyHolder { SecretSpendingKey(hasher.finalize_fixed().into()).produce_private_key_holder(None) } + #[must_use] + pub fn derive_keys_for_regular_shared_account( + &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 diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index acea846d..c4250afd 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -395,14 +395,6 @@ impl WalletCore { group_name: Label, ) -> Result { 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 @@ -410,7 +402,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_keys_for_regular_shared_account(identifier); let npk = keys.generate_nullifier_public_key(); let vpk = keys.generate_viewing_public_key(); let account_id = AccountId::from((&npk, identifier)); diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 1240b0ba..1730d855 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -293,17 +293,7 @@ impl UserKeyChain { 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, - _ => { - 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) - } + _ => holder.derive_keys_for_regular_shared_account(entry.identifier), }) } From 51cc82b50b9f473140c7d4e54fe52f745e4613a8 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 14:52:15 +0000 Subject: [PATCH 10/22] docs: document new methods --- .../src/key_management/group_key_holder.rs | 4 ++++ lez/wallet/src/storage/key_chain.rs | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lee/key_protocol/src/key_management/group_key_holder.rs b/lee/key_protocol/src/key_management/group_key_holder.rs index fbfd2550..db6d138d 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -146,6 +146,10 @@ 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`] so controllers sharing the GMS agree on keys. #[must_use] pub fn derive_keys_for_regular_shared_account( &self, diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 1730d855..1c3ab293 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -253,6 +253,8 @@ impl UserKeyChain { ) } + /// Iterates every owned private account (imported and generated), one + /// [`FoundPrivateAccount`] per identity. Excludes shared accounts. pub fn private_accounts(&self) -> impl Iterator> { self.imported_private_accounts .iter() @@ -284,6 +286,8 @@ 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, @@ -297,6 +301,8 @@ impl UserKeyChain { }) } + /// 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_nullifier_watch(&self) -> HashMap { let mut watch = HashMap::new(); @@ -321,6 +327,9 @@ impl UserKeyChain { watch } + /// Applies every watched nullifier the `message` publishes: decrypts the position-aligned + /// note, stores the new state, and rolls the watch 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, @@ -344,6 +353,8 @@ impl UserKeyChain { handled } + /// Decrypts the note at slot `i` for `account_id` (shared or sole-owned), 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, @@ -389,6 +400,8 @@ impl UserKeyChain { Some(new_nullifier) } + /// Recomputes `account_id`'s current update nullifier and inserts it into the watch -- used + /// after the view-tag pass stores a freshly discovered or updated account mid-sync. pub fn refresh_watch_entry( &self, watch: &mut HashMap, From ca8acb1c38da19f800d1569ce9c4923bce92dcf6 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 14:56:16 +0000 Subject: [PATCH 11/22] test(wallet): cover shared-account update sync --- lez/wallet/src/storage/key_chain.rs | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 1c3ab293..cbdba58b 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -889,6 +889,71 @@ mod tests { assert!(!watch.contains_key(&old_nullifier)); } + #[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_keys_for_regular_shared_account(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, 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 watch = kc.build_nullifier_watch(); + assert_eq!(watch.get(&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 watch); + + 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!(watch.get(&new_nullifier), Some(&account_id)); + assert!(!watch.contains_key(&old_nullifier)); + } + #[test] fn nullifier_sync_ignores_unwatched_nullifier() { let mut kc = UserKeyChain::default(); From cb6d21a937551a74bdc5a48b2310baaa760ae8bf Mon Sep 17 00:00:00 2001 From: agureev Date: Wed, 1 Jul 2026 20:58:03 +0400 Subject: [PATCH 12/22] refactor: docs, renaming --- .../src/key_management/group_key_holder.rs | 4 +- lez/wallet/src/lib.rs | 26 ++++--- lez/wallet/src/storage/key_chain.rs | 76 ++++++++++--------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/lee/key_protocol/src/key_management/group_key_holder.rs b/lee/key_protocol/src/key_management/group_key_holder.rs index db6d138d..b18960cf 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -149,9 +149,9 @@ impl GroupKeyHolder { /// 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`] so controllers sharing the GMS agree on keys. + /// to [`Self::derive_keys_for_shared_account`]. #[must_use] - pub fn derive_keys_for_regular_shared_account( + pub fn derive_regular_shared_account_keys_from_identifier( &self, identifier: Identifier, ) -> PrivateKeyHolder { diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index c4250afd..17ffa610 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -402,7 +402,7 @@ impl WalletCore { .group_key_holder(&group_name) .context(format!("Group '{group_name}' not found"))?; - let keys = holder.derive_keys_for_regular_shared_account(identifier); + 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, identifier)); @@ -691,18 +691,20 @@ impl WalletCore { let mut blocks = std::pin::pin!(poller.poll_block_range(last_synced_block.saturating_add(1)..=block_id)); - let mut watch = self.storage.key_chain().build_nullifier_watch(); + // 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 { + // Eagerly decrypt note updates using expected nullifiers. let handled = if let LeeTransaction::PrivacyPreserving(pp_tx) = &tx { self.storage .key_chain_mut() - .sync_updates_via_nullifiers(&pp_tx.message, &mut watch) + .sync_updates_via_nullifiers(&pp_tx.message, &mut index) } else { HashSet::new() }; - self.sync_private_accounts_with_tx(tx, &mut watch, &handled); + self.sync_private_accounts_with_tx(tx, &mut index, &handled); } self.storage.set_last_synced_block(block.header.block_id); @@ -722,7 +724,7 @@ impl WalletCore { fn sync_private_accounts_with_tx( &mut self, tx: LeeTransaction, - watch: &mut HashMap, + index: &mut HashMap, handled: &HashSet, ) { let LeeTransaction::PrivacyPreserving(tx) = tx else { @@ -745,6 +747,9 @@ impl WalletCore { .iter() .enumerate() .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)| { @@ -779,19 +784,21 @@ impl WalletCore { .key_chain_mut() .insert_private_account(affected_account_id, kind, new_acc) .expect("Account Id should exist"); + // Insert the initial nullifier awaited for the new ID. + // This allows to skip tag matching for noticing updates to it. self.storage .key_chain() - .refresh_watch_entry(watch, affected_account_id); + .refresh_next_nullifier_entry(index, affected_account_id); } // Scan for updates to shared accounts (GMS-derived). - self.sync_shared_private_accounts_with_tx(&tx, watch, handled); + self.sync_shared_private_accounts_with_tx(&tx, index, handled); } fn sync_shared_private_accounts_with_tx( &mut self, tx: &PrivacyPreservingTransaction, - watch: &mut HashMap, + index: &mut HashMap, handled: &HashSet, ) { let shared_keys: Vec<_> = self @@ -816,6 +823,7 @@ impl WalletCore { .iter() .enumerate() { + // If already decrypted or the tag does nto match, skip. if handled.contains(&ciph_id) || encrypted_data.view_tag != view_tag { continue; } @@ -841,7 +849,7 @@ impl WalletCore { .update_shared_private_account_state(&account_id, new_acc); self.storage .key_chain() - .refresh_watch_entry(watch, account_id); + .refresh_next_nullifier_entry(index, account_id); } } } diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index cbdba58b..74c209a9 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -297,64 +297,73 @@ impl UserKeyChain { 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_keys_for_regular_shared_account(entry.identifier), + _ => 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_nullifier_watch(&self) -> HashMap { - let mut watch = HashMap::new(); + pub fn build_latest_nullifier_index(&self) -> HashMap { + let mut index = HashMap::new(); + // 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.kind); let nsk = found.key_chain.private_key_holder.nullifier_secret_key; let commitment = Commitment::new(&account_id, found.account); - watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + index.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); } + // 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; let commitment = Commitment::new(&account_id, &entry.account); - watch.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + index.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); } - watch + index } /// Applies every watched nullifier the `message` publishes: decrypts the position-aligned - /// note, stores the new state, and rolls the watch to the account's next nullifier. Returns + /// 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, - watch: &mut HashMap, + index: &mut HashMap, ) -> HashSet { + // Get the nullifier information if awaiting the nullifier. let hits: Vec<(usize, Nullifier, AccountId)> = message .new_nullifiers .iter() .enumerate() - .filter_map(|(i, (nullifier, _))| watch.get(nullifier).map(|&id| (i, *nullifier, id))) + .filter_map(|(i, (nullifier, _))| index.get(nullifier).map(|&id| (i, *nullifier, id))) .collect(); let mut handled = HashSet::new(); for (i, old_nullifier, account_id) in hits { + // 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) { - watch.remove(&old_nullifier); - watch.insert(new_nullifier, account_id); + index.remove(&old_nullifier); + // Update the index to await for the new state of the account, i.e. + // the new nullifier. + index.insert(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` (shared or sole-owned), stores the new - /// state, and returns the account's next update nullifier. `None` if keys or decryption fail. + /// 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, @@ -400,11 +409,10 @@ impl UserKeyChain { Some(new_nullifier) } - /// Recomputes `account_id`'s current update nullifier and inserts it into the watch -- used - /// after the view-tag pass stores a freshly discovered or updated account mid-sync. - pub fn refresh_watch_entry( + /// Recomputes `account_id`'s current update nullifier and inserts it into the index. + pub fn refresh_next_nullifier_entry( &self, - watch: &mut HashMap, + index: &mut HashMap, account_id: AccountId, ) { let (account, nsk) = if let Some(entry) = self.shared_private_account(account_id) { @@ -421,7 +429,7 @@ impl UserKeyChain { return; }; let nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, account), &nsk); - watch.insert(nullifier, account_id); + index.insert(nullifier, account_id); } pub fn add_imported_public_account(&mut self, private_key: lee::PrivateKey) { @@ -846,8 +854,8 @@ mod tests { let old_nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, &old_account), &nsk); - let mut watch = kc.build_nullifier_watch(); - assert_eq!(watch.get(&old_nullifier), Some(&account_id)); + let mut index = kc.build_latest_nullifier_index(); + assert_eq!(index.get(&old_nullifier), Some(&account_id)); let new_account = Account { balance: 150, @@ -876,7 +884,7 @@ mod tests { ..Default::default() }; - let handled = kc.sync_updates_via_nullifiers(&message, &mut watch); + let handled = kc.sync_updates_via_nullifiers(&message, &mut index); assert_eq!(handled, HashSet::from([0])); assert_eq!( @@ -885,8 +893,8 @@ mod tests { ); let new_nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk); - assert_eq!(watch.get(&new_nullifier), Some(&account_id)); - assert!(!watch.contains_key(&old_nullifier)); + assert_eq!(index.get(&new_nullifier), Some(&account_id)); + assert!(!index.contains_key(&old_nullifier)); } #[test] @@ -896,7 +904,7 @@ mod tests { let label = Label::new("group"); let holder = GroupKeyHolder::new(); let identifier = 0; - let keys = holder.derive_keys_for_regular_shared_account(identifier); + 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; @@ -917,8 +925,8 @@ mod tests { let old_nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, &old_account), &nsk); - let mut watch = kc.build_nullifier_watch(); - assert_eq!(watch.get(&old_nullifier), Some(&account_id)); + let mut index = kc.build_latest_nullifier_index(); + assert_eq!(index.get(&old_nullifier), Some(&account_id)); let new_account = Account { balance: 250, @@ -941,7 +949,7 @@ mod tests { ..Default::default() }; - let handled = kc.sync_updates_via_nullifiers(&message, &mut watch); + let handled = kc.sync_updates_via_nullifiers(&message, &mut index); assert_eq!(handled, HashSet::from([0])); assert_eq!( @@ -950,12 +958,12 @@ mod tests { ); let new_nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk); - assert_eq!(watch.get(&new_nullifier), Some(&account_id)); - assert!(!watch.contains_key(&old_nullifier)); + assert_eq!(index.get(&new_nullifier), Some(&account_id)); + assert!(!index.contains_key(&old_nullifier)); } #[test] - fn nullifier_sync_ignores_unwatched_nullifier() { + fn nullifier_sync_ignores_unindexed_nullifier() { let mut kc = UserKeyChain::default(); let key_chain = KeyChain::new_os_random(); @@ -967,17 +975,17 @@ mod tests { let account = Account::default(); kc.add_imported_private_account(key_chain, None, identifier, account.clone()); - let mut watch = kc.build_nullifier_watch(); - let unwatched = Nullifier::for_account_update( + 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![(unwatched, [0; 32])], + new_nullifiers: vec![(unindexed, [0; 32])], ..Default::default() }; - let handled = kc.sync_updates_via_nullifiers(&message, &mut watch); + let handled = kc.sync_updates_via_nullifiers(&message, &mut index); assert!(handled.is_empty()); assert_eq!(kc.private_account(account_id).unwrap().account, &account); From 2a9c8574cd918d3fd5de695569b872d59e76dfbf Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 17:42:42 +0000 Subject: [PATCH 13/22] refactor(wallet): compute update nullifiers inlined --- lez/wallet/src/lib.rs | 30 +++++++++++++++++------------ lez/wallet/src/storage/key_chain.rs | 23 ---------------------- 2 files changed, 18 insertions(+), 35 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 34192112..412ca34c 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -803,14 +803,19 @@ impl WalletCore { .map(|(kind, res_acc)| { let npk = &key_chain.nullifier_public_key; let account_id = lee::AccountId::for_private_account(npk, &kind); - (account_id, kind, res_acc) + let nsk = key_chain.private_key_holder.nullifier_secret_key; + let nullifier = Nullifier::for_account_update( + &Commitment::new(&account_id, &res_acc), + &nsk, + ); + (account_id, kind, res_acc, nullifier) }) }) .collect::>() }) .collect::>(); - for (affected_account_id, kind, new_acc) in affected_accounts { + for (affected_account_id, kind, new_acc, nullifier) in affected_accounts { info!( "Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}" ); @@ -818,11 +823,9 @@ impl WalletCore { .key_chain_mut() .insert_private_account(affected_account_id, kind, new_acc) .expect("Account Id should exist"); - // Insert the initial nullifier awaited for the new ID. - // This allows to skip tag matching for noticing updates to it. - self.storage - .key_chain() - .refresh_next_nullifier_entry(index, affected_account_id); + // Await the account's next update by its nullifier, so later updates + // to it are caught without tag matching. + index.insert(nullifier, affected_account_id); } // Scan for updates to shared accounts (GMS-derived). @@ -843,12 +846,13 @@ impl WalletCore { let keys = self.storage.key_chain().derive_shared_account_keys(entry)?; let npk = keys.generate_nullifier_public_key(); let vpk = keys.generate_viewing_public_key(); + let nsk = keys.nullifier_secret_key; let 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 @@ -878,12 +882,14 @@ impl WalletCore { .expect("Ciphertext ID is expected to fit in u32"), ) { info!("Synced shared account {account_id:#?} with new state {new_acc:#?}"); + let nullifier = Nullifier::for_account_update( + &Commitment::new(&account_id, &new_acc), + &nsk, + ); self.storage .key_chain_mut() .update_shared_private_account_state(&account_id, new_acc); - self.storage - .key_chain() - .refresh_next_nullifier_entry(index, account_id); + index.insert(nullifier, account_id); } } } diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 74c209a9..60df6945 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -409,29 +409,6 @@ impl UserKeyChain { Some(new_nullifier) } - /// Recomputes `account_id`'s current update nullifier and inserts it into the index. - pub fn refresh_next_nullifier_entry( - &self, - index: &mut HashMap, - account_id: AccountId, - ) { - let (account, nsk) = if let Some(entry) = self.shared_private_account(account_id) { - let Some(keys) = self.derive_shared_account_keys(entry) else { - return; - }; - (&entry.account, keys.nullifier_secret_key) - } else if let Some(found) = self.private_account(account_id) { - ( - found.account, - found.key_chain.private_key_holder.nullifier_secret_key, - ) - } else { - return; - }; - let nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, account), &nsk); - index.insert(nullifier, account_id); - } - 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)); From 37b0108c43f664445076a5b33bd6f13706c3ed1b Mon Sep 17 00:00:00 2001 From: agureev Date: Wed, 1 Jul 2026 21:49:43 +0400 Subject: [PATCH 14/22] chore: update artifacts --- .../privacy_preserving_circuit.bin | Bin 597356 -> 597352 bytes artifacts/lez/programs/amm.bin | Bin 532892 -> 532944 bytes .../lez/programs/associated_token_account.bin | Bin 451260 -> 451264 bytes .../lez/programs/authenticated_transfer.bin | Bin 405220 -> 405220 bytes artifacts/lez/programs/bridge.bin | Bin 429004 -> 429008 bytes artifacts/lez/programs/clock.bin | Bin 403564 -> 403564 bytes artifacts/lez/programs/faucet.bin | Bin 421656 -> 421632 bytes artifacts/lez/programs/pinata.bin | Bin 393960 -> 393992 bytes artifacts/lez/programs/pinata_token.bin | Bin 399884 -> 399884 bytes artifacts/lez/programs/token.bin | Bin 483156 -> 483200 bytes artifacts/lez/programs/vault.bin | Bin 413504 -> 413508 bytes 11 files changed, 0 insertions(+), 0 deletions(-) diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin index 2b6bee3fcb00a53bcaefc9624c3bc8ddf49d5a1e..fa70e8ee682dd012f971ad677f4c86085a1c574e 100644 GIT binary patch delta 14344 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 Q(Z!0Ar0rT1J^!!&59u2y=>Px# delta 14355 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 Q*jb8_uJ3$4_PGcC2jH;M1& diff --git a/artifacts/lez/programs/amm.bin b/artifacts/lez/programs/amm.bin index 50a86ac32976a8c8eb37325600cc0b8e0e328e89..7e8d0bb82980c291e41c6ba36ffcff68105fa8a8 100644 GIT binary patch delta 12945 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 GpZGu54>3;w delta 12897 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 delta 9498 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 U>eTBdwV9(R9rd0CZC;)Kzx)`Q(*OVf diff --git a/artifacts/lez/programs/authenticated_transfer.bin b/artifacts/lez/programs/authenticated_transfer.bin index 3236f568c2429b72311df1513f866d8fd9a5974c..193d4ec284d0c1ddb88965f1470c5b884e44f0b3 100644 GIT binary patch delta 9209 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 delta 9220 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 diff --git a/artifacts/lez/programs/clock.bin b/artifacts/lez/programs/clock.bin index 52ed41690bd8d771887df5096168b4373b6b8a05..8c8c1b4f2ded9c09bb5a15ed672cf922c2473937 100644 GIT binary patch delta 8379 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 delta 9814 zcmai(dq7oH*1-3<=U%161hfQ2J?DZ7iVBE4lrJF>vCP!a0Urs8&q%3gN@cn}&~nmA zOS`BfRyJWnN@X1#^;0KntSp_z8Y@#QdstRhQ|V;&{m!}ja1P&S@!xN+z1CiPt+m(Q z=d$2AeG~UPa6Z`bCBiLh*P+C0qnYNPYo6 z<>K>jvWo*gMQASG2ahPwL=*ZRk0SD0E5bWe6S0z?hBY`L!~szhrii;;6JCN>!!wkS zfx(w4;zGVAYNP=JUOh|`+a-H1S43frb4!7f`T19E3eJ18QdND~E; zf4))?2J?M#{t4rH^Qy$R@rBd+pu6D88l5)3|5YgdRz9DuXeJMOn39>Y`C__#rtUCNogT1YIgT2IsjJ_z1q+#k2iXXt@)=x8MaXz9tTbU^{-h z;CLLYkq%bHvjI5A;zC7C>5f5m*j765AzTgHmDi&O8HT-5J`?uhkX>o7^;AR$A05XzvmnJ$`3n`*0xFb`Y9QiSY{^?x&mjNGTBxb35lrb?B;wINJb2}2q zDoun-^+7C?HeVCvk{82=VY|1~P|;pES<2fd83PBY$321TA~-1K zco7v*!@|d;gVU+#dY5AfiR`P+(O=w`0=}oYsflmkT3GfVk0=kWy;?n?oHphTR@?ak z)u~k6B283EM-RdYOEk+L^8|M6uZT<+-wYSPb|ALGg)Y7Tm%!Q5UVa)UVW}n>Bsapf z$VY@YApXO^rwr7~31tJQut%FqQalNlxcDP`rJefe6=xtFFOg42)v|^BO9K&$%3^4el7K9xClQL=jaFI2oP; zKaZJKQmql*3A2o4=4<$VWWc>Z&%rGhSP6Sw z3+#j43!H`B3yjZE#J?Y+H%f<=!-X}P$g~ooh*#m*HJTV@$peh$nzx#Bk7vRiFwi6o z%z~4G&LvV0A6#u4P{eV#9E0}!qr+ev-Y)fL6wu=ycPb`*I0+|n)~#sxu2WTCpcL=+ zZgQ&VF*x=KN9S&I&Je|TY4ZoTa-Fm0qHEdYde)Sid>+n5-@eIzfRDQDWnD)VI~)-6 z8Ax&l#BTV2i+_Q`AK~0le`Xm}k5I&@HGV72W)*)jQW5(%IgNMw4a{Lf<~cRx=iviz zq2xiM6fqt9B^I~$h@PVr5tbfj)sf=yihGQh(P}?Wpf-hu^aPzyUg;~ZCwh%ULUJ1x z-qnC(-jHKOV|YqD>0D{+;Ek|-UXQ`YT-^0Wj-!i9;0~L)Q9||uVjTlzD;)#x!)NOK zB2w-|<5oEa+Y3yAt8gd>xf!XG@GZ#Aj%G$8ek={hHGevsjswL~z8P+Vyu@PFBhCbe z7pi*#=|sG82XNk#zz2F?o6>u7yBLnj_?9Nc?@90CWjNM`KW~W59!2-7T@g2O; zVUKvkSTL5MDE=K z&e@y|r~Sp*^w+_x8S2)^tqkY6hEt963@d??= zhD%&_9)v}Mv)n)7Gw9n*`G(1~qSrNXPTJoJyK?9eUmJT#~+I|I+^(;q0%R{r>`w zg3II~np4hY;&Nw_@ybFq;7QqVD~a~$;-HLM)NGzrUcI^J-vmc*cj8cI%&k^W_yWD= zv0f`(ESGxE{ekUA=;DmzZ{dm^j^2QKC=l2@J7#?Ef*V}C13nz7iz8D1JY3?^AH@BZ z4abG_qr@K=Sipdtusy-ke^kF3J&S}k(tVSyc|Oqy?`p3%H=Fn2h`&01b(>3EFw

b8UH?WuoJB}e{ju%&%p&UAAg5)I_b^t+NcWh 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 diff --git a/artifacts/lez/programs/pinata.bin b/artifacts/lez/programs/pinata.bin index 809feb82b4e85171dc88a23ed02c31e1ef6e6ec4..47bc615811a6bd6854723be9d9754338b8702f60 100644 GIT binary patch delta 9909 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 Gv*>>sudz-5 delta 9833 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 G3;z!b?wAAs diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin index 39aa583faa8f37ed3021e8dc641c11f2d28903ca..41b061ef4770e57fbfe476dea3ed114ebcf627eb 100644 GIT binary patch delta 9053 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 E0p?T+Q2+n{ delta 9032 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 diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin index bfd11c8822d9b752c6489476987f65e59f38d3ad..1882cfb6593158a00c62c0cc0cfe9d9748f16135 100644 GIT binary patch delta 12914 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 B(|Z5_ diff --git a/artifacts/lez/programs/vault.bin b/artifacts/lez/programs/vault.bin index a08868e568b02c8b50c50c27ba7dfa5d2f0e54fa..7e053181832f97329ebbb4f9168e6cbb796113b5 100644 GIT binary patch delta 8658 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 Date: Wed, 1 Jul 2026 17:54:26 +0000 Subject: [PATCH 15/22] refactor(wallet): place private_accounts next to private_account --- lez/wallet/src/storage/key_chain.rs | 66 ++++++++++++++--------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 60df6945..50c6e68d 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -218,6 +218,39 @@ impl UserKeyChain { }) } + /// Iterates every owned private account (imported and generated), one + /// [`FoundPrivateAccount`] per identity. Excludes shared accounts. + pub fn private_accounts(&self) -> impl Iterator> { + 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()), + }) + }), + ) + } + #[must_use] pub fn private_account_key_chain_by_index( &self, @@ -253,39 +286,6 @@ impl UserKeyChain { ) } - /// Iterates every owned private account (imported and generated), one - /// [`FoundPrivateAccount`] per identity. Excludes shared accounts. - pub fn private_accounts(&self) -> impl Iterator> { - 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()), - }) - }), - ) - } - /// 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] From d2633af413fb2ad18c11996a59a36fa8f7044c5d Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 1 Jul 2026 18:23:19 +0000 Subject: [PATCH 16/22] refactor(wallet): introduce a NullifierIndex struct --- lez/wallet/src/lib.rs | 33 +++++-------- lez/wallet/src/storage/key_chain.rs | 72 +++++++++++++++++++++-------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 412ca34c..3fe103c3 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -7,10 +7,7 @@ reason = "Most of the shadows come from args parsing which is ok" )] -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, -}; +use std::{collections::HashSet, path::PathBuf}; pub use account_manager::AccountIdentity; use anyhow::{Context as _, Result}; @@ -25,7 +22,7 @@ use lee::{ }, }; use lee_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, SharedSecretKey, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, SharedSecretKey, account::Nonce, compute_digest_for_path, program::InstructionData, }; use log::info; @@ -37,7 +34,7 @@ use crate::{ account::{AccountIdWithPrivacy, Label}, config::WalletConfigOverrides, poller::TxPoller, - storage::key_chain::SharedAccountEntry, + storage::key_chain::{NullifierIndex, SharedAccountEntry}, }; pub mod account; @@ -758,7 +755,7 @@ impl WalletCore { fn sync_private_accounts_with_tx( &mut self, tx: LeeTransaction, - index: &mut HashMap, + index: &mut NullifierIndex, handled: &HashSet, ) { let LeeTransaction::PrivacyPreserving(tx) = tx else { @@ -804,28 +801,24 @@ impl WalletCore { let npk = &key_chain.nullifier_public_key; let account_id = lee::AccountId::for_private_account(npk, &kind); let nsk = key_chain.private_key_holder.nullifier_secret_key; - let nullifier = Nullifier::for_account_update( - &Commitment::new(&account_id, &res_acc), - &nsk, - ); - (account_id, kind, res_acc, nullifier) + (account_id, kind, res_acc, nsk) }) }) .collect::>() }) .collect::>(); - for (affected_account_id, kind, new_acc, nullifier) 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) .expect("Account Id should exist"); - // Await the account's next update by its nullifier, so later updates - // to it are caught without tag matching. - index.insert(nullifier, affected_account_id); } // Scan for updates to shared accounts (GMS-derived). @@ -835,7 +828,7 @@ impl WalletCore { fn sync_shared_private_accounts_with_tx( &mut self, tx: &PrivacyPreservingTransaction, - index: &mut HashMap, + index: &mut NullifierIndex, handled: &HashSet, ) { let shared_keys: Vec<_> = self @@ -882,14 +875,10 @@ impl WalletCore { .expect("Ciphertext ID is expected to fit in u32"), ) { info!("Synced shared account {account_id:#?} with new state {new_acc:#?}"); - let nullifier = Nullifier::for_account_update( - &Commitment::new(&account_id, &new_acc), - &nsk, - ); + index.track(account_id, &new_acc, &nsk); self.storage .key_chain_mut() .update_shared_private_account_state(&account_id, new_acc); - index.insert(nullifier, account_id); } } } diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 50c6e68d..948b9d1d 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -10,7 +10,8 @@ use key_protocol::key_management::{ }; use lee::{Account, AccountId, privacy_preserving_transaction::message::Message}; use lee_core::{ - Commitment, EncryptionScheme, Identifier, Nullifier, PrivateAccountKind, SharedSecretKey, + Commitment, EncryptionScheme, Identifier, Nullifier, NullifierSecretKey, PrivateAccountKind, + SharedSecretKey, }; use log::{debug, warn}; use serde::{Deserialize, Serialize}; @@ -61,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); + +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 { + 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 { @@ -304,8 +340,8 @@ impl UserKeyChain { /// 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) -> HashMap { - let mut index = HashMap::new(); + 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. @@ -313,8 +349,7 @@ impl UserKeyChain { let account_id = AccountId::for_private_account(&found.key_chain.nullifier_public_key, found.kind); let nsk = found.key_chain.private_key_holder.nullifier_secret_key; - let commitment = Commitment::new(&account_id, found.account); - index.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + index.track(account_id, found.account, &nsk); } // Same for the shared accounts. @@ -323,8 +358,7 @@ impl UserKeyChain { continue; }; let nsk = keys.nullifier_secret_key; - let commitment = Commitment::new(&account_id, &entry.account); - index.insert(Nullifier::for_account_update(&commitment, &nsk), account_id); + index.track(account_id, &entry.account, &nsk); } index @@ -336,14 +370,16 @@ impl UserKeyChain { pub fn sync_updates_via_nullifiers( &mut self, message: &Message, - index: &mut HashMap, + index: &mut NullifierIndex, ) -> HashSet { // Get the nullifier information if awaiting the nullifier. let hits: Vec<(usize, Nullifier, AccountId)> = message .new_nullifiers .iter() .enumerate() - .filter_map(|(i, (nullifier, _))| index.get(nullifier).map(|&id| (i, *nullifier, id))) + .filter_map(|(i, (nullifier, _))| { + index.account_for(nullifier).map(|id| (i, *nullifier, id)) + }) .collect(); let mut handled = HashSet::new(); @@ -351,10 +387,9 @@ impl UserKeyChain { // 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) { - index.remove(&old_nullifier); // Update the index to await for the new state of the account, i.e. // the new nullifier. - index.insert(new_nullifier, account_id); + index.update(&old_nullifier, new_nullifier, account_id); // Record that this nullifier's position can be skipped for scanning. handled.insert(i); } @@ -397,8 +432,7 @@ impl UserKeyChain { let (kind, new_account) = EncryptionScheme::decrypt(&encrypted.ciphertext, &secret, commitment, ciph_id)?; - let new_nullifier = - Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk); + 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); @@ -832,7 +866,7 @@ mod tests { 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.get(&old_nullifier), Some(&account_id)); + assert_eq!(index.account_for(&old_nullifier), Some(account_id)); let new_account = Account { balance: 150, @@ -870,8 +904,8 @@ mod tests { ); let new_nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk); - assert_eq!(index.get(&new_nullifier), Some(&account_id)); - assert!(!index.contains_key(&old_nullifier)); + assert_eq!(index.account_for(&new_nullifier), Some(account_id)); + assert!(index.account_for(&old_nullifier).is_none()); } #[test] @@ -903,7 +937,7 @@ mod tests { 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.get(&old_nullifier), Some(&account_id)); + assert_eq!(index.account_for(&old_nullifier), Some(account_id)); let new_account = Account { balance: 250, @@ -935,8 +969,8 @@ mod tests { ); let new_nullifier = Nullifier::for_account_update(&Commitment::new(&account_id, &new_account), &nsk); - assert_eq!(index.get(&new_nullifier), Some(&account_id)); - assert!(!index.contains_key(&old_nullifier)); + assert_eq!(index.account_for(&new_nullifier), Some(account_id)); + assert!(index.account_for(&old_nullifier).is_none()); } #[test] From b6e148249eddba5bcea6bca80874496a36e8d41f Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Thu, 2 Jul 2026 10:08:37 +0000 Subject: [PATCH 17/22] refactor(wallet): inline the hit loop in sync_updates_via_nullifiers --- lez/wallet/src/storage/key_chain.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 948b9d1d..3ceefeee 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -372,24 +372,18 @@ impl UserKeyChain { message: &Message, index: &mut NullifierIndex, ) -> HashSet { - // Get the nullifier information if awaiting the nullifier. - let hits: Vec<(usize, Nullifier, AccountId)> = message - .new_nullifiers - .iter() - .enumerate() - .filter_map(|(i, (nullifier, _))| { - index.account_for(nullifier).map(|id| (i, *nullifier, id)) - }) - .collect(); - let mut handled = HashSet::new(); - for (i, old_nullifier, account_id) in hits { + 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); + index.update(old_nullifier, new_nullifier, account_id); // Record that this nullifier's position can be skipped for scanning. handled.insert(i); } From 6c1dd53aa5f3c9e1456696e67b4ffc69ce70ab82 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Thu, 2 Jul 2026 10:08:37 +0000 Subject: [PATCH 18/22] refactor(wallet): thread &Message through the view-tag sync passes --- lez/wallet/src/lib.rs | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 3fe103c3..b1a71995 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -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::{ @@ -727,15 +728,15 @@ impl WalletCore { let bar = indicatif::ProgressBar::new(num_of_blocks); while let Some(block) = blocks.try_next().await? { for tx in block.body.transactions { - // Eagerly decrypt note updates using expected nullifiers. - let handled = if let LeeTransaction::PrivacyPreserving(pp_tx) = &tx { - self.storage - .key_chain_mut() - .sync_updates_via_nullifiers(&pp_tx.message, &mut index) - } else { - HashSet::new() + let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else { + continue; }; - self.sync_private_accounts_with_tx(tx, &mut index, &handled); + // 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); @@ -754,14 +755,10 @@ impl WalletCore { fn sync_private_accounts_with_tx( &mut self, - tx: LeeTransaction, + message: &Message, index: &mut NullifierIndex, handled: &HashSet, ) { - let LeeTransaction::PrivacyPreserving(tx) = tx else { - return; - }; - let affected_accounts = self .storage .key_chain() @@ -771,9 +768,9 @@ 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() @@ -822,12 +819,12 @@ impl WalletCore { } // Scan for updates to shared accounts (GMS-derived). - self.sync_shared_private_accounts_with_tx(&tx, index, handled); + self.sync_shared_private_accounts_with_tx(message, index, handled); } fn sync_shared_private_accounts_with_tx( &mut self, - tx: &PrivacyPreservingTransaction, + message: &Message, index: &mut NullifierIndex, handled: &HashSet, ) { @@ -848,11 +845,8 @@ impl WalletCore { 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 already decrypted or the tag does nto match, skip. if handled.contains(&ciph_id) || encrypted_data.view_tag != view_tag { @@ -864,7 +858,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, From 1bd396006bb2070bc7d494895fb84c36908eac7c Mon Sep 17 00:00:00 2001 From: agureev Date: Wed, 8 Jul 2026 16:36:05 +0400 Subject: [PATCH 19/22] docs: fix typos --- lez/wallet/src/lib.rs | 2 +- lez/wallet/src/storage/key_chain.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index b1a71995..1e4ba380 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -848,7 +848,7 @@ impl WalletCore { for (ciph_id, encrypted_data) in message.encrypted_private_post_states.iter().enumerate() { - // If already decrypted or the tag does nto match, skip. + // If already decrypted or the tag does not match, skip. if handled.contains(&ciph_id) || encrypted_data.view_tag != view_tag { continue; } diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index 3ceefeee..51b3762c 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -378,7 +378,7 @@ impl UserKeyChain { let Some(account_id) = index.account_for(old_nullifier) else { continue; }; - // Try decrypting the commitment connectted to the nullifier and get the next + // Try decrypting the commitment connected 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. From b31aecd2c064ed8544fb1195033dec5cc196942c Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Wed, 8 Jul 2026 14:33:59 +0000 Subject: [PATCH 20/22] feat(wallet): reject length-mismatched privacy transactions --- .../privacy_preserving_transaction/message.rs | 28 +++++++++++++++++++ lez/wallet/src/lib.rs | 7 +++++ 2 files changed, 35 insertions(+) diff --git a/lee/state_machine/src/privacy_preserving_transaction/message.rs b/lee/state_machine/src/privacy_preserving_transaction/message.rs index 9fafad9e..1315cf4a 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/message.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/message.rs @@ -84,6 +84,20 @@ impl Message { Sha256::digest(bytes).into() } + + /// Ensure that the commitments, nullifiers, and ciphertexts agree. + pub fn validate_note_lengths(&self) -> Result { + let count = self.new_nullifiers.len(); + if self.new_commitments.len() != count || self.encrypted_private_post_states.len() != count + { + return Err(LeeError::InvalidInput(format!( + "Note vectors disagree in length with {count} nullifiers, {} commitments, and {} ciphertexts", + self.new_commitments.len(), + self.encrypted_private_post_states.len(), + ))); + } + Ok(count) + } } #[cfg(test)] @@ -140,6 +154,20 @@ pub mod tests { } } + #[test] + fn validate_note_lengths_accepts_matching_and_rejects_mismatched() { + assert_eq!(Message::default().validate_note_lengths().unwrap(), 0); + + let mismatched = Message { + new_commitments: vec![Commitment::new( + &AccountId::new([0; 32]), + &Account::default(), + )], + ..Default::default() + }; + assert!(mismatched.validate_note_lengths().is_err()); + } + #[test] fn hash_privacy_pinned() { let msg = Message { diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 1e4ba380..3f7a900d 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -506,6 +506,12 @@ impl WalletCore { tx: &lee::privacy_preserving_transaction::PrivacyPreservingTransaction, acc_decode_mask: &[AccDecodeData], ) -> Result<()> { + let note_count = tx.message.validate_note_lengths()?; + anyhow::ensure!( + note_count == acc_decode_mask.len(), + "Decode mask has {} entries but the transaction has {note_count} notes", + acc_decode_mask.len(), + ); for (output_index, acc_decode_data) in acc_decode_mask.iter().enumerate() { match acc_decode_data { AccDecodeData::Decode(secret, acc_account_id) => { @@ -731,6 +737,7 @@ impl WalletCore { let LeeTransaction::PrivacyPreserving(pp_tx) = &tx else { continue; }; + pp_tx.message.validate_note_lengths()?; // Eagerly decrypt note updates using expected nullifiers. let handled = self .storage From 24faedd6862f20f1981c1a6288a6f6fce170a6e1 Mon Sep 17 00:00:00 2001 From: agureev Date: Thu, 9 Jul 2026 00:08:41 +0400 Subject: [PATCH 21/22] feat: relax the mask length requirement --- lez/wallet/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 0b49e726..661304e7 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -508,7 +508,7 @@ impl WalletCore { ) -> Result<()> { let note_count = tx.message.validate_note_lengths()?; anyhow::ensure!( - note_count == acc_decode_mask.len(), + note_count >= acc_decode_mask.len(), "Decode mask has {} entries but the transaction has {note_count} notes", acc_decode_mask.len(), ); From a3593e36b7a921462228d2be36eb4febea12b619 Mon Sep 17 00:00:00 2001 From: agureev Date: Thu, 9 Jul 2026 14:15:39 +0400 Subject: [PATCH 22/22] refactor: change sync function name --- lez/wallet/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 661304e7..4b4ba149 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -743,7 +743,7 @@ impl WalletCore { .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.sync_private_accounts_with_message(&pp_tx.message, &mut index, &handled); } self.storage.set_last_synced_block(block.header.block_id); @@ -760,7 +760,7 @@ impl WalletCore { Ok(()) } - fn sync_private_accounts_with_tx( + fn sync_private_accounts_with_message( &mut self, message: &Message, index: &mut NullifierIndex,