zkSVM

App

Litepaper

Documentation

Links

DocsHow it works

Attestations and receipts

Saying something true without opening anything.

3 min read

Two ways to say something true about shielded value without opening it: one on-chain, one off.

Attestations

The statement

A note in this pool, unspent right now, holds at least threshold lamports.

Not which note, how much more, or whose.

Why it is not a membership proof

The obvious circuit — "I know a note in the tree with amount ≥ threshold" — is wrong, and instructively so. The tree is append-only: a note spent last year is still a leaf. Whether a note is unspent lives in the nullifier records, which are Solana accounts, not something a circuit can read. A pure membership proof would let anyone who once held 100 SOL prove it forever.

So attest spends the note and recreates it in one transaction:

in:   note(amount, pk, blinding)          nullifier goes on record
out:  note(amount, pk, fresh blinding)    same owner, same amount

The circuit enforces that the output is the same asset and amount under the same key, and that amount >= threshold. The nullifier being accepted is the proof of unspent-ness — the system program would have refused it otherwise. The holder's balance does not change.

The holder ID

The public inputs include

tag = Poseidon2(s, scope)

where s is the holder's spending key and scope is a field element chosen by the verifier — in the app, the hash of the wording. Within one scope the same key always yields the same tag; across scopes tags are unlinkable. A verifier can recognize a returning holder without learning who they are, and two verifiers cannot compare.

A verifier should put a nonce in the scope. Without one, an old attestation can be presented again.

Reading one

The program emits

Rust
Attested { scope, tag, threshold }

and a verifier needs only the transaction: that it succeeded, and that the event was logged while the pool program was the one executing. The SDK's parseAttestedEvents follows the invoke lines for that reason — any program can print bytes that look like an event. On-chain, another program calls attest by CPI and takes success as the answer.

What it is not

It is not a way to count funds. The tag is a pseudonym of a key: value moved to a fresh key attests again under a fresh tag. Weighting a vote by shielded balance needs funds locked for the duration — an escrow, which returnable notes are most of the way to.

Receipts

The statement

The commitment C opens to a note owned by pk, holding at least min_amount. This receipt is for context.

Public inputs: [commitment, asset_id, pk, min_amount, context]. About 700 constraints; it proves in well under a second.

Why not just reveal the opening

Anyone can check a payment if you hand over (amount, pk, blinding). But the blinding is not harmless. For a note owned by an address or a condition, it is the nullifier key — its holder can compute the note's nullifier and watch the chain for the moment it is spent. And the amount becomes exact when a floor would have done.

A receipt proves the same fact and discloses neither.

Verification is three checks

  1. The proof verifies against the receipt circuit's key.
  2. pk is the recipient the verifier expects — recomputed from the address: the pk half of a zks… address, Poseidon2(hi, lo) for a Solana address, or Poseidon4(…) for a returnable note.
  3. commitment is one of the pool's leaves.

The proof alone covers only the first. The app does all three.

What it does not show

Who wrote it. The payer knows the opening because they built the note; the payee knows it because they received it. A receipt proves the payment. It also does not show that the note is unspent — and for a returnable note, the payment may since have gone back, which is why the app displays the return date alongside.

Wire formats: Protocol. SDK: Verifying proofs.