Skip to content

Commit 5692eaf

Browse files
author
drcapybara
committed
fix constraints and counter, fix toolchain
1 parent 1f102d8 commit 5692eaf

File tree

4 files changed

+133
-37
lines changed

4 files changed

+133
-37
lines changed

Cargo.lock

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ structopt = { version = "0.3.26", default-features = false }
2121
env_logger = "0.11.5"
2222
thiserror = "1.0.63"
2323
criterion = "0.3"
24+
hashbrown = "0.14"
2425

2526
[dev-dependencies]
2627
debug_print = { version = "1.0.0" }

README.md

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ You can download the repo and run the main branch with:
99
cargo test
1010
```
1111

12-
Which is run in release mode by default. This repo requires the nightly toolchain. If you are seeing errors related to:
12+
Which is run in release mode by default. This repo requires the nightly Rust toolchain. If you are seeing errors related to:
1313

1414
```bash
1515
6 | #![feature(specialization)]
@@ -26,7 +26,7 @@ error[E0554]: `#![feature]` may not be used on the stable release channel
2626
| ^^^^^^^^^^^^^^^^^^
2727
```
2828

29-
Then please double check your toolchain. Otherwise, this repo should work out of the box.
29+
Then please double check your toolchain. You may need to run `rustup update nightly` and ensure the `rustc` component is installed. Otherwise, this repo should work out of the box.
3030

3131
You can also run:
3232

@@ -83,9 +83,9 @@ Our approach is to insert the following gates into the circuit with the requisit
8383
│ │
8484
▼ │
8585
+-----------------------------+ │
86-
| 6. process_recursive_layer |──┘
86+
| 6. evaluate_recursive_circuit|──┘
8787
| Handle recursion, verify, |
88-
| and loop through steps. |
88+
| and loop through steps. |
8989
+-----------------------------+
9090
9191
@@ -100,22 +100,22 @@ You can also view a rough sketch of a circuit diagram of the entire setup:
100100

101101

102102
### Initial Setup
103-
- **Counter Initialization**: A counter gate is initialized to track the depth of recursion.
104-
- **Hash Initialization**: A virtual hash target gate is inserted and registered as a public input, marking the starting point of the hash chain.
105-
- **Hash Gate**: An updateable hash gate is added to enable hash updates as the recursion progresses.
103+
- **Counter Initialization**: A counter gate is initialized to track the depth of recursion. The counter starts at 1 after the base case performs the first hash iteration.
104+
- **Hash Initialization**: A virtual hash target gate is inserted and registered as a public input, marking the starting point of the hash chain. The initial hash is set to the zero hash `[F::ZERO; 4]` (all zeros).
105+
- **Hash Gate**: An updateable hash gate is added to enable hash updates as the recursion progresses. The circuit computes `current_hash_out = hash(current_hash_in)`, where `current_hash_in` is either the initial hash (base case) or the previous proof's output hash (recursive case).
106106

107107
### Recursive Hashing
108-
- **Verifier Data Setup**: Circuit common data is prepared, including configuration and partial witnesses required for recursion.
109-
- **Base Case Identification**: A condition is set to identify whether the current computation is the base case or a recursive case.
110-
- **Hash Chain Connection**: The current hash is connected to the previous hash output or set as the initial hash based on whether it's a base or recursive step.
108+
- **Verifier Data Setup**: Circuit common data is prepared, including configuration and partial witnesses required for recursion. This sets up the recursive proof structure using plonky2's cyclic recursion capabilities.
109+
- **Base Case Identification**: A condition flag is set to identify whether the current computation is the base case (`condition=false`) or a recursive case (`condition=true`). In the base case, the hash chain starts with the zero hash. In recursive cases, each proof verifies the previous proof and extends the chain.
110+
- **Hash Chain Connection**: The current hash input is connected to either the previous proof's output hash (when `condition=true`) or the initial hash (when `condition=false`). The circuit ensures proper chaining by verifying the inner cyclic proof and computing the next hash in the sequence.
111111

112112
### Recursive Proof Verification
113-
- **Circuit Building**: The circuit for the current step is built.
114-
- **Proof Generation**: A proof of the correctness of the current hash computation is generated using the circuit data.
115-
- **Proof Verification**: The generated proof is verified to ensure that the hash was computed correctly.
113+
- **Circuit Building**: The circuit for the current step is built with all necessary gates and constraints. The circuit structure remains constant regardless of the number of steps, enabling constant-size proofs.
114+
- **Proof Generation**: A proof of the correctness of the current hash computation is generated using the circuit data. Each recursive step produces a new proof that verifies the previous proof and extends the hash chain by one iteration.
115+
- **Proof Verification**: The generated proof is verified to ensure that the hash was computed correctly. The verification process checks both the proof's validity and that the hash chain was computed correctly by comparing against the expected hash value.
116116

117117
### Final Verification
118-
- **Final Hash Check**: After all recursive steps, the final hash is compared against the expected result to confirm the integrity of the entire hash chain.
118+
- **Final Hash Check**: After all recursive steps, the final hash is compared against the expected result to confirm the integrity of the entire hash chain. The `verify` function checks that `current_hash == hash^counter(initial_hash)` by iteratively hashing the initial hash `counter` times and comparing the result. This provides an additional validation beyond the proof verification itself.
119119

120120
## Usage:
121121

@@ -148,29 +148,41 @@ let (proof, circuit_data) =
148148

149149
// Verify
150150
let verification_result =
151-
<CircuitBuilder<GoldilocksField, D> as HashChain<GoldilocksField, D, C>>::verify(proof, circuit_data);
151+
<CircuitBuilder<GoldilocksField, D> as HashChain<GoldilocksField, D, C>>::verify(proof, &circuit_data);
152152
assert!(verification_result.is_ok());
153153
```
154154

