-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Nova-based VDF using MinRoot as an example (#86)
* add the MinRoot VDF example * optimize a bit * optimization and cleanup * clippy * add examples to CI * use inbuilt pow_vartime
- Loading branch information
1 parent
63f08c0
commit ff40d17
Showing
2 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
//! Demonstrates how to use Nova to produce a recursive proof of the correct execution of | ||
//! iterations of the MinRoot function, thereby realizing a Nova-based verifiable delay function (VDF). | ||
//! We currently execute a single iteration of the MinRoot function per step of Nova's recursion. | ||
type G1 = pasta_curves::pallas::Point; | ||
type G2 = pasta_curves::vesta::Point; | ||
type S1 = nova_snark::spartan_with_ipa_pc::RelaxedR1CSSNARK<G1>; | ||
type S2 = nova_snark::spartan_with_ipa_pc::RelaxedR1CSSNARK<G2>; | ||
use ::bellperson::{gadgets::num::AllocatedNum, ConstraintSystem, SynthesisError}; | ||
use ff::PrimeField; | ||
use generic_array::typenum::U2; | ||
use neptune::{ | ||
circuit::poseidon_hash, | ||
poseidon::{Poseidon, PoseidonConstants}, | ||
Strength, | ||
}; | ||
use nova_snark::{ | ||
traits::{Group, StepCircuit}, | ||
CompressedSNARK, PublicParams, RecursiveSNARK, | ||
}; | ||
use num_bigint::BigUint; | ||
use std::marker::PhantomData; | ||
|
||
// A trivial test circuit that we will use on the secondary curve | ||
#[derive(Clone, Debug)] | ||
struct TrivialTestCircuit<F: PrimeField> { | ||
_p: PhantomData<F>, | ||
} | ||
|
||
impl<F> StepCircuit<F> for TrivialTestCircuit<F> | ||
where | ||
F: PrimeField, | ||
{ | ||
fn synthesize<CS: ConstraintSystem<F>>( | ||
&self, | ||
_cs: &mut CS, | ||
z: AllocatedNum<F>, | ||
) -> Result<AllocatedNum<F>, SynthesisError> { | ||
Ok(z) | ||
} | ||
|
||
fn compute(&self, z: &F) -> F { | ||
*z | ||
} | ||
} | ||
|
||
#[derive(Clone, Debug)] | ||
struct MinRootCircuit<F: PrimeField> { | ||
x_i: F, | ||
y_i: F, | ||
x_i_plus_1: F, | ||
y_i_plus_1: F, | ||
pc: PoseidonConstants<F, U2>, | ||
} | ||
|
||
impl<F: PrimeField> MinRootCircuit<F> { | ||
// produces a sample non-deterministic advice, executing one invocation of MinRoot per step | ||
fn new(num_steps: usize, x_0: &F, y_0: &F, pc: &PoseidonConstants<F, U2>) -> (F, Vec<Self>) { | ||
// although this code is written generically, it is tailored to Pallas' scalar field | ||
// (p - 3 / 5) | ||
let exp = BigUint::parse_bytes( | ||
b"23158417847463239084714197001737581570690445185553317903743794198714690358477", | ||
10, | ||
) | ||
.unwrap(); | ||
|
||
let mut res = Vec::new(); | ||
let mut x_i = *x_0; | ||
let mut y_i = *y_0; | ||
for _i in 0..num_steps { | ||
let x_i_plus_1 = (x_i + y_i).pow_vartime(exp.to_u64_digits()); // computes the fifth root of x_i + y_i | ||
|
||
// sanity check | ||
let sq = x_i_plus_1 * x_i_plus_1; | ||
let quad = sq * sq; | ||
let fifth = quad * x_i_plus_1; | ||
debug_assert_eq!(fifth, x_i + y_i); | ||
|
||
let y_i_plus_1 = x_i; | ||
|
||
res.push(Self { | ||
x_i, | ||
y_i, | ||
x_i_plus_1, | ||
y_i_plus_1, | ||
pc: pc.clone(), | ||
}); | ||
|
||
x_i = x_i_plus_1; | ||
y_i = y_i_plus_1; | ||
} | ||
|
||
let z0 = Poseidon::<F, U2>::new_with_preimage(&[*x_0, *y_0], pc).hash(); | ||
|
||
(z0, res) | ||
} | ||
} | ||
|
||
impl<F> StepCircuit<F> for MinRootCircuit<F> | ||
where | ||
F: PrimeField, | ||
{ | ||
fn synthesize<CS: ConstraintSystem<F>>( | ||
&self, | ||
cs: &mut CS, | ||
z: AllocatedNum<F>, | ||
) -> Result<AllocatedNum<F>, SynthesisError> { | ||
// Allocate four variables for holding non-deterministic advice: x_i, y_i, x_i_plus_1, y_i_plus_1 | ||
let x_i = AllocatedNum::alloc(cs.namespace(|| "x_i"), || Ok(self.x_i))?; | ||
let y_i = AllocatedNum::alloc(cs.namespace(|| "y_i"), || Ok(self.y_i))?; | ||
let x_i_plus_1 = AllocatedNum::alloc(cs.namespace(|| "x_i_plus_1"), || Ok(self.x_i_plus_1))?; | ||
|
||
// check that z = hash(x_i, y_i), where z is an output from the prior step | ||
let z_hash = poseidon_hash( | ||
cs.namespace(|| "input hash"), | ||
vec![x_i.clone(), y_i.clone()], | ||
&self.pc, | ||
)?; | ||
cs.enforce( | ||
|| "z =? z_hash", | ||
|lc| lc + z_hash.get_variable(), | ||
|lc| lc + CS::one(), | ||
|lc| lc + z.get_variable(), | ||
); | ||
|
||
// check the following conditions hold: | ||
// (i) x_i_plus_1 = (x_i + y_i)^{1/5}, which can be more easily checked with x_i_plus_1^5 = x_i + y_i | ||
// (ii) y_i_plus_1 = x_i | ||
// (1) constraints for condition (i) are below | ||
// (2) constraints for condition (ii) is avoided because we just used x_i wherever y_i_plus_1 is used | ||
let x_i_plus_1_sq = x_i_plus_1.square(cs.namespace(|| "x_i_plus_1_sq"))?; | ||
let x_i_plus_1_quad = x_i_plus_1_sq.square(cs.namespace(|| "x_i_plus_1_quad"))?; | ||
let x_i_plus_1_pow_5 = x_i_plus_1_quad.mul(cs.namespace(|| "x_i_plus_1_pow_5"), &x_i_plus_1)?; | ||
cs.enforce( | ||
|| "x_i_plus_1_pow_5 = x_i + y_i", | ||
|lc| lc + x_i_plus_1_pow_5.get_variable(), | ||
|lc| lc + CS::one(), | ||
|lc| lc + x_i.get_variable() + y_i.get_variable(), | ||
); | ||
|
||
// return hash(x_i_plus_1, y_i_plus_1) since Nova circuits expect a single output | ||
poseidon_hash( | ||
cs.namespace(|| "output hash"), | ||
vec![x_i_plus_1, x_i.clone()], | ||
&self.pc, | ||
) | ||
} | ||
|
||
fn compute(&self, z: &F) -> F { | ||
// sanity check | ||
let z_hash = Poseidon::<F, U2>::new_with_preimage(&[self.x_i, self.y_i], &self.pc).hash(); | ||
debug_assert_eq!(z, &z_hash); | ||
|
||
// compute output hash using advice | ||
Poseidon::<F, U2>::new_with_preimage(&[self.x_i_plus_1, self.y_i_plus_1], &self.pc).hash() | ||
} | ||
} | ||
|
||
fn main() { | ||
let pc = PoseidonConstants::<<G1 as Group>::Scalar, U2>::new_with_strength(Strength::Standard); | ||
|
||
let circuit_primary = MinRootCircuit { | ||
x_i: <G1 as Group>::Scalar::zero(), | ||
y_i: <G1 as Group>::Scalar::zero(), | ||
x_i_plus_1: <G1 as Group>::Scalar::zero(), | ||
y_i_plus_1: <G1 as Group>::Scalar::zero(), | ||
pc: pc.clone(), | ||
}; | ||
|
||
let circuit_secondary = TrivialTestCircuit { | ||
_p: Default::default(), | ||
}; | ||
|
||
// produce public parameters | ||
let pp = PublicParams::< | ||
G1, | ||
G2, | ||
MinRootCircuit<<G1 as Group>::Scalar>, | ||
TrivialTestCircuit<<G2 as Group>::Scalar>, | ||
>::setup(circuit_primary, circuit_secondary.clone()); | ||
|
||
// produce non-deterministic advice | ||
let num_steps = 3; | ||
let (z0_primary, minroot_circuits) = MinRootCircuit::new( | ||
num_steps, | ||
&<G1 as Group>::Scalar::zero(), | ||
&<G1 as Group>::Scalar::one(), | ||
&pc, | ||
); | ||
let z0_secondary = <G2 as Group>::Scalar::zero(); | ||
|
||
type C1 = MinRootCircuit<<G1 as Group>::Scalar>; | ||
type C2 = TrivialTestCircuit<<G2 as Group>::Scalar>; | ||
// produce a recursive SNARK | ||
println!("Generating a RecursiveSNARK..."); | ||
let mut recursive_snark: Option<RecursiveSNARK<G1, G2, C1, C2>> = None; | ||
|
||
for (i, circuit_primary) in minroot_circuits.iter().take(num_steps).enumerate() { | ||
let res = RecursiveSNARK::prove_step( | ||
&pp, | ||
recursive_snark, | ||
circuit_primary.clone(), | ||
circuit_secondary.clone(), | ||
z0_primary, | ||
z0_secondary, | ||
); | ||
assert!(res.is_ok()); | ||
println!("RecursiveSNARK::prove_step {}: {:?}", i, res.is_ok()); | ||
recursive_snark = Some(res.unwrap()); | ||
} | ||
|
||
assert!(recursive_snark.is_some()); | ||
let recursive_snark = recursive_snark.unwrap(); | ||
|
||
// verify the recursive SNARK | ||
println!("Verifying a RecursiveSNARK..."); | ||
let res = recursive_snark.verify(&pp, num_steps, z0_primary, z0_secondary); | ||
println!("RecursiveSNARK::verify: {:?}", res.is_ok()); | ||
assert!(res.is_ok()); | ||
|
||
// produce a compressed SNARK | ||
println!("Generating a CompressedSNARK..."); | ||
let res = CompressedSNARK::<_, _, _, _, S1, S2>::prove(&pp, &recursive_snark); | ||
println!("CompressedSNARK::prove: {:?}", res.is_ok()); | ||
assert!(res.is_ok()); | ||
let compressed_snark = res.unwrap(); | ||
|
||
// verify the compressed SNARK | ||
println!("Verifying a CompressedSNARK..."); | ||
let res = compressed_snark.verify(&pp, num_steps, z0_primary, z0_secondary); | ||
println!("CompressedSNARK::verify: {:?}", res.is_ok()); | ||
assert!(res.is_ok()); | ||
} |