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>
181 lines
8.2 KiB
Rust
181 lines
8.2 KiB
Rust
// Copyright The Hush Developers 2019-2022
|
|
// Released under the GPLv3
|
|
|
|
//! The Sapling params on a `LightClient` borrow the image that rust-embed bakes into the binary
|
|
//! (`Cow::Borrowed(&'static [u8])`) instead of copying ~49 MiB onto the heap for every client.
|
|
//!
|
|
//! Every client here is on the "main" chain (the "test" chain, which `LightClient::unconnected`
|
|
//! hard-codes, panics in `base58_pubkey_address` when the wallet is created) and lives in a temp
|
|
//! data dir `<tmp>/data`, so the wallet file and `get_zcash_params_path()` (`<data>/../.zcash-params`)
|
|
//! both stay inside the temp dir: nothing is written to ~/.silentdragonxlite or ~/.zcash-params.
|
|
|
|
use std::borrow::Cow;
|
|
use std::path::PathBuf;
|
|
|
|
use bellman::groth16::Parameters;
|
|
use pairing::bls12_381::Bls12;
|
|
use sha2::{Digest, Sha256};
|
|
use tempdir::TempDir;
|
|
|
|
use silentdragonxlitelib::lightclient::{LightClient, LightClientConfig};
|
|
use silentdragonxlitelib::SaplingParams;
|
|
|
|
const SEED: &str = "youth strong sweet gorilla hammer unhappy congress stamp left stereo riot salute road tag clean toilet artefact fork certain leopard entire civil degree wonder";
|
|
|
|
const OUTPUT_LEN: usize = 3_592_860;
|
|
const SPEND_LEN: usize = 47_958_396;
|
|
const OUTPUT_SHA256: &str = "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4";
|
|
const SPEND_SHA256: &str = "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13";
|
|
|
|
fn main_config(tmp: &TempDir) -> LightClientConfig {
|
|
let data: PathBuf = tmp.path().join("data");
|
|
LightClientConfig::create_unconnected("main".to_string(), data.to_str().map(|s| s.to_string()))
|
|
}
|
|
|
|
fn embedded(name: &str) -> Cow<'static, [u8]> {
|
|
SaplingParams::get(name).expect("embedded Sapling params")
|
|
}
|
|
|
|
fn sha256_hex(bytes: &[u8]) -> String {
|
|
hex::encode(Sha256::digest(bytes))
|
|
}
|
|
|
|
fn is_borrowed(c: &Cow<'static, [u8]>) -> bool {
|
|
matches!(c, Cow::Borrowed(_))
|
|
}
|
|
|
|
/// Borrowed (no heap copy), the right length and content, and the files are not swapped.
|
|
fn assert_params_pinned(lc: &LightClient, ctx: &str) {
|
|
assert!(is_borrowed(&lc.sapling_output), "{}: sapling_output is not Cow::Borrowed", ctx);
|
|
assert!(is_borrowed(&lc.sapling_spend), "{}: sapling_spend is not Cow::Borrowed", ctx);
|
|
assert_eq!(lc.sapling_output.len(), OUTPUT_LEN, "{}: sapling_output length", ctx);
|
|
assert_eq!(lc.sapling_spend.len(), SPEND_LEN, "{}: sapling_spend length", ctx);
|
|
assert_eq!(sha256_hex(&lc.sapling_output[..]), OUTPUT_SHA256, "{}: sapling_output hash", ctx);
|
|
assert_eq!(sha256_hex(&lc.sapling_spend[..]), SPEND_SHA256, "{}: sapling_spend hash", ctx);
|
|
}
|
|
|
|
#[test]
|
|
fn params_borrowed_and_pinned() {
|
|
// Restore from a seed phrase.
|
|
let tmp = TempDir::new("sdxl-params").unwrap();
|
|
let cfg = main_config(&tmp);
|
|
let lc = LightClient::new_from_phrase(SEED.to_string(), &cfg, 0, 0, false).unwrap();
|
|
assert_params_pinned(&lc, "new_from_phrase");
|
|
// The borrow points into the embedded image itself.
|
|
assert_eq!(lc.sapling_spend.as_ptr(), embedded("sapling-spend.params").as_ptr());
|
|
assert_eq!(lc.sapling_output.as_ptr(), embedded("sapling-output.params").as_ptr());
|
|
|
|
// The GUI open path: create a new wallet, save it, then open it from disk.
|
|
let tmp2 = TempDir::new("sdxl-params").unwrap();
|
|
let cfg2 = main_config(&tmp2);
|
|
let lc2 = LightClient::new(&cfg2, 0).unwrap();
|
|
assert_params_pinned(&lc2, "new");
|
|
lc2.do_save().unwrap();
|
|
drop(lc2);
|
|
let lc3 = LightClient::read_from_disk(&cfg2).unwrap();
|
|
assert_params_pinned(&lc3, "read_from_disk");
|
|
}
|
|
|
|
#[test]
|
|
fn set_params_guard_and_hash() {
|
|
let tmp = TempDir::new("sdxl-params").unwrap();
|
|
let cfg = main_config(&tmp);
|
|
let mut lc = LightClient::new_from_phrase(SEED.to_string(), &cfg, 0, 0, false).unwrap();
|
|
|
|
let out = embedded("sapling-output.params").to_vec();
|
|
let spend = embedded("sapling-spend.params").to_vec();
|
|
|
|
// Fields already filled from the embedded image: a valid set is Ok and does not overwrite them.
|
|
let p_out = lc.sapling_output.as_ptr();
|
|
let p_spend = lc.sapling_spend.as_ptr();
|
|
assert!(lc.set_sapling_params(&out, &spend).is_ok());
|
|
assert!(is_borrowed(&lc.sapling_output) && is_borrowed(&lc.sapling_spend));
|
|
assert_eq!(lc.sapling_output.as_ptr(), p_out);
|
|
assert_eq!(lc.sapling_spend.as_ptr(), p_spend);
|
|
|
|
// Empty fields (as with embed_params off).
|
|
lc.sapling_output = Cow::Borrowed(&[]);
|
|
lc.sapling_spend = Cow::Borrowed(&[]);
|
|
|
|
// Swapped files fail the hash check and leave both fields untouched.
|
|
assert!(lc.set_sapling_params(&spend, &out).is_err());
|
|
assert!(lc.sapling_output.is_empty());
|
|
assert!(lc.sapling_spend.is_empty());
|
|
|
|
// The right files are taken as owned copies with identical bytes.
|
|
assert!(lc.set_sapling_params(&out, &spend).is_ok());
|
|
assert!(matches!(lc.sapling_output, Cow::Owned(_)), "sapling_output is not Cow::Owned");
|
|
assert!(matches!(lc.sapling_spend, Cow::Owned(_)), "sapling_spend is not Cow::Owned");
|
|
assert!(lc.sapling_output[..] == out[..]);
|
|
assert!(lc.sapling_spend[..] == spend[..]);
|
|
|
|
// A second call does not overwrite them.
|
|
let p = lc.sapling_spend.as_ptr();
|
|
let po = lc.sapling_output.as_ptr();
|
|
assert!(lc.set_sapling_params(&out, &spend).is_ok());
|
|
assert_eq!(lc.sapling_spend.as_ptr(), p);
|
|
assert_eq!(lc.sapling_output.as_ptr(), po);
|
|
|
|
// The params are also written to <data>/../.zcash-params, i.e. inside the temp dir.
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
let dir = tmp.path().join(".zcash-params");
|
|
let spend_file = dir.join("sapling-spend.params");
|
|
let output_file = dir.join("sapling-output.params");
|
|
assert!(spend_file.exists(), "{} missing", spend_file.display());
|
|
assert!(output_file.exists(), "{} missing", output_file.display());
|
|
assert_eq!(std::fs::metadata(&spend_file).unwrap().len(), SPEND_LEN as u64);
|
|
assert_eq!(std::fs::metadata(&output_file).unwrap().len(), OUTPUT_LEN as u64);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn borrowed_params_parse_identically() {
|
|
let tmp = TempDir::new("sdxl-params").unwrap();
|
|
let cfg = main_config(&tmp);
|
|
let lc = LightClient::new_from_phrase(SEED.to_string(), &cfg, 0, 0, false).unwrap();
|
|
|
|
// The new borrowed path against the old heap-copy path: the prover gets identical keys.
|
|
let spend_new = Parameters::<Bls12>::read(&lc.sapling_spend[..], false).unwrap();
|
|
let spend_old = Parameters::<Bls12>::read(&embedded("sapling-spend.params").to_vec()[..], false).unwrap();
|
|
assert!(spend_new == spend_old, "spend params parse differently");
|
|
drop(spend_new);
|
|
drop(spend_old);
|
|
|
|
let output_new = Parameters::<Bls12>::read(&lc.sapling_output[..], false).unwrap();
|
|
let output_old = Parameters::<Bls12>::read(&embedded("sapling-output.params").to_vec()[..], false).unwrap();
|
|
assert!(output_new == output_old, "output params parse differently");
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn rss_anon_kib() -> u64 {
|
|
let status = std::fs::read_to_string("/proc/self/status").unwrap();
|
|
for line in status.lines() {
|
|
if line.starts_with("RssAnon:") {
|
|
return line["RssAnon:".len()..].trim().trim_end_matches("kB").trim().parse().unwrap();
|
|
}
|
|
}
|
|
panic!("no RssAnon in /proc/self/status");
|
|
}
|
|
|
|
/// Run alone: `--test sapling_params -- --ignored --test-threads=1`. The old code copied both
|
|
/// params onto the heap for every client (~49 MiB of RssAnon).
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
#[ignore]
|
|
fn open_rss_anon_delta() {
|
|
// Warm up one-time allocations (lazy statics, allocator arenas) with a first client.
|
|
let tmp0 = TempDir::new("sdxl-params").unwrap();
|
|
let _warm = LightClient::new_from_phrase(SEED.to_string(), &main_config(&tmp0), 0, 0, false).unwrap();
|
|
|
|
let tmp = TempDir::new("sdxl-params").unwrap();
|
|
let cfg = main_config(&tmp);
|
|
let before = rss_anon_kib();
|
|
let lc = LightClient::new_from_phrase(SEED.to_string(), &cfg, 0, 0, false).unwrap();
|
|
let after = rss_anon_kib();
|
|
let delta = after.saturating_sub(before);
|
|
println!("RssAnon before {} kB, after {} kB, delta {} kB", before, after, delta);
|
|
assert_eq!(lc.sapling_spend.len(), SPEND_LEN);
|
|
assert!(delta < 16 * 1024, "opening a client grew RssAnon by {} kB", delta);
|
|
}
|