Skip to Content
⚠️ This documentation is AI-generated, for personal use only, and is not supported or endorsed by Google.
GuidesEnd-to-end Prover & Verifier

End-to-end Prover & Verifier

This is the wire-up page: given a compiled Circuit<Field>, a filled Dense<Field> witness, and a matching public-input container, what are the minimum calls to produce and verify a proof? Everything in the library composes into this pipeline; everything elsewhere in the docs is one of its inputs.

The two sides

Three pieces flow between the two sides:

  • The compiled circuit (or its 32-byte id, when the verifier already has the bytes cached).
  • The public input — a Dense<Field> of npub_in values.
  • The proof blob — a std::vector<uint8_t> produced by ZkProof::write.

The prover

// 1. Pick the Ligero parameters (out-of-band agreement with the verifier). const size_t rate = 4; const size_t nreq = 128; // 2. Allocate the proof container. ZkProof<Field> zkp(*circuit, rate, nreq); // 3. Build the prover. ReedSolomonFactory rs_factory(field); ZkProver<Field, ReedSolomonFactory> prover(*circuit, field, rs_factory); // 4. Set up randomness. SecureRandomEngine rng; // 5. Build the Fiat-Shamir transcript with a domain separator. const uint8_t init[] = "my-attestation/v1"; Transcript tp(init, sizeof(init) - 1); // 6. Commit, then prove. prover.commit(zkp, W, tp, rng); bool ok = prover.prove(zkp, W, tp); // 7. Serialise the proof. std::vector<uint8_t> proof_bytes; zkp.write(proof_bytes, field);

Everything interesting happens in steps 6 and 7. commit writes the Ligero commitment to the witness into zkp and appends the Merkle root to the transcript. prove runs the sumcheck rounds and opens the Ligero columns, filling out the rest of zkp. write serialises the completed ZkProof to bytes.

The verifier

// 1. Same rate and nreq as the prover. const size_t rate = 4; const size_t nreq = 128; // 2. Allocate and read the proof. ZkProof<Field> zkp(*circuit, rate, nreq); ReadBuffer pbuf{proof_bytes.data(), proof_bytes.size()}; zkp.read(pbuf, field); // 3. Build the verifier. ZkVerifier<Field, ReedSolomonFactory> verifier(*circuit, rs_factory, rate, nreq, field); // 4. Rebuild the transcript with the same domain separator. const uint8_t init[] = "my-attestation/v1"; Transcript tv(init, sizeof(init) - 1); // 5. Verify. bool ok = verifier.verify(zkp, pub, tv);

The verifier’s public input container pub must agree with the first npub_in values of the prover’s witness W. The transcript domain separator must match byte-for-byte; the rate and nreq must match; the circuit (or its id, through the transcript) must match. Any one mismatch flips one byte of challenge, which flips every downstream check.

What flows where

  • Transcript. The Fiat-Shamir state. Seeded with the init byte string (treat it as a per-application domain separator — include a scheme name and a version), then extended by every protocol-level write. See Transcript & Randomness.
  • rate and nreq. Explicit arguments to both ZkProof and ZkVerifier. There is no in-band negotiation: both sides must know the values out-of-band. (rate = 4, nreq = 128) is the v5 preset at 86+ bits of statistical security; (rate = 7, nreq = 132) is the v7 preset at ~109 bits. See Ligero.
  • SecureRandomEngine. The prover needs unbiased entropy to generate zero-knowledge pads. Do not pass a deterministic RNG in production — the pads are what keep the witness hidden. The verifier never consumes randomness.
  • Proof blob. A ZkProof serialises to a single std::vector<uint8_t> via write(); the inverse is read(). The blob carries the commitment root, the sumcheck polynomials, and the Ligero opening responses. It does not carry the circuit or its id — the transcript binding assumes both sides already agree on which circuit is in play.

Minimum working diff against zk_test.cc

The smallest runnable example in the upstream tree is lib/zk/zk_test.cc. It differs from the sketch above in two places:

  • It builds the circuit inline rather than reading it from bytes. Swap that out for CircuitReader::from_bytes to get the cached-blob flow.
  • It uses a fixed-byte transcript init string. In production, include a version tag so a rotated protocol does not accidentally accept old proofs.

Pitfalls to watch for

  • Mismatched transcript init on prover and verifier. Proof fails verification with an opaque error. Treat the init string as part of your protocol specification and version it.
  • Reusing one SecureRandomEngine across threads without synchronisation. Standard thread-safety rules apply; the library does not serialise access for you.
  • Forgetting to bind the circuit id into the transcript. The ZkProver and ZkVerifier write it for you as part of commit / verify; do not try to do it manually or you will double-write.
  • Shipping proofs with rate / nreq embedded in the blob. They are not. If the verifier uses different values, every query lands in the wrong place and verification silently fails. Pin them in your protocol header.
Last updated on