We’re using Groth16 zkSNARKs with a fixed secret witness (it only depends on either the PoL winning note or the core node state), and we need to output up to some fixed quota of distinct proofs for that same witness.
To enforce the quota, the circuit takes an index and hashes it together with a secret key (from the core node or the PoL winning note) to derive a per-proof selection randomness. That randomness is then used as the preimage of a proof nullifier. The circuit also enforces index < quota as a public constraint, so you can’t mint more than quota (that depends if it’s a leader or a core node) valid proofs for a given note / node. Each proof reveals a different nullifier, and we can track nullifiers to detect quota violations.
The issue is obvious: generating these proofs one-by-one is slow. Almost everything we do is identical across proofs, because the “you won the PoL” / “you are a core node” part is unchanged. The only moving parts are the index, the derived selection randomness, and the resulting nullifier. Running full witness computation + full proving X times means paying the big cost X times.
The prover outputs three group elements ($[A]_1, [B]_1, [B]_2$). Each is basically:
Example (schematically):
$$ [A]_1 = [\alpha]1 + \sum{i=1}^m a_i [u_i(\tau)]_1 + r[\delta]_1 $$
The key detail is that these proof elements are linear combinations of witness values with a fixed basis. MSMs are the bottleneck. So if 90–95% of the witness values are identical across proofs, we’re repeatedly doing the same giant MSM.
Split the proving work into:
index < quota check + selection randomness derivation + nullifier computation.Compute the common part once, and then for each proof only recompute the tiny index-dependent delta.
Circom’s witness calculator evaluates all signals from inputs. In our circuit, the expensive stuff (Merkle paths, Poseidon hashes of the secret key, lottery evaluation, etc.) does not change with the index. Only the signals “downstream” of the index input change.
So the approach is:
index = 0 and cache all signal values.