sparq
Back to examples

Prove you may hire a car — reveal nothing

Live via bb.js — real proof in your tabResearch-grade · not externally audited
  1. RDF credentialsprivate, on-device
  2. SPARQL eligibility querythe public relation
  3. ZK proof (UltraHonk)generated in your tab
  4. Desk sees “Eligible”+ the proof, nothing else
Your credentials (private)
never leaves your device
@prefix cred: <https://www.w3.org/2018/credentials#> .
@prefix sch:  <https://schema.org/> .
@prefix ex:   <https://example.org/vc/> .
@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .

# ── Gov-ID credential (issued by HM Passport Office) ──────────────
ex:govId a cred:VerifiableCredential ;
    cred:issuer    <https://gov.uk/issuers/hmpo> ;
    cred:holder    ex:holder ;
    sch:birthDate  "1994-04-02"^^xsd:date ;
    ex:age         "30"^^xsd:integer .          # ← the value proven hidden

# ── Driving-licence credential (issued by the DVLA) ───────────────
ex:licence a cred:VerifiableCredential ;
    cred:issuer        <https://gov.uk/issuers/dvla> ;
    cred:holder        ex:holder ;               # ← same holder as gov-ID
    ex:licenceNumber   "MORGA753116SM9IJ" ;      # ← stays private
    ex:licenceValid    true ;
    ex:statusListIndex "48172"^^xsd:integer .    # ← revocation slot (hidden)

Two W3C Verifiable Credentials — a gov-ID and a DVLA driving licence. The car-hire desk never sees this Turtle; only commitments to it and the eligibility verdict.

The desk’s eligibility query (public)
Initializing ZK prover… Live via bb.js
# Car-hire desk eligibility — answered with ZERO disclosure of the data.
ASK {
  ?id      ex:age          ?age ;
           cred:holder     ?person .
  ?licence cred:holder     ?person ;          # same holder across both VCs
           ex:licenceValid true .
  FILTER( ?age >= 25 )                         # ← the only age fact revealed: ≥ 25
}

3025 — provable. The age value itself is the private witness and is never disclosed.

fresh proof in your tab (default)

Idle — choose an age and generate a proof.

The composed presentation — per-circuit progress
Idle — run the composition to check eligibility

A full car-hire presentation composes one sub-proof per relation: 2× scan + filter (age ≥ 25) + 2× hidden-issuer + revocation + join (same holder) + holder proof-of-possession. Below, each member shows its honest status. Exactly one of the 8 members — the age-gate filter_int — is proven and verified live in your browser; the other 7 are compiled, gate-counted and proven by the native crate over bb, but are not run in-browser here. A green tick means a real in-tab cryptographic check happened.

  1. 1. Scan · gov-ID graphscan_k2_n64_r8· 34,821 gatesthe age + holder rows genuinely come from the committed gov-ID credentialcomposed (native)
  2. 2. Scan · DVLA graphscan_k2_n64_r8· 34,821 gatesthe holder + validity rows genuinely come from the committed licence credentialcomposed (native)
  3. 3. Filter · age ≥ 25filter_int_d2· 17,416 gatesthe hidden age satisfies ≥ 25 — the only age fact disclosed (proven live here)queued
  4. 4. Hidden issuer · gov-IDhidden_issuer_d4· 16,946 gatesthe gov-ID was signed by a trusted issuer, without revealing which keycomposed (native)
  5. 5. Hidden issuer · DVLAhidden_issuer_d4· 16,946 gatesthe licence was signed by a trusted issuer, without revealing which keycomposed (native)
  6. 6. Revocation · not revokedrevoke_unset_d10· 899 gatesthe licence is not revoked, at a hidden status-list indexcomposed (native)
  7. 7. Join · same holderjoin_eq_na16_nb16· 7,025 gatesboth credentials share one holder, without disclosing whocomposed (native)
  8. 8. Holder · proof of possessionholder_pok· 10,334 gatesthe presenter possesses the bound holder keycomposed (native)

Composed verdict

run the age-gate to reach a verdict

