perf(lite-backend): borrow embedded Sapling params instead of copying

Every LightClient copied the Sapling spend and output parameters, which
rust-embed already compiles into the binary, onto the heap: 51,551,256
bytes (49.2 MiB) of private memory per live client, whether or not it
ever sent anything.

LightClient::sapling_output and sapling_spend are now Cow<'static, [u8]>
and borrow the embedded image. set_sapling_params still stores an owned
copy, with its is-empty guard, hash checks and disk write unchanged. The
prover receives the same bytes.

Behaviour changes:
- about 49 MiB less anonymous memory per live LightClient; the wallet
  file and JSON are unchanged;
- the first send or shield pages the 51.5 MB of embedded params in from
  the binary, where client creation used to copy them (an estimated
  0.3-1 s from the AppImage);
- the public fields sapling_output and sapling_spend change type from
  Vec<u8> to Cow<'static, [u8]>: a Rust API change, with no C ABI or
  exported-symbol change.

tests/sapling_params.rs checks that a restored, a new and a reloaded
client all borrow the embedded image (same pointer, pinned lengths and
SHA-256), that set_sapling_params keeps its guard and hash checks, and
that the borrowed params parse like a heap copy. An ignored test
measures 576 KiB of RssAnon growth for a second client, against 46.5 MiB
with the old heap copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 09:50:03 -05:00
co-authored by Claude Opus 5.5
parent bd4c06dfd0
commit 6de9b5cc90
2 changed files with 197 additions and 16 deletions
@@ -2,6 +2,7 @@
// Released under the GPLv3
use crate::lightwallet::LightWallet;
use std::borrow::Cow;
use std::sync::{Arc, RwLock, Mutex, mpsc::channel};
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering};
use std::path::{Path, PathBuf};
@@ -302,8 +303,8 @@ pub struct LightClient {
pub config : LightClientConfig,
// zcash-params
pub sapling_output : Vec<u8>,
pub sapling_spend : Vec<u8>,
pub sapling_output : Cow<'static, [u8]>,
pub sapling_spend : Cow<'static, [u8]>,
sync_lock : Mutex<()>,
sync_status : Arc<RwLock<WalletStatus>>, // The current syncing status of the Wallet.
@@ -338,8 +339,8 @@ impl LightClient {
fn read_sapling_params(&mut self) {
// Read Sapling Params
use crate::SaplingParams;
self.sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref());
self.sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref());
self.sapling_output = SaplingParams::get("sapling-output.params").unwrap();
self.sapling_spend = SaplingParams::get("sapling-spend.params").unwrap();
}
pub fn set_sapling_params(&mut self, sapling_output: &[u8], sapling_spend: &[u8]) -> Result<(), String> {
use sha2::{Sha256, Digest};
@@ -354,10 +355,10 @@ impl LightClient {
}
// Will not overwrite previous params
if self.sapling_output.is_empty() {
self.sapling_output.extend_from_slice(sapling_output);
self.sapling_output = Cow::Owned(sapling_output.to_vec());
}
if self.sapling_spend.is_empty() {
self.sapling_spend.extend_from_slice(sapling_spend);
self.sapling_spend = Cow::Owned(sapling_spend.to_vec());
}
// Ensure that the sapling params are stored on disk properly as well.
@@ -389,8 +390,8 @@ impl LightClient {
let mut l = LightClient {
wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), &config, 0,0 )?)),
config : config.clone(),
sapling_output : vec![],
sapling_spend : vec![],
sapling_output : Cow::Borrowed(&[]),
sapling_spend : Cow::Borrowed(&[]),
sync_lock : Mutex::new(()),
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
shutdown_flag : Arc::new(AtomicBool::new(false)),
@@ -418,8 +419,8 @@ impl LightClient {
let mut l = LightClient {
wallet : Arc::new(RwLock::new(LightWallet::new(None, config, latest_block, 0)?)),
config : config.clone(),
sapling_output : vec![],
sapling_spend : vec![],
sapling_output : Cow::Borrowed(&[]),
sapling_spend : Cow::Borrowed(&[]),
sync_lock : Mutex::new(()),
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
shutdown_flag : Arc::new(AtomicBool::new(false)),
@@ -446,8 +447,8 @@ impl LightClient {
let mut l = LightClient {
wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), config, birthday, number)?)),
config : config.clone(),
sapling_output : vec![],
sapling_spend : vec![],
sapling_output : Cow::Borrowed(&[]),
sapling_spend : Cow::Borrowed(&[]),
sync_lock : Mutex::new(()),
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
shutdown_flag : Arc::new(AtomicBool::new(false)),
@@ -503,8 +504,8 @@ impl LightClient {
let mut lc = LightClient {
wallet : Arc::new(RwLock::new(wallet)),
config : config.clone(),
sapling_output : vec![],
sapling_spend : vec![],
sapling_output : Cow::Borrowed(&[]),
sapling_spend : Cow::Borrowed(&[]),
sync_lock : Mutex::new(()),
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
shutdown_flag : Arc::new(AtomicBool::new(false)),
@@ -1837,8 +1838,8 @@ pub mod tests {
let lc = LightClient {
wallet : Arc::new(RwLock::new(LightWallet::new(None, &config, 0, 0).unwrap())),
config : config,
sapling_output : vec![],
sapling_spend : vec![],
sapling_output : std::borrow::Cow::Borrowed(&[]),
sapling_spend : std::borrow::Cow::Borrowed(&[]),
sync_lock : Mutex::new(()),
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
shutdown_flag : Arc::new(AtomicBool::new(false)),