Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
plonky2 = { git = "https://github.com/mir-protocol/plonky2", branch = "semaphore-example" }
anyhow = "1.0.56"
plonky2 = { git = "https://github.com/mir-protocol/plonky2" }
anyhow = "1.0.56"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Plonky2 implementation of the [Semaphore protocol](http://semaphore.appliedzkp.org/)
# Plonky2 implementation of the [Semaphore protocol](https://semaphore.pse.dev)

Used as an example in the ZKHack Plonky2 presentation.

## Compilation
```bash
rustup override set nightly # Requires nightly Rust
cargo test --release
```
```
6 changes: 4 additions & 2 deletions src/access_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::signal::{Digest, Signal, C, F};
pub struct AccessSet(pub MerkleTree<F, PoseidonHash>);

impl AccessSet {
// Verify the plonky2 proof of the given nullifier (in the signal structure) and topic.
pub fn verify_signal(
&self,
topic: Digest,
Expand All @@ -34,6 +35,7 @@ impl AccessSet {
})
}

// Generate the plonky2 proof for the given key pair and topic.
pub fn make_signal(
&self,
private_key: Digest,
Expand All @@ -46,7 +48,7 @@ impl AccessSet {
let mut pw = PartialWitness::new();

let targets = self.semaphore_circuit(&mut builder);
self.fill_semaphore_targets(&mut pw, private_key, topic, public_key_index, targets);
self.fill_semaphore_targets(&mut pw, private_key, topic, public_key_index, targets)?;

let data = builder.build();
let proof = data.prove(pw)?;
Expand All @@ -56,7 +58,7 @@ impl AccessSet {
nullifier,
proof: proof.proof,
},
data.to_verifier_data(),
data.verifier_data(),
))
}
}
37 changes: 27 additions & 10 deletions src/circuit.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use plonky2::field::field_types::Field;
use plonky2::hash::hash_types::{HashOutTarget, MerkleCapTarget};
use anyhow::Result;
use plonky2::field::types::Field;
use plonky2::hash::hash_types::HashOutTarget;
use plonky2::hash::merkle_proofs::MerkleProofTarget;
use plonky2::hash::poseidon::PoseidonHash;
use plonky2::iop::target::Target;
use plonky2::iop::witness::{PartialWitness, Witness};
use plonky2::iop::witness::{PartialWitness, WitnessWrite};
use plonky2::plonk::circuit_builder::CircuitBuilder;

use crate::access_set::AccessSet;
Expand All @@ -23,6 +24,19 @@ impl AccessSet {
}

pub fn semaphore_circuit(&self, builder: &mut CircuitBuilder<F, 2>) -> SemaphoreTargets {
// To create the circuit 'values', we call the `add_virtual_target` and it's variants (ie.
// `add_virtual_target_arr`, or `add_virtual_hash` which internally calls it for the 4
// field values that conform the hash.
// The method `add_virtual_target` keeps an incremental index for each new virtual target
// created, and returns a new `VirtualTarget`, used for intermediate values in witness
// generation (which when needed can be copied to specific witness location (Wire)).
//
// The `register_public_input` and it's variants, append to the CircuitBuilder's
// `public_inputs` vector the given Target (either VirtualTarget or Wire).
//
// Notice that when created, the targets are not given any value, and they will be set later
// at the method `fill_semaphore_targets`.

// Register public inputs.
let merkle_root = builder.add_virtual_hash();
builder.register_public_inputs(&merkle_root.elements);
Expand All @@ -44,7 +58,7 @@ impl AccessSet {
builder.verify_merkle_proof::<PoseidonHash>(
[private_key, [zero; 4]].concat(),
&public_key_index_bits,
&MerkleCapTarget(vec![merkle_root]),
merkle_root,
&merkle_proof,
);

Expand All @@ -64,14 +78,16 @@ impl AccessSet {
}
}

// Fill the semaphore targets that we defined at the method `semaphore_circuit` with the given
// values.
pub fn fill_semaphore_targets(
&self,
pw: &mut PartialWitness<F>,
private_key: Digest,
topic: Digest,
public_key_index: usize,
targets: SemaphoreTargets,
) {
) -> Result<()> {
let SemaphoreTargets {
merkle_root,
topic: topic_target,
Expand All @@ -80,21 +96,22 @@ impl AccessSet {
public_key_index: public_key_index_target,
} = targets;

pw.set_hash_target(merkle_root, self.0.cap.0[0]);
pw.set_targets(&private_key_target, &private_key);
pw.set_targets(&topic_target, &topic);
pw.set_hash_target(merkle_root, self.0.cap.0[0])?;
pw.set_target_arr(&private_key_target, &private_key)?;
pw.set_target_arr(&topic_target, &topic)?;
pw.set_target(
public_key_index_target,
F::from_canonical_usize(public_key_index),
);
)?;

let merkle_proof = self.0.prove(public_key_index);
for (ht, h) in merkle_proof_target
.siblings
.into_iter()
.zip(merkle_proof.siblings)
{
pw.set_hash_target(ht, h);
pw.set_hash_target(ht, h)?;
}
Ok(())
}
}
112 changes: 97 additions & 15 deletions src/recursion.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use plonky2::iop::witness::{PartialWitness, Witness};
use anyhow::Result;
use plonky2::iop::witness::{PartialWitness, WitnessWrite};
use plonky2::plonk::circuit_builder::CircuitBuilder;
use plonky2::plonk::circuit_data::{CircuitConfig, VerifierCircuitData, VerifierCircuitTarget};
use plonky2::plonk::circuit_data::{CircuitConfig, VerifierCircuitData};
use plonky2::plonk::proof::ProofWithPublicInputs;

use crate::access_set::AccessSet;
Expand All @@ -14,7 +15,7 @@ impl AccessSet {
topic1: Digest,
signal1: Signal,
verifier_data: &VerifierCircuitData<F, C, 2>,
) -> (Digest, Digest, PlonkyProof) {
) -> Result<(Digest, Digest, PlonkyProof, VerifierCircuitData<F, C, 2>)> {
let config = CircuitConfig::standard_recursion_zk_config();
let mut builder = CircuitBuilder::new(config);
let mut pw = PartialWitness::new();
Expand All @@ -38,40 +39,121 @@ impl AccessSet {
.chain(topic1)
.collect();

// `add_virtual_proof_with_pis` is an extended version of the `add_virtual_target`, but
// that takes care of adding all the values of the proof and the public inputs (pis).
let proof_target0 = builder.add_virtual_proof_with_pis(&verifier_data.common);
// set the public inputs
builder.register_public_inputs(&proof_target0.public_inputs);
// `set_proof_with_pis_target` is an extended version of the `set_target`, but that takes
// care of adding all the values of the proof and the public inputs.
pw.set_proof_with_pis_target(
&proof_target0,
&ProofWithPublicInputs {
proof: signal0.proof,
public_inputs: public_inputs0,
},
);
)?;
// add & set the verifier data
let vd_target =
builder.add_virtual_verifier_data(verifier_data.common.config.fri_config.cap_height);
pw.set_verifier_data_target(&vd_target, &verifier_data.verifier_only)?;

// now, the same as we did with the proof0, with the proof1 related values:
let proof_target1 = builder.add_virtual_proof_with_pis(&verifier_data.common);
builder.register_public_inputs(&proof_target1.public_inputs);
pw.set_proof_with_pis_target(
&proof_target1,
&ProofWithPublicInputs {
proof: signal1.proof,
public_inputs: public_inputs1,
},
);
)?;
let vd_target =
builder.add_virtual_verifier_data(verifier_data.common.config.fri_config.cap_height);
pw.set_verifier_data_target(&vd_target, &verifier_data.verifier_only)?;

let vd_target = VerifierCircuitTarget {
constants_sigmas_cap: builder
.add_virtual_cap(verifier_data.common.config.fri_config.cap_height),
};
pw.set_cap_target(
&vd_target.constants_sigmas_cap,
&verifier_data.verifier_only.constants_sigmas_cap,
);
)?;

builder.verify_proof(proof_target0, &vd_target, &verifier_data.common);
builder.verify_proof(proof_target1, &vd_target, &verifier_data.common);
builder.verify_proof::<C>(&proof_target0, &vd_target, &verifier_data.common);
builder.verify_proof::<C>(&proof_target1, &vd_target, &verifier_data.common);

let data = builder.build();
let recursive_proof = data.prove(pw).unwrap();
let recursive_proof = data.prove(pw)?;

data.verify(recursive_proof.clone())?;

Ok((
signal0.nullifier,
signal1.nullifier,
recursive_proof.proof,
data.verifier_data(),
))
}
}

#[cfg(test)]
mod tests {
use anyhow::Result;
use plonky2::field::types::{Field, Sample};
use plonky2::hash::merkle_tree::MerkleTree;
use plonky2::hash::poseidon::PoseidonHash;
use plonky2::plonk::config::Hasher;
use plonky2::plonk::proof::ProofWithPublicInputs;

use crate::access_set::AccessSet;
use crate::signal::{Digest, F};

#[test]
fn test_recursion() -> Result<()> {
let n = 1 << 20;
let private_keys: Vec<Digest> = (0..n).map(|_| F::rand_array()).collect();
let public_keys: Vec<Vec<F>> = private_keys
.iter()
.map(|&sk| {
PoseidonHash::hash_no_pad(&[sk, [F::ZERO; 4]].concat())
.elements
.to_vec()
})
.collect();
let access_set = AccessSet(MerkleTree::new(public_keys, 0));

// first proof
let i0 = 12;
let topic0 = F::rand_array();
let (signal0, vd0) = access_set.make_signal(private_keys[i0], topic0, i0)?;
access_set.verify_signal(topic0, signal0.clone(), &vd0)?;

data.verify(recursive_proof.clone()).unwrap();
// second proof
let i1 = 42;
let topic1 = F::rand_array();
let (signal1, vd1) = access_set.make_signal(private_keys[i1], topic1, i1)?;
access_set.verify_signal(topic1, signal1.clone(), &vd1)?;

// generate recursive proof
let (nullifier0, nullifier1, recursive_proof, vd2) =
access_set.aggregate_signals(topic0, signal0, topic1, signal1, &vd0)?;

// verify recursive proof
let public_inputs: Vec<F> = access_set
.0
.cap
.0
.iter()
.flat_map(|h| h.elements)
.chain(nullifier0)
.chain(topic0)
.chain(access_set.0.cap.0.iter().flat_map(|h| h.elements))
.chain(nullifier1)
.chain(topic1)
.collect();

(signal0.nullifier, signal1.nullifier, recursive_proof.proof)
vd2.verify(ProofWithPublicInputs {
proof: recursive_proof,
public_inputs,
})?;
Ok(())
}
}
8 changes: 5 additions & 3 deletions src/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub struct Signal {
#[cfg(test)]
mod tests {
use anyhow::Result;
use plonky2::field::field_types::Field;
use plonky2::field::types::{Field, Sample};
use plonky2::hash::merkle_tree::MerkleTree;
use plonky2::hash::poseidon::PoseidonHash;
use plonky2::plonk::config::Hasher;
Expand All @@ -27,7 +27,7 @@ mod tests {
#[test]
fn test_semaphore() -> Result<()> {
let n = 1 << 20;
let private_keys: Vec<Digest> = (0..n).map(|_| F::rand_arr()).collect();
let private_keys: Vec<Digest> = (0..n).map(|_| F::rand_array()).collect();
let public_keys: Vec<Vec<F>> = private_keys
.iter()
.map(|&sk| {
Expand All @@ -39,9 +39,11 @@ mod tests {
let access_set = AccessSet(MerkleTree::new(public_keys, 0));

let i = 12;
let topic = F::rand_arr();
let topic = F::rand_array();

// generate the plonky2 proof for the given key
let (signal, vd) = access_set.make_signal(private_keys[i], topic, i)?;
// verify the plonky2 proof (contained in `signal`)
access_set.verify_signal(topic, signal, &vd)
}
}