What is revealed vs hidden
The desk sees (public) Stays private
Eligible: true — the holder may hire a carThe exact date of birth and age (only “≥ 25 is true” leaks)
The predicates asked (age ≥ 25; a valid licence; one shared holder)The licence number and every other undisclosed credential field
Commitments C(G) to both credential graphs + a freshness nonceThe holder’s identity (the join value hides behind a commitment)
That both credentials were signed by issuers in the desk’s trusted setWhich specific issuer key signed (on the hidden-issuer path)
What each circuit proves — public vs hidden

A full car-hire presentation composes one sub-proof per relation. Each member binds its statement to a public commitment and proves one SPARQL-shaped primitive over committed credential graphs. Below: what each verifies, what the desk sees (public) versus what stays the holder’s secret (hidden), and — the honest part — whether it runs live in your tab today or is only compiled / composed.

Live vs designed. Exactly one statement is proven live in your browser: the age-gate FILTER (filter_int_d2). The other members are wired (compiled + gate-counted + exercised by native tests) or designed (gadget complete, with a documented open seam) — not run in-browser. The full six-member cross-credential composition has never been assembled and verified as one unit. The deployed demo does not verify any signature, scan, join, revocation or holder key.

filterfilter_int_d2· 17,416 gates
live in your tab

A hidden value satisfies a predicate — here, that the renter’s age is ≥ 25.

Public: challenge, operand_enc, op, bound, expected (the verdict)

Hidden: the value’s exact decimal digits (the age)

Primitive: range / comparison over a hidden integer (in-circuit blake3 token binding)

Status: the age-gate is the ONLY statement proven live in your browser tab (filter_int_d2); d1/d3/d4 are wired

scanscan_k2_n64_r8· 34,821 gates
wired (native)

Every disclosed row genuinely came from a committed credential graph — and no matching row was hidden (completeness).

Public: challenge, per-graph commitments, the BGP pattern, the disclosed rows, row_count, attribution

Hidden: the per-graph row counts and the term encodings

Primitive: set-membership + completeness (BGP triple-pattern scan over committed graphs)

Status: all 8 scan members compile with native prove / verify / tamper tests; not run in-browser

joinjoin_eq_na16_nb16· 7,025 gates
wired (native)

Two credentials share one value at the join slots (one holder across both VCs) — without disclosing who.

Public: challenge, the two graph commitments, a hiding join_commitment, the join slots

Hidden: both graphs’ encodings, the two joined rows, the shared join value, and a blinder

Primitive: equality-join over committed graphs (single-prover; the joined entity never appears in a public input)

Status: all 4 join members compile + tested natively; the cross-credential join is not run in-browser

issuerhidden_issuer_d4· 16,946 gates
designed

A trusted issuer signed the credential — without revealing which authority’s key signed it.

Public: challenge, the signed message m, the trusted-issuer key-set root

Hidden: the issuer public key, the signature, and which key-set member it is

Primitive: in-circuit signature verification + set-membership (Schnorr over Baby-JubJub + hidden-key Merkle membership)

Status: gadget + binding complete; ADDITIVE only and inherits the open soundness audit — clear-key attestation is still mandatory

holderholder_pok· 10,334 gates
designed

The presenter possesses the holder key the credential was issued to (proof of possession).

Public: challenge, the issuer-attested holder_pk_digest

Hidden: the holder secret key and the holder public-key point

Primitive: proof-of-possession / proof-of-knowledge (Baby-JubJub key-pair PoK)

Status: compiled; the in-circuit hidden-key tier is DEFERRED at the manifest seam — the landed binding is the clear-key tier that discloses the holder point

revokerevoke_unset_d10· 899 gates
designed

The licence is NOT revoked, at a hidden status-list index (non-revocation).

Public: challenge, the status-list root, the index_commitment

Hidden: the status-list index, the status bit (= 0), the blinding, and the Merkle siblings

Primitive: non-membership / hidden-index status (Merkle inclusion + bit-unset + index cross-binding)

Status: compiled; the clear-index path still leaks the index unless the committed-index path is used