155155
We observe a total uncompressed proof size of 133440 bytes, regardless of number of steps in the chain. We find this is very nice because this number stays the same no matter how many hashes we compute. In theory, recursively verifiable proofs of this nature can compress extremely large computations into a very small space. Think fully-succinct blockchains, in which light clients can verify the entire state of the chain trustlessly by verifying a small and simple proof in trivial amounts of time.
156156

157+
### Public Inputs Structure
158+
159+
The proof's public inputs follow this structure:
160+
- `[0..4]`: Initial hash (4 field elements) - remains constant throughout the chain, set to `[F::ZERO; 4]`
161+
- `[4..8]`: Current hash (4 field elements) - the hash after `counter` iterations
162+
- `[8]`: Counter (1 field element) - the number of hash iterations performed (starts at 1, increments by 1 each recursive step)
163+
- `[9..]`: Verifier data (variable length) - plonky2's verifier circuit data
164+
165+
The verification function validates that `current_hash == hash^counter(initial_hash)`, ensuring the hash chain was computed correctly.
166+
157167
## Benches
158168

159169
This crate uses criterion for formal benchmarks. Bench prover and verifier performance with:
160170

161171
```bash
162172
cargo bench
163173
```
164-
Here are some prelimnary performance metrics observed thus far:
165-
166-
| Circuit depth (steps) | Prover Runtime (s) | Verifier Runtime (ms)| System RAM Used (mb)|
167-
|-----------------------|--------------------|----------------------|---------------------|
168-
| 2 | 3.3680 s | 3.1013 ms | 375.692 |
169-
| 4 | 4.2126 s | 3.1220 ms | 381.536 |
170-
| 8 | 5.7366 s | 3.0812 ms | 392.716 |
171-
| 16 | 8.8146 s | 3.1098 ms | 405.516 |
172-
| 32 | 14.957 s | 3.0865 ms | 417.704 |
173-
| 64 | 27.294 s | 3.1625 ms | 436.424 |
174+
Here are some preliminary performance metrics observed on Apple M4 Pro (Darwin):
175+
176+
| Circuit depth (steps) | Prover Runtime (s) | Verifier Runtime (ms)|
177+
|-----------------------|--------------------|----------------------|
178+
| 2 | 2.629 s | 2.474 ms |
179+
| 4 | 3.163 s | 2.454 ms |
180+
| 8 | 4.196 s | 2.491 ms |
181+
| 16 | 6.474 s | 2.498 ms |
182+
| 32 | 10.77 s | 2.496 ms |
183+
| 64 | 18.78 s | 2.518 ms |
184+
185+
**Key Observation**: Verifier runtime remains constant (~2.5ms) regardless of chain length, demonstrating the succinctness property of recursive proofs. Prover time scales roughly linearly with the number of steps.
174186

175187

176188
## Acknowledgments

src/lib.rs

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ pub trait HashChain<F: RichField + Extendable<D>, const D: usize, C: GenericConf
8383
proof: Proof<F, C, D>,
8484
verifier_data_target: VerifierCircuitTarget,
8585
cyclic_circuit_data: &CircuitMap<F, C, D>,
86+
initial_hash_target: HashOutTarget,
87+
current_hash_out: HashOutTarget,
88+
counter: Target,
8689
) -> Result<Proof<F, C, D>, HashChainError>;
8790

