perf(lite-backend): encode note addresses from pk_d instead of deriving
note_address() re-derived a note's payment address from its viewing key and diversifier (a group hash, an ivk hash and a 252-bit scalar multiplication) for every note in `notes` and `list` and for each new note in the block scan. The z-balance sums did the same derivation in their address filter, so `balance`, which sums four balances for each of Z z-addresses, cost Z x (N + 2U) derivations for N notes, U of them unspent. do_list_notes also encoded up to Z + Z^2 addresses to find the spendable ones and cloned that set once per transaction. Now: - note_address() encodes the note's stored pk_d with its diversifier. Both decryption and wallet load build that pk_d from the same key and diversifier, so the string is byte-identical; - the three z-balance sums share one address filter. The per-address sums behind `balance` compare (diversifier, pk_d) with each z-address directly; any other address string keeps the old exact string compare; - those sums work from a copy of the z-addresses whose lock is released before any note is read, so the txs-then-zkeys lock order that sync uses is unchanged; - do_list_notes builds the spendable-address set with Z encodes and borrows it. No output changes: balances and the notes/list/balance JSON are byte-identical. The address filter no longer contains an unwrap (it could not fail). Tests compare against verbatim copies of the old functions over randomized chains (note addresses, also after a reload; every filtered balance for own, diversified, foreign and junk address strings; the per-address balances; the spendable set), and a concurrency test fails with a deadlock if the zkeys lock is held across the sums. A differential run against the unmodified backend compared per-address balances and the JSON after every operation, through reorgs and reloads, and the old build re-saves the new files byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+100
-45
@@ -105,6 +105,33 @@ impl ToBase58Check for [u8] {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which notes a z-balance sum counts. `Addr` compares (diversifier, pk_d) directly. Because
|
||||
/// encode_payment_address is injective on (diversifier, pk_d), it selects exactly the notes whose
|
||||
/// old derived string == encode(pa). `Str` keeps the old exact string compare (on the cheap
|
||||
/// note_address) for arbitrary caller strings, so no decoding and no new panic paths.
|
||||
enum ZAddrFilter {
|
||||
All,
|
||||
Addr(PaymentAddress<Bls12>),
|
||||
Str(String),
|
||||
}
|
||||
|
||||
impl ZAddrFilter {
|
||||
fn from_opt(addr: &Option<String>) -> Self {
|
||||
match addr {
|
||||
Some(a) => ZAddrFilter::Str(a.clone()),
|
||||
None => ZAddrFilter::All,
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(&self, hrp: &str, nd: &SaplingNoteData) -> bool {
|
||||
match self {
|
||||
ZAddrFilter::All => true,
|
||||
ZAddrFilter::Addr(pa) => nd.diversifier == pa.diversifier && nd.note.pk_d == pa.pk_d,
|
||||
ZAddrFilter::Str(a) => LightWallet::note_address(hrp, nd).as_ref() == Some(a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LightWallet {
|
||||
// Is the wallet encrypted? If it is, then when writing to disk, the seed is always encrypted
|
||||
// and the individual spending keys are not written
|
||||
@@ -490,10 +517,12 @@ impl LightWallet {
|
||||
}
|
||||
|
||||
pub fn note_address(hrp: &str, note: &SaplingNoteData) -> Option<String> {
|
||||
match note.extfvk.fvk.vk.into_payment_address(note.diversifier, &JUBJUB) {
|
||||
Some(pa) => Some(encode_payment_address(hrp, &pa)),
|
||||
None => None
|
||||
}
|
||||
// note.pk_d == g_d(diversifier) * ivk(note.extfvk) by construction: decryption
|
||||
// (parse_note_plaintext_without_memo) and SaplingNoteData::read (data.rs:158) both build
|
||||
// it that way. So this encodes exactly what into_payment_address() would, without the
|
||||
// group hash, ivk hash and 252-bit scalar multiplication.
|
||||
let pa = PaymentAddress { pk_d: note.note.pk_d.clone(), diversifier: note.diversifier };
|
||||
Some(encode_payment_address(hrp, &pa))
|
||||
}
|
||||
|
||||
pub fn get_birthday(&self) -> u64 {
|
||||
@@ -1065,33 +1094,26 @@ impl LightWallet {
|
||||
|
||||
pub fn zbalance(&self, addr: Option<String>) -> u64 {
|
||||
let unconfirmed_balance = self.unconfirmed_zbalance(addr.clone());
|
||||
let confirmed_balance = self.confirmed_zbalance_f(&ZAddrFilter::from_opt(&addr));
|
||||
|
||||
let confirmed_balance = self.txs.read().unwrap()
|
||||
confirmed_balance + unconfirmed_balance
|
||||
}
|
||||
|
||||
// Unspent (and not pending-spent) note value matching `f`. The spent checks run first so spent
|
||||
// notes never reach the address filter; both are pure, so the sum equals the old
|
||||
// filter(addr).map(if unspent {v} else {0}).sum().
|
||||
fn confirmed_zbalance_f(&self, f: &ZAddrFilter) -> u64 {
|
||||
let hrp = self.config.hrp_sapling_address();
|
||||
self.txs.read().unwrap()
|
||||
.values()
|
||||
.map(|tx| {
|
||||
tx.notes.iter()
|
||||
.filter(|nd| {
|
||||
match addr.as_ref() {
|
||||
Some(a) => *a == encode_payment_address(
|
||||
self.config.hrp_sapling_address(),
|
||||
&nd.extfvk.fvk.vk
|
||||
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
|
||||
),
|
||||
None => true
|
||||
}
|
||||
})
|
||||
.map(|nd| {
|
||||
if nd.spent.is_none() && nd.unconfirmed_spent.is_none() {
|
||||
nd.note.value
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none())
|
||||
.filter(|nd| f.matches(hrp, nd))
|
||||
.map(|nd| nd.note.value)
|
||||
.sum::<u64>()
|
||||
})
|
||||
.sum::<u64>();
|
||||
|
||||
confirmed_balance + unconfirmed_balance
|
||||
.sum::<u64>()
|
||||
}
|
||||
|
||||
// Get all (unspent) utxos. Unconfirmed spent utxos are included
|
||||
@@ -1119,10 +1141,15 @@ impl LightWallet {
|
||||
}
|
||||
|
||||
pub fn verified_zbalance(&self, addr: Option<String>) -> u64 {
|
||||
self.verified_zbalance_f(&ZAddrFilter::from_opt(&addr))
|
||||
}
|
||||
|
||||
fn verified_zbalance_f(&self, f: &ZAddrFilter) -> u64 {
|
||||
let anchor_height = match self.get_target_height_and_anchor_offset() {
|
||||
Some((height, anchor_offset)) => height - anchor_offset as u32 ,
|
||||
None => return 0,
|
||||
};
|
||||
let hrp = self.config.hrp_sapling_address();
|
||||
|
||||
self.txs
|
||||
.read()
|
||||
@@ -1133,16 +1160,7 @@ impl LightWallet {
|
||||
tx.notes
|
||||
.iter()
|
||||
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none())
|
||||
.filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it.
|
||||
match addr.as_ref() {
|
||||
Some(a) => *a == encode_payment_address(
|
||||
self.config.hrp_sapling_address(),
|
||||
&nd.extfvk.fvk.vk
|
||||
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
|
||||
),
|
||||
None => true
|
||||
}
|
||||
})
|
||||
.filter(|nd| f.matches(hrp, nd))
|
||||
.map(|nd| nd.note.value)
|
||||
.sum::<u64>()
|
||||
} else {
|
||||
@@ -1153,7 +1171,14 @@ impl LightWallet {
|
||||
}
|
||||
|
||||
pub fn spendable_zbalance(&self, addr: Option<String>) -> u64 {
|
||||
self.spendable_zbalance_f(&ZAddrFilter::from_opt(&addr))
|
||||
}
|
||||
|
||||
// Lock order: txs.read, then zkeys.read inside have_spendingkey_for_extfvk (same order as
|
||||
// scan_block). Never call this while holding a zkeys guard.
|
||||
fn spendable_zbalance_f(&self, f: &ZAddrFilter) -> u64 {
|
||||
let anchor_height = self.get_anchor_height();
|
||||
let hrp = self.config.hrp_sapling_address();
|
||||
|
||||
self.txs
|
||||
.read()
|
||||
@@ -1164,20 +1189,11 @@ impl LightWallet {
|
||||
tx.notes
|
||||
.iter()
|
||||
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none())
|
||||
.filter(|nd| f.matches(hrp, nd))
|
||||
.filter(|nd| {
|
||||
// Check to see if we have this note's spending key.
|
||||
self.have_spendingkey_for_extfvk(&nd.extfvk)
|
||||
})
|
||||
.filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it.
|
||||
match addr.as_ref() {
|
||||
Some(a) => *a == encode_payment_address(
|
||||
self.config.hrp_sapling_address(),
|
||||
&nd.extfvk.fvk.vk
|
||||
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
|
||||
),
|
||||
None => true
|
||||
}
|
||||
})
|
||||
.map(|nd| nd.note.value)
|
||||
.sum::<u64>()
|
||||
} else {
|
||||
@@ -1227,6 +1243,45 @@ impl LightWallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-zkey z balances for do_balance, in zkeys order:
|
||||
/// (address, zbalance, unconfirmed, verified_zbalance, spendable_zbalance).
|
||||
/// Equal to calling zbalance/unconfirmed_zbalance/verified_zbalance/spendable_zbalance with
|
||||
/// Some(encoded zaddress), in that order, without re-deriving any note's address.
|
||||
pub fn z_balances_by_address(&self) -> Vec<(String, u64, u64, u64, u64)> {
|
||||
let hrp = self.config.hrp_sapling_address();
|
||||
// Snapshot, then RELEASE the zkeys guard before any txs lock: scan_block holds txs.write
|
||||
// while taking zkeys.read/write (ensure_hd_zaddresses -> add_zaddr), and
|
||||
// spendable_zbalance_f re-reads zkeys. Holding zkeys here would invert the lock order
|
||||
// and re-enter a writer-preferring RwLock (deadlock).
|
||||
let addrs: Vec<(String, PaymentAddress<Bls12>)> = self.zkeys.read().unwrap().iter()
|
||||
.map(|zk| (encode_payment_address(hrp, &zk.zaddress), zk.zaddress.clone()))
|
||||
.collect();
|
||||
|
||||
addrs.into_iter().map(|(address, pa)| {
|
||||
let f = ZAddrFilter::Addr(pa);
|
||||
let zbalance = {
|
||||
let unconfirmed_balance = self.unconfirmed_zbalance(Some(address.clone()));
|
||||
self.confirmed_zbalance_f(&f) + unconfirmed_balance
|
||||
};
|
||||
let unconfirmed = self.unconfirmed_zbalance(Some(address.clone()));
|
||||
let verified = self.verified_zbalance_f(&f);
|
||||
let spendable = self.spendable_zbalance_f(&f);
|
||||
(address, zbalance, unconfirmed, verified, spendable)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Encoded z-addresses whose FIRST zkey with that address has a spending key. Same set as
|
||||
/// get_all_zaddresses() filtered by have_spending_key_for_zaddress (Z encodes, not Z + Z^2).
|
||||
pub fn spendable_zaddress_set(&self) -> HashSet<String> {
|
||||
let hrp = self.config.hrp_sapling_address();
|
||||
let mut first: HashMap<String, bool> = HashMap::new();
|
||||
for zk in self.zkeys.read().unwrap().iter() {
|
||||
first.entry(encode_payment_address(hrp, &zk.zaddress))
|
||||
.or_insert_with(|| zk.have_spending_key());
|
||||
}
|
||||
first.into_iter().filter(|(_, spendable)| *spendable).map(|(a, _)| a).collect()
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn add_toutput_to_wtx(&self, height: i32, timestamp: u64, txid: &TxId, vout: &TxOut, n: u64) {
|
||||
|
||||
Reference in New Issue
Block a user