zkSVM

App

Litepaper

Documentation

Links

DocsBuild

Verifying proofs

Check attestations and receipts in your backend.

2 min read

Your service asks a user to prove something — that they hold enough, that they paid an invoice. This page is the checking side, in a backend or a script.

Attestations

The user sends you a transaction signature. The proof was already verified on-chain; you are establishing that the transaction is what it claims to be.

1. Issue a scope

Give each request its own wording and keep it:

TypeScript
import { textToFr } from "@zksvm/sdk";

const wording = `example.org login ${crypto.randomUUID()}`;
const scope = textToFr(wording);            // sha256, reduced into the field

A fresh value per request is what stops an old attestation being presented again.

2. Read the transaction

TypeScript
import { parseAttestedEvents, frFromBytes } from "@zksvm/sdk";

const tx = await conn.getTransaction(signature, { maxSupportedTransactionVersion: 0 });
if (!tx || tx.meta?.err) throw new Error("no such successful transaction");

const [event] = parseAttestedEvents(tx.meta.logMessages ?? [], PROGRAM_ID);
if (!event) throw new Error("not an attestation by this pool");

parseAttestedEvents only returns events that were logged while your pool program was executing. Any program can print bytes that look like an event; the parser follows the invoke lines so that those do not count. Always pass the program id you trust.

3. Decide

TypeScript
const ok =
  frFromBytes(event.scope) === scope &&
  event.threshold >= required &&
  (tx.blockTime ?? 0) > issuedAt;

const holder = frFromBytes(event.tag);      // stable per key, per scope

Store holder if one person should qualify once. It is a pseudonym: the same key under the same scope always produces it, and it means nothing under any other scope.

Receipts

The user sends you a receipt: a proof plus what it states. Nothing is on-chain, so all three checks are yours.

TypeScript
import { verifyReceipt, addressPk } from "@zksvm/sdk";
import verifyingKey from "./receipt.vkey.json";   // circuits/build/receipt/verification_key.json

// 1. The proof is sound.
if (!(await verifyReceipt(receipt, verifyingKey))) throw new Error("bad proof");

// 2. It is about the recipient you expect.
if (receipt.pk !== (await addressPk(myAddress.toBytes()))) throw new Error("wrong recipient");

// 3. The note is in the pool.
if (!leaves.has(receipt.commitment)) throw new Error("no such note");

For the second check, derive pk the way the payment was addressed:

Paid topk
a zks… addressits first 32 bytes, as a field element
a Solana addressaddressPk(address)
a Solana address, returnablyreturnablePk(address, refundPk, notBefore) — the receipt carries the last two

For the third, keep the set of commitments from NoteAdded events, as any pool mirror does.

Then apply your own rules: receipt.minAmount >= invoice.total, receipt.context === invoice.reference.

What you have established

That a note paying your address at least that much exists in the pool, made out for that purpose. Either party to a payment can write its receipt, so it identifies the payment, not the presenter — match it to an invoice reference you issued, and accept each commitment once.

For a returnable payment, wait until it has been redeemed before treating it as settled: until then it can still go back to its sender.

Concepts: Attestations and receipts.