Demo only
// nullifiers ·

Claim once.

// live demo

Mint a secret. Claim it. Try to claim it twice.

Every hash below is real SHA-256 from crypto.subtle.digest. Secrets are random 32-byte demo values created in this tab.

1

Your wallet: mint secret tickets

Each ticket has a random secret s that only its holder knows. The set stores only the commitment C = SHA-256(s). The holder can also compute the nullifier N = SHA-256(s || "nullifier") but nobody sees it until a claim.

2

Claim

Use the Claim button on a ticket, press it again to attempt a double-claim, or try a forged secret. Only the nullifier is published, never s.

Honest note: this demo has no zero-knowledge proof. To check membership, the page itself recomputes C from the secret, so the page can link the two. A real system replaces that check with a ZK proof so the verifier learns neither s nor C. See "How it works".
3

The public ledger: what everyone sees

Two lists and a log. No secrets, no ticket numbers, no link between the lists. Both lists are shown sorted by value, not in time order, so list position does not leak mint or claim order.

Commitment set

merkle root: (empty)

    Spent nullifiers

    0 spent
      (no claims yet)
      4

      Observer: try to link a nullifier to a commitment

      An observer holds both public lists. This tries four obvious ways to connect a spent nullifier to a commitment by hashing, on every pair. A real link needs the secret s, which means inverting SHA-256.

      // how it works

      Prove you hold a ticket without showing which.

      Commitment

      C = SHA-256(s) goes in a public set. It reveals nothing about s because SHA-256 cannot be run backwards.

      Nullifier

      N = SHA-256(s || "nullifier") is the same for the same secret every time, so a second claim collides with the first and is rejected. It looks unrelated to C.

      Unlinkable

      Anyone can see N was spent. Nobody can tell which C it came from, so they cannot tell who claimed.

      Private (witness)

      The secret s and the Merkle path showing its commitment sits in the set.

      Public inputs

      The set's Merkle root R and the nullifier N.

      What the ZK proof asserts

      "I know an s such that C = H(s) is a leaf under R and N = H(s || "nullifier")." The verifier checks the proof and that N is not already spent. It never learns s, C, or which leaf.

      Hidden (in a real build)

      • Which commitment was claimed
      • The secret itself
      • Any link between a claim and the earlier mint

      Public

      • That some valid claim happened, and its nullifier
      • When it happened, and which account paid the fee
      • The whole commitment set and its root

      About the primitives. This demo uses plain SHA-256. A Solana build would use a ZK-friendly hash (e.g. Poseidon) inside a Groth16 or PLONK circuit, with the nullifier stored in an on-chain account or bitmap so a second claim fails. Web Crypto has no ZK prover, so none is shown here.

      Known limitations — please read

      • No ZK proof in this demo. The page checks membership by hashing your secret directly, so it can link a claim to a commitment. Only the public ledger view is unlinkable by construction.
      • Reduces linkability, not anonymity. Timing, the fee-paying account, IP addresses and claim order can all re-identify a claimer even when the nullifier reveals nothing.
      • Demo secrets only. Never paste a real seed phrase or private key. The secrets here are random throwaway values shown on screen.
      • Small anonymity set. A set with few commitments hides you among few. Privacy grows with the number of honest tickets.
      • State lives in this tab. Reload and everything is gone. Nothing is stored or sent.
      • Unaudited, no token. No program is deployed and no audit exists. The $NULL ticker is a placeholder only. Not connected to mainnet or real funds.