8891
fn configure_layers() -> CommonData<F, D>;
@@ -93,6 +96,9 @@ pub trait HashChain<F: RichField + Extendable<D>, const D: usize, C: GenericConf
9396
common_data: CommonData<F, D>,
9497
cyclic_circuit_data: CircuitMap<F, C, D>,
9598
verifier_data_target: VerifierCircuitTarget,
99+
initial_hash_target: HashOutTarget,
100+
current_hash_out: HashOutTarget,
101+
counter: Target,
96102
steps: usize,
97103
) -> ProofAndCircuitResult<F, C, D>;
98104
}
@@ -143,7 +149,8 @@ where
143149
/// ```
144150
///
145151
/// Following this approach, we can build a properly constrained recursive hash chain
146-
/// circuit. (At least thats the plan!)
152+
/// circuit. The implementation uses plonky2's cyclic recursion to create a constant-size
153+
/// proof that verifies an arbitrary number of hash iterations.
147154
///
148155
/// ## Usage
149156
/// ```
@@ -262,13 +269,22 @@ where
262269
common_data,
263270
cyclic_circuit_data,
264271
verifier_data_target,
272+
initial_hash_target,
273+
current_hash_out,
274+
counter,
265275
steps,
266276
)
267277
}
268278

269279
// Setup the recursive hashes structure by establishing the size of the inputs and outputs
270280
// and connecting them to each other appropriately. Additionally setup the conditional proof
271281
// verification depending on whether we are in the base layer or not.
282+
//
283+
// The circuit connects:
284+
// - initial_hash_target to inner_cyclic_initial_hash (ensures initial hash stays constant)
285+
// - current_hash_in to either inner_cyclic_latest_hash (if condition=true) or initial_hash_target (if condition=false)
286+
// - counter to condition * inner_cyclic_counter + 1 (increments counter when condition=true)
287+
// This ensures proper hash chaining: each recursive step uses the previous proof's output hash as input.
272288
fn setup_recursive_layers(
273289
&self,
274290
builder: &mut CircuitBuilder<F, D>,
@@ -338,11 +354,38 @@ where
338354
proof: ProofWithPublicInputs<F, C, D>,
339355
verifier_circuit_target: VerifierCircuitTarget,
340356
cyclic_circuit_data: &CircuitData<F, C, D>,
357+
initial_hash_target: HashOutTarget,
358+
_current_hash_out: HashOutTarget,
359+
counter: Target,
341360
) -> Result<ProofWithPublicInputs<F, C, D>, HashChainError> {
342361
let mut pw = PartialWitness::new();
343362
pw.set_bool_target(condition, true);
344363
pw.set_proof_with_pis_target(&inner_cyclic_proof_with_pub_inputs, &proof);
345364
pw.set_verifier_data_target(&verifier_circuit_target, &cyclic_circuit_data.verifier_only);
365+
366+
// Set the public inputs from the previous proof to ensure proper chaining
367+
// Public inputs structure: [initial_hash (4), current_hash (4), counter (1), ...verifier_data...]
368+
// Note: The circuit connects these via setup_recursive_layers, but we need to set
369+
// the initial values to match the previous proof's outputs
370+
let prev_initial_hash = &proof.public_inputs[0..4];
371+
let prev_counter = proof.public_inputs[8];
372+
373+
// Set initial hash (should remain constant across all iterations)
374+
for (i, &element) in prev_initial_hash.iter().enumerate() {
375+
pw.set_target(initial_hash_target.elements[i], element);
376+
}
377+
378+
// Set counter to match what the circuit will compute
379+
// The circuit computes: counter = condition * inner_counter + 1
380+
// Since condition=true, this becomes: counter = inner_counter + 1
381+
// We need to set it to prev_counter + 1 to match the circuit's computation
382+
let one = F::ONE;
383+
pw.set_target(counter, prev_counter + one);
384+
385+
// Note: current_hash_out is computed by the circuit from current_hash_in,
386+
// which is connected to inner_cyclic_latest_hash from the previous proof.
387+
// We don't need to set it explicitly - the circuit handles the chaining.
388+
346389
let proof = cyclic_circuit_data.prove(pw)?;
347390
check_cyclic_proof_verifier_data(
348391
&proof,
@@ -362,24 +405,50 @@ where
362405
common_data: CommonCircuitData<F, D>,
363406
cyclic_circuit_data: CircuitData<F, C, D>,
364407
verifier_data_target: VerifierCircuitTarget,
408+
initial_hash_target: HashOutTarget,
409+
current_hash_out: HashOutTarget,
410+
counter: Target,
365411
steps: usize,
366412
) -> Result<(ProofWithPublicInputs<F, C, D>, CircuitData<F, C, D>), HashChainError> {
367413
// Setup the partial witness for the proof, and set the
368-
// initial public input wires with an array of field elements set to
369-
// the empty hash
414+
// initial public input wires with actual hash values
415+
// Use zero hash as the starting point (all zeros)
370416
let mut pw = PartialWitness::new();
371-
let initial_hash = [];
372-
let initial_hash_pub_inputs = initial_hash.into_iter().enumerate().collect();
417+
let initial_hash: [F; 4] = [F::ZERO; 4];
418+
419+
// Set the initial hash in the witness (starting point of the chain)
420+
for (i, &element) in initial_hash.iter().enumerate() {
421+
pw.set_target(initial_hash_target.elements[i], element);
422+
}
423+
424+
// Compute the first hash (base case: hash the initial hash once)
425+
let first_hash = hash_n_to_hash_no_pad::<F, PoseidonPermutation<F>>(&initial_hash);
426+
427+
// Set counter to 1 (we've done one hash iteration)
428+
// Note: current_hash_out will be computed by the circuit from current_hash_in
429+
// In the base case (condition=false), current_hash_in = initial_hash_target
430+
// So current_hash_out = hash(initial_hash) = first_hash
431+
let one = F::ONE;
432+
pw.set_target(counter, one);
373433

374434
// Set the condition wire to false because we are not in the recursive case
375435
// initially
376436
pw.set_bool_target(condition, false);
437+
438+
// Create base proof with proper public inputs: [initial_hash (4), current_hash (4), counter (1)]
439+
let mut base_pub_inputs = Vec::new();
440+
base_pub_inputs.extend_from_slice(&initial_hash);
441+
base_pub_inputs.extend_from_slice(&first_hash.elements);
442+
base_pub_inputs.push(one);
443+
let base_pub_inputs_map: hashbrown::HashMap<usize, F> =
444+
base_pub_inputs.into_iter().enumerate().collect();
445+
377446
pw.set_proof_with_pis_target::<C, D>(
378447
&inner_cyclic_proof_with_pub_inputs,
379448
&cyclic_base_proof(
380449
&common_data,
381450
&cyclic_circuit_data.verifier_only,
382-
initial_hash_pub_inputs,
451+
base_pub_inputs_map,
383452
),
384453
);
385454

@@ -392,32 +461,38 @@ where
392461

393462
// Setup the expected data for the verifier
394463
pw.set_verifier_data_target(&verifier_data_target, &cyclic_circuit_data.verifier_only);
395-
let proof = cyclic_circuit_data.prove(pw)?;
464+
let mut proof = cyclic_circuit_data.prove(pw)?;
396465
check_cyclic_proof_verifier_data(
397466
&proof,
398467
&cyclic_circuit_data.verifier_only,
399468
&cyclic_circuit_data.common,
400469
)?;
401470
cyclic_circuit_data.verify(proof.clone())?;
402471

403-
// Base case of the recursion
404-
let mut proof = Self::prove_current_layer(
472+
// Base case of the recursion - now prove with condition=true to start recursive chaining
473+
proof = Self::prove_current_layer(
405474
condition,
406475
inner_cyclic_proof_with_pub_inputs.clone(),
407476
proof,
408477
verifier_data_target.clone(),
409478
&cyclic_circuit_data,
479+
initial_hash_target,
480+
current_hash_out,
481+
counter,
410482
)?;
411483
cyclic_circuit_data.verify(proof.clone())?;
412484

413-
// Subsequent recursive steps
485+
// Subsequent recursive steps - each step chains the previous hash
414486
for _ in 0..steps {
415487
proof = Self::prove_current_layer(
416488
condition,
417489
inner_cyclic_proof_with_pub_inputs.clone(),
418490
proof,
419491
verifier_data_target.clone(),
420492
&cyclic_circuit_data,
493+
initial_hash_target,
494+
current_hash_out,
495+
counter,
421496
)?;
422497
}
423498

@@ -491,7 +566,7 @@ mod tests {
491566
GoldilocksField,
492567
D,
493568
C,
494-
>>::build_hash_chain_circuit(&mut circuit, 2)
569+
>>::build_hash_chain_circuit(&mut circuit, 128)
495570
.unwrap();
496571

497572
let result =

0 commit comments

Comments
 (0)