Skip to content
VOUCHVOUCHDocs

Trust and security

Threat model

What an attacker could try at each layer, what stops it, and the test that shows it is stopped. Where something is not stopped, this page says so.

On this page

#What is being protected

AssetThreatWho wants it
The holder’s privacyLearning the balance, the notes or addresses; linking proofsVerifiers, observers, other verifiers
The verifier’s decisionA false Valid: forged, edited, borrowed, reused, stale or revoked proofsA dishonest holder, or a thief of a proof
On-chain valueA receipt, a line or a mint without a real proof behind itAnyone who can send a transaction
The holder’s keysExfiltration while provingMalware, a malicious build

#Attacks on the proof

AttackStopped byShown by
Edit the claim (500 → 5,000 ZEC) and re-seal the checksumThe signature covers the statementforged-claim vector → ProofInvalid
Re-address a proof to yourselfThe audience is in the signed statementreaddressed vector → ProofInvalid
Flip a byte of the evidenceHalo2 verificationtampered vector; the e2e tampered-proof check
Extend the expiryThe expiry is signedextended-expiry vector → ProofInvalid
Truncate or corrupt the fileChecksum; injective decodingcodec tests → Malformed
Count one note in several slotsThe distinct-tag check before Halo2real-proof test: one note five times, refused
Prove more than you holdThe threshold constraintvouch_threshold; the impossible-claim test
Use a note created after H₀The tree at H₀ does not contain itrefused at proving; dormant-too-young vector
Use a spent noteIMT non-membership at the spent-set rootreal-proof dormancy tests
Prove with only a viewing keyThe RedPallas spend-authority signatureby construction

#Attacks on the context

AttackStopped byResidual risk
Show a proof to a verifier it was not made forThe audience checknone
Use a revoked proofThe revocation list at verify timeA verifier that never fetches the list
Pledge the same coins twice to one verifierScope tags and the verifier’s registry → AlreadyUsedA verifier that names no period, or keeps no registry
Pledge the same coins to two verifiers—Not stopped: funds are not locked. Each proof is single-use where it lands.
Spend right after proving—Not stopped: a proof is as of one block. Ask again; pof-credit enforces freshness.
Feed a verifier a fake anchorAnchors come from the verifier’s own tableA wrong entry can only make honest proofs fail

#Attacks on the programs

AttackStopped byLiteSVM test
Replay an attestationOne receipt per proof idreplay_of_the_same_proof_fails
Sign with a key not on the allowlistThe allowlistwrong_key_is_refused
Reuse a signature over another messageExact message matchsignature_over_a_different_message_is_refused
Point Ed25519 offsets into other dataEvery offset must be u16::MAXoffsets_into_another_instruction_are_refused
Put the signature after the submit, or truncate itOnly earlier, complete entries counta_signature_after_submit_or_a_truncated_entry_does_not_count
Submit a stale, future, expired or invalid verdictSlot age, expiry and verdict checksstale_future_expired_invalid_and_unknown_are_refused
Open a line from someone else’s receiptThe beneficiary must signonly_the_bound_account_can_open_a_line
Consume a receipt twice, or as a fake consumerThe consumed flag; the consumer-program PDA signaturemark_consumed_needs_a_consumer_program_signature
Claim the config before the deployerinitialize needs the upgrade authorityonly_the_upgrade_authority_initializes_once
Draw on a stale prooffresh_untila_line_draws_only_while_its_proof_is_fresh
Refresh with an older anchorAnchor no older than the lasta_refresh_must_meet_the_terms_and_not_go_back_in_time
Mint from a stale feedSecure Mint freshnessa_stale_feed_mints_nothing_until_refreshed
Mint as someone other than the issuerhas_one = issueronly_the_issuer_mints
Publish a wrong-signer, older or expired batchpublish checkspublish_refuses_what_it_must

#The attestor

An attestor that lies can make a Solana program believe a false verdict. This is the one place trust is concentrated, and it is mitigated, not removed:

  • programs can require k of n distinct allowlisted attestors (2 of 3 is tested);
  • every verdict an attestor signs can be re-derived by anyone from the proof and public data, so a lie is detectable after the fact;
  • the attestor holds no user data and never sees keys: a compromised attestor cannot spend anyone’s ZEC.

A Zcash light client on Solana would remove the attestor for anchors; verifying the Halo2 proof in a zkVM and checking a Groth16 wrapper on Solana would remove it for proofs. Both are on the roadmap.

#Privacy leaks to know about

LeakTo whomMitigation
The threshold and the anchor heightsThe verifierBy design: that is the fact proven
Reuse within a periodThat verifier onlyBy design, when the verifier names a period
The certified notes’ nullifiers (exits)The railMake the certificate only when ready to send
Timing between certificate and depositThe railNot mitigated
The verify linkWhoever has the linkThe proof is after the #, so it never reaches a server log

#Out of scope

  • The origin of funds before they entered the shielded pool, and any “clean funds” claim.
  • Compliance with any specific regulation.
  • Side channels on the holder’s machine, and malicious builds of the prover (check the release SHA-256, or build from source).
  • Availability of lightwalletd, the site, the attestor or Solana.