Proof of Commitment

In Getting Started with zk-SNARKs, we wrote our first ZK‑SNARK circuit to prove that we know the secret preimage of a public hash without revealing the secret itself.

Concretely, the statement we proved was essentially:

Statement (naïve): I know a secret such that H(secret) = hash.

While this already captures a very important zero‑knowledge idea, it has a subtle but serious practical limitation. Once a proof for a given secret is published, anyone can copy and replay that same proof to convince a verifier again.

This is not a flaw of SNARKs. It is a direct consequence of two of their defining properties:

  • Non‑interactivity: the proof is a static object that can be posted or transmitted.
  • Public verifiability: anyone who sees the proof and the public inputs can verify it.

Together, these properties imply that a valid proof is a reusable artifact. If the underlying statement never changes, the same proof will remain valid forever.

To address this problem, we introduce a new concept called context binding


Context Binding

What we really want in many applications is not just:

“I know this secret.”

but rather:

“I know this secret right now, for this specific request, action, or session.”

In other words, we want each proof to be fresh and context‑dependent, while still being tied to the same long‑term secret.

The standard cryptographic trick to achieve this is to introduce a nonce (or challenge): a fresh value that changes every time a proof is generated.

So instead of proving:

Statement (static): I know a secret such that C(secret) = true

we now prove:

Statement (dynamic): I know a secret such that C(secret, nonce) = true

where:

  • secret is the same long‑term private witness as before
  • nonce is a fresh public value chosen by the verifier or the surrounding protocol

The crucial idea is that the circuit must cryptographically bind the secret to the nonce. This ensures that:

  • a proof generated for one nonce cannot be reused for another nonce
  • replaying an old proof automatically fails if the nonce has changed

This turns a static proof into a non‑interactive challenge–response protocol.


Defining Proof of Commitment

With Proof of Commitment, the prover demonstrates knowledge of a secret preimage corresponding to a public hash value, without revealing the secret itself, and with context binding to prevent replay attacks.

More precisely, the prover:

  • keeps secret as a private input
  • takes a fresh nonce as a public input
  • produces two public outputs:
    • secretHash = Poseidon(secret)
    • authHash = Poseidon(secret, nonce)

Intuitively:

  • secretHash plays the same role as in the first tutorial: it is a public commitment to the secret
  • authHash is the context binding value that binds the secret to the nonce, making the proof instance‑specific

Using the ZK‑Toolbox (Developer View)

Let’s now look at how this idea is exposed at the API level using our zk‑toolbox library.

Step 1: Generate Inputs

We generate:

  • a random secret (private)
  • a random nonce (public)
import { ProofOfCommitment, randomBigInt32ModP, poseidon } from "@prifilabs/zk-toolbox";

const privateInputs = { secret: randomBigInt32ModP() };
const publicInputs  = { nonce:  randomBigInt32ModP() };

Step 2: Generate the Proof

const proofOfCommitment = new ProofOfCommitment();
const { proof, publicOutputs } = await proofOfCommitment.generate(privateInputs, publicInputs);

At this point we have:

  • a SNARK proof object
  • two public outputs: secretHash and authHash

Step 3: (Optional) Verify Output Correctness Locally

This step is not required for cryptographic verification, but it is extremely useful for debugging, building intuition, and writing unit tests.

const secretHash = poseidon([ privateInputs.secret ]);
console.assert(publicOutputs.secretHash == secretHash);

const authHash = poseidon([ privateInputs.secret, publicInputs.nonce ]);
console.assert(publicOutputs.authHash == authHash);

Step 4: Verify the Proof

Only the following values are needed for verification:

  • the SNARK proof
  • the public input (nonce)
  • the public outputs (secretHash, authHash)

The private input (secret) is never revealed.

const res = await proofOfCommitment.verify(proof, publicInputs, publicOutputs);
console.assert(res);

Behind the Scenes: The CIRCOM Circuit

Here is the full circuit used by Proof of Commitment:

pragma circom 2.0.0;

include "../node_modules/circomlib/circuits/poseidon.circom";

template ProofOfCommitment() {
    // private inputs
    signal input secret;
  
    // public inputs
    signal input nonce;
  
    // public outputs
    signal output secretHash;
    signal output authHash;
  
    // computes secretHash as Poseidon(Secret)
    component secretHasher = Poseidon(1);
    secretHasher.inputs <== [secret];
    secretHash <== secretHasher.out;
  
    // computes authHash as Poseidon(Secret, nonce)
    component authHasher = Poseidon(2);
    authHasher.inputs <== [secret, nonce];
    authHash <== authHasher.out;
}

component main {public [nonce]} = ProofOfCommitment();

How the nonce ensures context binding

Suppose an attacker observes a valid triple:

  • (proof, nonce, authHash)

If the attacker tries to reuse the same proof with a different nonce nonce', thus the circuit constraints will no longer be satisfied and the SNARK verification will fail.

Why? Because authHash would have to equal:

Poseidon(secret, nonce')

but the attacker does not know secret and therefore cannot recompute a valid value for authHash.


Conceptual Interpretation

This construction can be seen as a zero‑knowledge analogue of:

  • a MAC (message authentication code)
  • a challenge–response authentication protocol

except that:

  • the secret key is never revealed
  • the computation is enforced inside a SNARK circuit
  • the verifier only sees commitments and proofs

Conclusion and Applications

Proof of Commitment allows us to:

  • prove knowledge of a secret preimage
  • generate many fresh proofs for the same secret
  • cryptographically bind each proof to a specific nonce

Example Applications

  1. Password‑style authentication (ZK login)
    A server stores only secretHash. A client proves knowledge of secret for a fresh server‑provided nonce but never send the password to the prover as done in classic password-based authentication scheme.

  2. Smart‑contract authorization
    A contract stores secretHash. A user submits transactions authenticated by a ZK proof. Each transaction includes a fresh nonce recorded on‑chain to prevent replay.

  3. Session binding
    The nonce can encode a timestamp, a session identifier, a transaction hash, an action label, ensuring that each proof is valid only for one specific context.


In our next chapter Proof of Membership, we generalize this construction to show how a prover can demonstrate knowledge of one secret among a collection of committed hashes, without disclosing which specific commitment corresponds to the known secret.