Skip to content
VOUCHVOUCHDocs

Architecture

The verification pipeline

One function decides every verdict: pof-verify::verify. This page walks through its six checks, the verdicts they produce, the policy a verifier can add, and the three places it runs.

On this page

#Six checks, cheapest first

The verification pipeline.pof bytes + contextaudience · anchors · revocations · now · policy1 · Formatmagic · version · checksum · decodefailMalformedpass2 · Expirynow < expires_atfailExpiredpass3 · Revocationno published secret matches the tagfailRevokedpass4 · Audience and scopeblake2b(id) = audience · epochfailWrongAudience · WrongScopepass5 · Anchors and policyboth roots published · fresh · dormantfailAnchorNotFound · AnchorTooOld · NotDormantLongEnoughpass6 · Proof and reusedistinct tags · signature · Halo2 · registryfailProofInvalid · AlreadyUsedpassValidclaim · anchor height · unmoved sinceEach verdict is typed, never a bool; the CLI turns it into an exit code.
Six checks, cheapest first. The first failure decides the verdict and the later checks are reported as not run, so no cryptography ever runs on a file that failed a cheap check.
#CheckFails as
1Magic, version, length limits, checksum, decode; HoldsAtLeast in whole 0.125 ZEC unitsMalformed { reason }
2now < expires_at (equality is expired, as on Solana)Expired { at }
3No published secret hashes to the revocation tagRevoked
4blake2b(identifier) equals audience; the epoch is the one requested, if anyWrongAudience · WrongScope
5The tree root at nc_height and the spent-set root at height are both in the table; the spent set fresh enough; the notes unmoved long enoughAnchorNotFound · AnchorTooOld · NotDormantLongEnough
6Five distinct tags; the RedPallas signature under rk; the Halo2 proof against the circuit’s key; then no tag in the registryProofInvalid · AlreadyUsed

ProofInvalid says the evidence does not prove the statement, not which constraint failed: verbose failure detail in a security tool is a debugging aid for an attacker.

#Context and policy

Rust
pub struct Context<'a> {
    audience: &'a str,               // your identifier; only its hash is compared
    anchors: &'a [AnchorRecord],     // the table you trust
    revoked_secrets: &'a [String],   // published secrets, hex
    now: u64,
    zk: &'a ZkVerifier,              // the embedded verifying keys
    policy: Policy,
    seen: &'a [String],              // your reuse registry: tags accepted in this scope
}

pub struct Policy {              // all optional
    tip_height: Option<u32>,     // with max_anchor_age: refuse a stale spent set
    max_anchor_age: Option<u32>,
    dormant_since: Option<u32>,  // notes unmoved since this block or earlier
    epoch: Option<u64>,          // the period your request named
}

Everything the verifier needs is passed in: it reads no files and makes no network calls, which is what lets it run unchanged in a browser.

#Verdicts

Rust
pub enum Verdict {
    Valid { claim: Claim, anchor_height: u32, dormant_since: Option<u32> },
    Expired { at: u64 },
    Revoked,
    WrongAudience,
    AnchorNotFound,
    AnchorTooOld { anchor_height: u32 },
    NotDormantLongEnough { dormant_since: u32, required: u32 },
    WrongScope { epoch: u64, required: u64 },
    AlreadyUsed,
    ProofInvalid { detail: String },
    Malformed { reason: String },
}

#Three hosts

One verifier, three hostspof-verifyverify(bytes, ctx)→ VerificationResultNative CLIpof-verify check · exit codesWASM in a Web Workerpof-wasm · VK embeddedAttestor servicepof-attest · signs the verdictCI jobs, scripts, railsfiles on disk, no network/verify · /rail · /reservesthe proof never leaves the tabSolana programs · rail APIEd25519 attestations, webhooks
The CLI, the browser and the attestor run the same Rust function. CI runs every test vector through the native build and the committed WASM build and checks the verdicts are identical.

In the browser

The site lazily loads /wasm/pof_wasm.js in a Web Worker. warm() loads the embedded parameters and verifying key (about 330 ms, once); each verification then takes about 90 ms. The page fetches /api/anchors and /api/revocations, calls verify(bytes, audience, now, anchorsJson, revokedJson) and renders the JSON result. A verify link carries the proof after the #, so it never reaches a server.

The CLI

pof-verify check reads local files only and turns the verdict into an exit code: 0 valid, 1 invalid, 2 expired, 3 malformed, 4 could not run.

The attestor

pof-attest runs the same verify under a per-core semaphore (429 when busy), with revocations from a static file and the site’s list (cached for 10 s, a 4 s timeout, keeping the last list on failure). It signs only a Valid verdict bound to a Solana account.

#The JSON result

Every host returns the same JSON: the verdict, the six checks in order, the matched anchor record, the proof’s scope and tags, an exit certificate’s revealed nullifiers, and envelope, the JSON projection of the proof (hex in lowercase, amounts in zatoshi, times in unix seconds). Its JSON Schema is at /schema/pof-v1.json, and CI checks every vector’s result against it.

JSON
{ "verdict":  { "kind": "Valid", "claim": { "kind": "HoldsAtLeast", "zatoshi": 100000000 }, "anchorHeight": 4410000 },
  "checks":   [ { "id": "format", "label": "…", "status": "pass", "detail": "…" }, … ],
  "envelope": { "version": 1, "claim": { … }, "audience": "412a96…", "binding": "0000…", "anchor": { … },
                "issuedAt": 1790622467, "expiresAt": 1800990467, "revocation": "704960c1…", "evidence": { … } },
  "sizeBytes": 11884, "checksum": "c82489…", "anchor": { "network": "testnet", "height": 4410000, … },
  "now": 1790693805, "verifier": "pof-verify 0.1.0" }

#Revocation

Revoking a proofHolderpof-prove revokeRevocation list/api/revocationsAny verifierbrowser · CLIAttestorPOST { secret }store itshared store (Redis)GET the list{ secrets: […] }blake2b(secret)[..16]= the file’s tag → RevokedGET (cached 10 s)refuses to sign422 · Revoked
Revocation is a hash lock: only the holder knows the secret, so the list can drop an entry but never forge one. The attestor caches the list for ten seconds.

#How agreement is tested

TestWhat it proves
pof-verify tests/fixtures.rsThe native verdict for every committed vector equals expected.json.
pnpm test:wasmThe committed WASM build gives the same verdicts, and every result matches the JSON Schema.
pnpm test:fixturesThe TypeScript codec parses the Rust encoder’s bytes; request-link rules agree in both languages.
embedded_vk_matches_keygenThe embedded verifying keys equal a fresh keygen.