“Proves” statements are as designed / as landed, not an audited guarantee. The composition verifier is not yet sound (sq-qhy4) and the estate is research-grade and not externally audited — see the honest-limits note below.

The composed circuit family

A full car-hire presentation composes one sub-proof per relation. This page proves the age-gate member live in your tab; the others are real sparq circuits (compiled + gate-counted today) that the native crate proves over bb and that compose into one ProofManifest. Wiring the remaining members into the browser is tracked as a follow-up.

CircuitProvesGatesIn-tab
filter_int_d2age ≥ 25 over the hidden integer age — proven live on this page17,416live
scan_k2_n64_r8BGP scan over each committed credential graph34,821composed
join_eq_na16_nb16both credentials share one holder, without disclosing who7,025composed
hidden_issuer_d4signed by a trusted issuer, without revealing which key16,946composed
revoke_unset_d10the licence is not revoked, at a hidden status-list index899composed
holder_pokthe presenter possesses the bound holder key10,334composed

Gate counts are the measured bb gates snapshot (non-canonical, for scale). All members sit roughly 15× under the bb.js single-thread browser ceiling.

Honest limits — read before you trust this

The age-gate proof on this page is real: a genuine UltraHonk proof of the sparq filter_int circuit, generated and verified by @aztec/bb.js in your browser. The exact age is the private witness and never appears in the public inputs.

On proof size and speed. The proof is inherently KB-scale: UltraHonk is a transparent, no-trusted-setup proof system whose proofs are kilobytes of field elements — not the ~200 byte single proof of a Groth16 SNARK, and there is no compact mode (a sub-KB single proof would need recursive aggregation, which is out of scope here). This page uses the keccak-oracle flavour (verifierTarget: 'evm'), which is ~43% smaller than the default while staying fully zero-knowledge. As a hard guardrail, the demo never uses any *-no-zk flavour (disableZk: true): those strip the ZK masking and would make the private age recoverable in principle. On plain GitHub Pages a service-worker shim is the only lever that unlocks multithreaded proving (timings are non-canonical and host-dependent).

But the sparq ZK estate is research-grade and has NOT been externally audited. No external accredited cryptographer has reviewed any part of sparq’s bespoke cryptography. The verifier-soundness claim rests entirely on sparq’s own internal, single-model self-audits. An external-cryptographer audit is required before any ZK security, privacy or attestation claim may be relied upon in production (tracked as bead sq-qhy4, and as gap CR-G1 in compliance/cryptoreview/).

The published security posture (SECURITY.md) treats the v1 verifier as NOT sound for a relying party. An internal re-audit finds it “sound as landed” under a stated threat model — but that is remediation progress, not a production guarantee, and it is single-model (pending re-review). Known privacy deferrals remain: holder possession is not yet bound to the specific credential, and the status-list IRI/version are disclosed (linkability).

Bottom line: this page demonstrates the flow and the mechanics of a sparq ZK query-proof, with a real proof in your tab. It does not assert that the proof is cryptographically trustworthy for production use. Treat any “verified” result here as a research demonstration.

SECURITY.mdCryptographic review readinessThe Noir circuits

The age-gate proves age ≥ 25 over a hidden date of birth with a real UltraHonk zero-knowledge proof, generated and verified entirely in your browser — the exact age, the licence number, and the holder’s identity are never revealed.

Details & full security caveat

A renter holds two W3C Verifiable Credentials — a gov-ID and a DVLA driving licence. The car-hire desk must learn only that the renter is ≥ 25, holds a valid, non-revoked licence, and that both credentials belong to the same person — and nothing else: not the date of birth, not the licence number, not the identity.

The proof runs in-tab via the third-party bb.js UltraHonk prover over a Noir circuit; the ~MB prover chunk loads only when you run the demo. The v1 verifier is research-grade: sound as landed under its stated threat model, but not externally audited and pending re-review — treat the timings and gate counts as indicative engineering numbers, not an audited cryptographic guarantee. Reproduction commands, the full circuit/public-input contract, and the soundness/linkability discussion live in the crate README and SKILL.md.

sparq-zk on GitHub ZK SKILL.md