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 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 08:50:12 -05:00
co-authored by Claude Opus 4.8
parent c045e56d4e
commit 0bdea68337
@@ -1,5 +1,7 @@
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::sync::Arc; use std::sync::Arc;
use std::cell::RefCell;
use std::collections::HashMap;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use pairing::bls12_381::{Bls12}; use pairing::bls12_381::{Bls12};
@@ -26,6 +28,24 @@ use zcash_primitives::{
use zcash_primitives::zip32::ExtendedSpendingKey; 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<HashMap<[u8; 169], ExtendedFullViewingKey>> = RefCell::new(HashMap::new());
}
fn read_efvk_cached<R: Read>(r: &mut R) -> io::Result<ExtendedFullViewingKey> {
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 struct BlockData {
pub height: i32, pub height: i32,
pub hash: BlockHash, pub hash: BlockHash,
@@ -147,7 +167,7 @@ impl SaplingNoteData {
let account = reader.read_u64::<LittleEndian>()? as usize; let account = reader.read_u64::<LittleEndian>()? as usize;
let extfvk = ExtendedFullViewingKey::read(&mut reader)?; let extfvk = read_efvk_cached(&mut reader)?;
let mut diversifier_bytes = [0u8; 11]; let mut diversifier_bytes = [0u8; 11];
reader.read_exact(&mut diversifier_bytes)?; reader.read_exact(&mut diversifier_bytes)?;