Tutorial: prove without revealing
This tutorial walks you through the smallest meaningful zero-knowledge application: a prover convinces a verifier that they know a secret without revealing it. You’ll write the Noir circuit, prepare keys, generate a proof, and verify it from your own Rust program.
By the end you’ll understand the full ProveKit loop and be ready to apply it to real workloads like credential verification, vote eligibility, or off-chain computation attestation.
Time: about 20 minutes. Prerequisites: a working ProveKit checkout, see Installation.
What you’ll build
Section titled “What you’ll build”A circuit that proves “I know two field elements whose Poseidon2 hash equals this public commitment”, and a Rust program that loads the resulting proof, verifies it, and prints the public commitment.
In a real application the “two field elements” might be the components of a private identity key; the “public commitment” is the hash that gets stored on-chain or in a server. Verification proves the prover holds the secret without revealing it.
Step 1: scaffold the Noir circuit
Section titled “Step 1: scaffold the Noir circuit”-
Create a new Noir package inside the repo for this tutorial.
Terminal window cd noir-examplesmkdir secret-knowledge && cd secret-knowledge -
Add a
Nargo.tomlthat pulls in theposeidon2dependency.Nargo.toml [package]name = "secret_knowledge"type = "bin"authors = [""]compiler_version = ">=0.22.0"[dependencies]poseidon2 = { tag = "v0.5.0-beta.0", git = "https://github.com/TaceoLabs/noir-poseidon", directory = "poseidon2" } -
Write the circuit. Public inputs go in the function signature after private ones, but you mark public inputs with the
pubkeyword.src/main.nr use dep::poseidon2;fn main(secret: [Field; 2], commitment: pub Field) {let computed = poseidon2::bn254::hash_2(secret);assert(computed == commitment);}This circuit enforces one constraint: that
poseidon2(secret) == commitment. Becausesecretis private andcommitmentis public, the resulting proof says “the prover knows a secret that hashes to commitment”, and nothing else.
Step 2: pick inputs
Section titled “Step 2: pick inputs”You need a valid (secret, commitment) pair where commitment == poseidon2(secret). Computing Poseidon2 outside the circuit takes an off-circuit hash implementation; for this tutorial, reuse a precomputed pair from noir-examples/basic:
secret = ["1", "2"]commitment = "0x0e90c132311e864e0c8bca37976f28579a2dd9436bbc11326e21ec7c00cea5b2"That commitment is Poseidon2(BN254) of [1, 2], already verified by the canonical basic example. Once you’ve completed the tutorial, swap in your own (secret, commitment) pair generated however you like.
Then compile the circuit:
cargo run --release --bin provekit-cli -- prepareprepare reads only the Noir source, not Prover.toml. It writes secret_knowledge.pkp (prover key) and secret_knowledge.pkv (verifier key) into the package directory.
Step 3: prove and verify with the CLI
Section titled “Step 3: prove and verify with the CLI”-
Generate a proof. ProveKit reads the prover key (
.pkp) and your inputs, and writesproof.np.Terminal window cargo run --release --bin provekit-cli -- prove -
Verify it locally, this is the fastest sanity check before integrating elsewhere.
Terminal window cargo run --release --bin provekit-cli -- verifyExit code
0means the proof verifies. If you see a failure, the most common cause is a stale.pkp/.pkvpair, re-runprepare. -
Inspect the public inputs the proof actually exposes.
Terminal window cargo run --release --bin provekit-cli -- show-inputs --hex \secret_knowledge.pkv \proof.npYou’ll see only
commitment, the secret never appears.
Step 4: verify from your own Rust program
Section titled “Step 4: verify from your own Rust program”The CLI is great for development. In production, the verifier usually runs inside your code, a backend service, a smart-contract gateway, a CLI tool. Let’s verify the same proof from a standalone Rust binary.
-
Create a new Cargo project alongside (not inside) the repo.
Terminal window cd ../.. # back to repo rootcd .. # leave the provekit directorycargo new --bin secret-verifiercd secret-verifier -
Add the dependencies.
Cargo.toml [package]name = "secret-verifier"version = "0.1.0"edition = "2021"[dependencies]provekit-common = "1.0.0"provekit-verifier = "1.0.0"anyhow = "1" -
Write the verifier.
src/main.rs use std::path::Path;use provekit_common::{file::read, NoirProof, Verifier};use provekit_verifier::Verify;fn main() -> anyhow::Result<()> {// Adjust these paths to wherever you ran `prepare`/`prove`.let pkv_path = Path::new("../provekit/noir-examples/secret-knowledge/secret_knowledge.pkv");let proof_path = Path::new("../provekit/noir-examples/secret-knowledge/proof.np");let mut verifier: Verifier = read(pkv_path)?;let proof: NoirProof = read(proof_path)?;verifier.verify(&proof)?;println!("Proof verified. Prover knows a Poseidon2 preimage of the published commitment.");Ok(())} -
Run it.
Terminal window cargo run --releaseYou should see
Proof verified.
What you proved
Section titled “What you proved”A user with a Prover.toml can convince anyone with the matching .pkv and proof.np that they know two field elements that hash to the commitment, without ever transmitting those elements.
That’s the whole zero-knowledge claim. Everything else in ProveKit is plumbing around this shape: bigger circuits, more complex public input contracts, different hash configurations, different transport layers.
Clean up
Section titled “Clean up”cd ../provekit/noir-examples/secret-knowledgerm -f secret_knowledge.pkp secret_knowledge.pkv proof.nprm -rf targetWhere to go next
Section titled “Where to go next”- Proving flow, the conceptual pipeline behind what you just ran.
- Artifact lifecycle, the rules for treating
.pkp/.pkv/proof.npas deployment artifacts. - Integrations overview, pick the host language where ProveKit will run in your system.
- Designing circuits for ProveKit, ProveKit-specific mechanics for shaping your circuit.