@chainvue/verus-sapling builds and signs shielded Verus transactions. Signing
requires the Sapling extended spending key to be present on the signing host
— the same trust surface as a transparent WIF in @chainvue/verus-sdk, or any
"hot" signer. This document states what the library does and does not protect,
so integrators can reason about the residual risk.
- The spending key lives in the signing process's memory while a transaction is built. If that host is compromised, the key can be stolen — this is inherent to any software signer and is not something a library can prevent.
- The library is offline by design: it builds and signs bytes; a separate transport broadcasts them. Chain data (compact blocks, tree state) comes from a lightwalletd instance the consumer chooses.
Verified in the codebase (see src/wallet.ts, src/wasm.ts, the crate):
- The key never crosses the network. The spending/viewing key is placed only
in the local JSON
specpassed to the wasm prover. NoLightwalletdTransportmethod (getTreeState,getBlockRange,getTransaction,sendTransaction,getLatestHeight) ever receives it. - The key is never logged. No code path prints key material.
- Errors do not echo the key. No error message includes the key. The single exception is a malformed hex key, where the underlying hex error may reveal one character and its position — never the whole key.
- The signed transaction contains no key material — only proofs, signatures, and note ciphertexts.
- Value conservation is enforced before signing.
buildShieldedSpendrequires an explicitfeeSatsand refuses to sign unlessnote.valueSats == Σ outputs + feeSats(with amaxFeeSatsceiling). The Rust builder re-checks this against the decrypted note value. A forgotten change output can no longer silently donate the remainder to miners.
- wasm memory is not zeroized. The key persists in the wasm module's linear memory until the module/worker is torn down; a memory dump of the process could recover it. This is inherent to wasm-based proving.
- Host / supply-chain / page compromise. Malicious dependencies, XSS in a consuming web page, or a compromised OS can exfiltrate the key. Standard hot-signer caveats apply.
- Params authenticity — the browser Worker verifies the proving parameters'
SHA-256 (
verifyCanonicalParams); a Node caller loading params itself should call it too. Non-canonical params are refused. - Network privacy. Fetching a note's transaction and broadcasting through the same lightwalletd links your notes to your IP — a standard light-client caveat. Use the TLS default (or Tor/a trusted relay) and consider separating the fetch and broadcast paths for stronger unlinkability.
- Sender-side recovery of shielded sends.
t→zoutputs are built with no outgoing viewing key (ovk = None), so the sender cannot later detect its own shield outputs. Wallet implementers who want that should thread anovk.
- Scan with a viewing key, not the spending key. Note detection needs only a
Diversifiable Full Viewing Key. The API supports this:
detectNotes(..., { key: { dfvkHex } }). Derive the DFVK once, use it for all scanning, and load the full spending key only when actually signing a spend. This minimizes how long the spending key is in memory. - Never persist the spending key to disk. The example extension keeps it in
chrome.storage.session(in-memory, session-scoped) — neverchrome.storage.local. - Run the prover in an isolated context (a Web Worker), as the examples do.
- Treat the signing host as security-critical: pin dependencies, apply a strict CSP, and keep untrusted code out of the page that holds the key.
Report suspected security issues privately to the maintainer — do not open a public issue. Once the repository is public, use GitHub's private security advisory ("Report a vulnerability") flow.