We will review how to use the Plonky2 API for recursive circuits to implement a circuit that adds a new hash to an existing chain of hashes, while recursively verifying the chain. This enables the generation of a SNARK proof for a long hash chain, allowing verification of the chain faster than directly computing it. Instead of processing the entire chain, the verifier only needs to handle a compact proof that ensures the integrity of the chain.
The objective is to implement the logic for a recursive hash chain based on the following statement:
Given v1,vt, and a number of steps t, H the Poseidon hash function (SNARK-friendly hash function),
check that vt=H(t∣∣H(t−1∣∣…H(1∣∣v1))).
At each step, the prover will:
Perform one hash computation according to the structure of the hash chain
Recursively verify a SNARK proof that ensures the correctness of all previous steps
Plonky2 mixes STARK proofs which optimize for proof generation time at the expense of proof size, and SNARK proofs which optimize for proof size and verification time. See the Plonky2 deep-dive and the technical paper to learn more about Plonky2.
Here we set up some constants such as the degree of the extension field for the Goldilocks field, the Poseidon hash function Goldilocks field configuration and the field F used by the proof system:
Computing the hash chain
To compute the hash chain, we iteratively hash starting from an initial hash:
We can check the final hash from the proof against the one computed from scratch:
Building the recursive circuit
The circuit consists in setting up the variables and proof’s inputs and outputs on which we will put some constraints and relationships on.
Notably, it ensures that the output hash of the current proof is constructed as the hash of the counter (corresponding to the step t of the hash chain)
concatenated with the input hash of the current proof.
The inputs of the proof for level t correspond to the outputs of the inner proof for level t−1. So that there is only one proof to verify, the inner proof appears as an input of the current proof.
The critical part is the call to the function conditionally_verify_cyclic_proof_or_dummy which adds the verification circuit for the current circuit, allowing to verify the inner proof in-circuit.
When verifying proofs recursively, the circuit should ensure the consistency between the public parameters of the proofs, that is check that:
the counter corresponding to the current step t in the hash chain is decremented when verifying the inner recursive (aka cyclic) proof
the proofs refer to the same initial_hash from the start of the hash chain
when reaching the base proof, the hash should match the initial_hash, otherwise the inner proof’s output hash should match the outer proof input hash
Building the recursive proofs
The first proof doesn’t prove anything but takes the initial hash as a public input, and sets the condition public input for proof recursion to false, so that the circuit stops the recursive verification
When extending a proof, we construct a new proof with the condition for recursively verifying the inner proof to true, set the inner proof as public input to the proof, and set verifier-related information that needs to be included in the circuit to verify recursive proofs.
A useful feature is that updating the proof as the hash chain grows is relatively straightforward: you simply need to continue generating SNARK proofs that verify one or more hash operations on the current state of the hash chain and that the verifier circuit correctly verifies the previous SNARK proof from the prior step. Hence the term incremental in Incrementally Verifiable Computation (IVC).
Benchmarks
On an M2 Pro chip, cargo bench yields the following running times:
Hash Chain Length
Prove
Verify
Native Hash Chain Computation
10
5.8 s
3 ms
8 µs
20
10 s
3 ms
17 µs
30
13 s
3 ms
27 µs
40
17 s
3 ms
36 µs
50
21 s
3 ms
45 µs
100
42 s
3 ms
92 µs
200
80 s
3 ms
182 µs
1000
392 s
3 ms
921 µs
4000
25 min 7 s
3 ms
3.6 ms
Note that the verifier’s run-time cost remains constant because Plonky2 generates SNARK proofs, which can be verified in constant time.
For shorter hash chains (less than ~4000 in length), computing the chain natively is faster than verifying the SNARK proof. However, from a bandwidth perspective, verifying the SNARK proof is more efficient since the intermediate data used in the hash chain doesn’t need to be transmitted, and the SNARK proof is constant-sized (around 43 kB). This is especially useful in blockchain contexts, where validators would otherwise need to download the entire chain to verify it. With recursive SNARKs, validators can be convinced of the chain’s correctness by only receiving a proof for the latest state, without needing the full chain history. This approach is used by the Mina blockchain.
Optimization: several Poseidon calls per circuit
The previous approach is rather inefficient as it does the maximum number of recursion steps. We can instead pack several calls (let’s try one thousand) to the Poseidon hash function per recursion step to amortize the cost of recursion:
With this simple change, we’re proving a hash chain of length 30,000 in 14 seconds!