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
| Asset | Threat | Who wants it |
|---|---|---|
| The holder’s privacy | Learning the balance, the notes or addresses; linking proofs | Verifiers, observers, other verifiers |
| The verifier’s decision | A false Valid: forged, edited, borrowed, reused, stale or revoked proofs | A dishonest holder, or a thief of a proof |
| On-chain value | A receipt, a line or a mint without a real proof behind it | Anyone who can send a transaction |
| The holder’s keys | Exfiltration while proving | Malware, a malicious build |
#Attacks on the proof
| Attack | Stopped by | Shown by |
|---|---|---|
| Edit the claim (500 → 5,000 ZEC) and re-seal the checksum | The signature covers the statement | forged-claim vector → ProofInvalid |
| Re-address a proof to yourself | The audience is in the signed statement | readdressed vector → ProofInvalid |
| Flip a byte of the evidence | Halo2 verification | tampered vector; the e2e tampered-proof check |
| Extend the expiry | The expiry is signed | extended-expiry vector → ProofInvalid |
| Truncate or corrupt the file | Checksum; injective decoding | codec tests → Malformed |
| Count one note in several slots | The distinct-tag check before Halo2 | real-proof test: one note five times, refused |
| Prove more than you hold | The threshold constraint | vouch_threshold; the impossible-claim test |
| Use a note created after H₀ | The tree at H₀ does not contain it | refused at proving; dormant-too-young vector |
| Use a spent note | IMT non-membership at the spent-set root | real-proof dormancy tests |
| Prove with only a viewing key | The RedPallas spend-authority signature | by construction |
#Attacks on the context
| Attack | Stopped by | Residual risk |
|---|---|---|
| Show a proof to a verifier it was not made for | The audience check | none |
| Use a revoked proof | The revocation list at verify time | A verifier that never fetches the list |
| Pledge the same coins twice to one verifier | Scope tags and the verifier’s registry → AlreadyUsed | A 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 anchor | Anchors come from the verifier’s own table | A wrong entry can only make honest proofs fail |
#Attacks on the programs
| Attack | Stopped by | LiteSVM test |
|---|---|---|
| Replay an attestation | One receipt per proof id | replay_of_the_same_proof_fails |
| Sign with a key not on the allowlist | The allowlist | wrong_key_is_refused |
| Reuse a signature over another message | Exact message match | signature_over_a_different_message_is_refused |
| Point Ed25519 offsets into other data | Every offset must be u16::MAX | offsets_into_another_instruction_are_refused |
| Put the signature after the submit, or truncate it | Only earlier, complete entries count | a_signature_after_submit_or_a_truncated_entry_does_not_count |
| Submit a stale, future, expired or invalid verdict | Slot age, expiry and verdict checks | stale_future_expired_invalid_and_unknown_are_refused |
| Open a line from someone else’s receipt | The beneficiary must sign | only_the_bound_account_can_open_a_line |
| Consume a receipt twice, or as a fake consumer | The consumed flag; the consumer-program PDA signature | mark_consumed_needs_a_consumer_program_signature |
| Claim the config before the deployer | initialize needs the upgrade authority | only_the_upgrade_authority_initializes_once |
| Draw on a stale proof | fresh_until | a_line_draws_only_while_its_proof_is_fresh |
| Refresh with an older anchor | Anchor no older than the last | a_refresh_must_meet_the_terms_and_not_go_back_in_time |
| Mint from a stale feed | Secure Mint freshness | a_stale_feed_mints_nothing_until_refreshed |
| Mint as someone other than the issuer | has_one = issuer | only_the_issuer_mints |
| Publish a wrong-signer, older or expired batch | publish checks | publish_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
| Leak | To whom | Mitigation |
|---|---|---|
| The threshold and the anchor heights | The verifier | By design: that is the fact proven |
| Reuse within a period | That verifier only | By design, when the verifier names a period |
| The certified notes’ nullifiers (exits) | The rail | Make the certificate only when ready to send |
| Timing between certificate and deposit | The rail | Not mitigated |
| The verify link | Whoever has the link | The 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.