Skip to content

Designing circuits for ProveKit

ProveKit doesn’t constrain how you build circuits; that’s Noir’s job. But a handful of mechanics are specific to this proof system, and getting them right saves real debugging time.

ProveKit uses hashes in two independent places:

  • Inside the circuit, whatever your Noir code calls (poseidon2::bn254::hash_*, sha256, etc.).
  • Outside the circuit, for Merkle commitments and the Fiat-Shamir transcript, controlled by prepare --hash.

They are independent choices. The default prepare --hash skyscraper works for almost every circuit. Skyscraper is ProveKit’s custom BN254-tuned hash engine (in skyscraper/), with SIMD-accelerated arithmetic on aarch64 and the smallest transcript-side overhead of the options. Override --hash only when a downstream consumer (an EVM contract checking the transcript hash, for example) demands a specific algorithm.

Available --hash values: skyscraper (default), sha256, keccak, blake3, poseidon2.

Public inputs are exposed to the verifier and are part of the artifact ABI. Two things follow:

1. Order matters. Reordering public inputs in the Noir signature changes the ABI and invalidates every existing .pkv and proof. Treat the public-input list like a versioned API.

2. Verification ≠ authorization. A successful verify() says the proof is valid against these public inputs. Your application code must then check that “these” are the ones it expected: same election ID, same chain, same account. Skipping this check is the most common ZK-app authorization bug.

After verify() returns success:

let public_inputs = proof.public_inputs.0;
assert_eq!(public_inputs[0], expected_election_id, "wrong election");
assert_eq!(public_inputs[1], expected_chain_id, "wrong chain");

To confirm what your circuit actually exposes:

Terminal window
provekit-cli show-inputs --hex circuit.pkv proof.np

Record this output alongside released artifacts. It’s the ground truth of what your verifier sees.

ProveKit’s native Verify::verify(&mut self, ...) consumes internal verifier state on each call. To verify multiple proofs in one process, reload or clone fresh verifier state per attempt:

// Reload for each proof.
let mut verifier: Verifier = read(pkv_path)?;
verifier.verify(&proof_a)?;
let mut verifier: Verifier = read(pkv_path)?;
verifier.verify(&proof_b)?;

The error message on the second use of a stale verifier is Verifier has already been consumed; cannot verify twice. Each host wraps this differently (Verity’s JS/Swift/Kotlin SDKs hide the consume-on-use detail behind their own scheme objects); plan the lifecycle explicitly per platform guide.

ProveKit doesn’t need a circuit-version field in your proof. The .pkp and .pkv pair is bound to the exact circuit, branch, hash choice, and lockfile from one prepare run. Cross-circuit replay is impossible: a proof made with circuit_v1.pkp cannot verify against circuit_v2.pkv, even if both came from “the same Noir source” with a comma changed.

This means:

  • You cannot accidentally accept a proof from a different circuit version.
  • You can accidentally deploy new artifacts without matching the proofs already in flight. Always match .pkv to the proof set being verified.

See Artifact lifecycle for the full pairing rules.

provekit-cli generate-gnark-inputs produces params_for_recursive_verifier and r1cs.json from a matching .pkv and proof. When wrapped in Groth16 by the Go/gnark recursive verifier, those public inputs become the on-chain public inputs of the outer proof. Two implications:

  • Anything in public inputs gets stored on-chain after Groth16 wrapping. Don’t put privacy-sensitive values there.
  • Match the --hash between base proof and recursive verifier expectations. If the recursive verifier was built against a different transcript hash, the wrap fails.

The patterns below show up in every ZK application. They aren’t ProveKit-specific, so we link rather than duplicate:

  • Merkle membership proofs, prove “this leaf is in a set with this root.” See the Aztec Noir tutorials and the noir-passport example for production-scale instances.
  • Nullifiers, public, deterministic, one-way functions of a secret bound to a context, used to prevent replay. Common in vote schemes, airdrops, and credential systems. See ZK Hack write-ups and the Aztec privacy docs for canonical treatments.
  • Hash selection for in-circuit work, Poseidon2 is cheapest in-circuit on BN254; SHA-256 and Keccak cost roughly 100× more constraints but match what EVM contracts already expect. See the Ethproofs CSP benchmarks for measured comparisons across hashes and proof systems.

If your circuit pulls one of these patterns from a published library (e.g., a Noir Merkle tree crate, a nullifier helper), prefer the published implementation over copying a sketch from any documentation, including this site.