From 0bdea68337e809e8a181d2f6de3e0ce7b67812d4 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 14 Oct 2026 08:50:12 -0500 Subject: [PATCH] perf(litebe): memoize per-note viewing-key decode on wallet open Opening a wallet reads each shielded note from disk, and every note record embeds a 169-byte extended full viewing key that was decoded from scratch for every note. That decode is an elliptic-curve parse; a wallet with many notes sharing a handful of keys paid for it once per note, which dominated open time. - Note reads now go through a small content-addressed cache keyed on the exact 169 raw key bytes. The first note with a given key decodes it; the rest reuse the result. Because the cache is keyed on the raw bytes and the decoder is deterministic, the cached key is identical to a fresh parse. - The cache is thread-local and naturally bounded by the few distinct keys a wallet holds, so it adds no cross-thread state and cannot grow without bound. The bytes on disk are read exactly as before - this only avoids re-parsing identical bytes - so the loaded wallet is byte-identical, verified against the differential harness. The wallet file format is unchanged. Co-Authored-By: Claude Opus 4.8 --- .../lib/src/lightwallet/data.rs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/data.rs b/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/data.rs index 29d7b80..b5c64a2 100644 --- a/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/data.rs +++ b/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightwallet/data.rs @@ -1,5 +1,7 @@ use std::io::{self, Read, Write}; use std::sync::Arc; +use std::cell::RefCell; +use std::collections::HashMap; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use pairing::bls12_381::{Bls12}; @@ -26,6 +28,24 @@ use zcash_primitives::{ use zcash_primitives::zip32::ExtendedSpendingKey; +// C241: wallet open re-parsed the 169-byte ExtendedFullViewingKey once PER NOTE. Memoize the decode, +// keyed by the exact 169 raw bytes (content-addressed, so the result is always identical to a fresh +// parse). Thread-local and naturally bounded by the handful of distinct keys a wallet holds. +thread_local! { + static EFVK_CACHE: RefCell> = RefCell::new(HashMap::new()); +} + +fn read_efvk_cached(r: &mut R) -> io::Result { + let mut raw = [0u8; 169]; + r.read_exact(&mut raw)?; + if let Some(k) = EFVK_CACHE.with(|c| c.borrow().get(&raw).cloned()) { + return Ok(k); + } + let k = ExtendedFullViewingKey::read(&raw[..])?; // identical parser on identical bytes + EFVK_CACHE.with(|c| c.borrow_mut().insert(raw, k.clone())); + Ok(k) +} + pub struct BlockData { pub height: i32, pub hash: BlockHash, @@ -147,7 +167,7 @@ impl SaplingNoteData { let account = reader.read_u64::()? as usize; - let extfvk = ExtendedFullViewingKey::read(&mut reader)?; + let extfvk = read_efvk_cached(&mut reader)?; let mut diversifier_bytes = [0u8; 11]; reader.read_exact(&mut diversifier_bytes)?;