From 25bd5d85f52fef102c0b35756fb5783221e7dafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Thu, 5 Sep 2024 08:54:18 +0200 Subject: [PATCH] Add Pietrzak's VDF construction (#818) * First draft of pietrzak's vdf * Use fs * clean up * Handle iterations that are not a power of two * Remove obsolete comment * simplify * Allow shorter proofs * refactor * clean up + docs * docs * Refactor * Compute proof size * Use biguint for challenge * unused import * refactor * clean up * simplify * comment * simplify * simplify more * clean up * simplify * fmt * Fix iteration computation * clean up fs * simplify * Review comments * Handle overflow gracefully --- fastcrypto-vdf/src/class_group/mod.rs | 18 +- .../src/math/parameterized_group.rs | 57 +++++- fastcrypto-vdf/src/vdf/mod.rs | 1 + fastcrypto-vdf/src/vdf/pietrzak/mod.rs | 179 ++++++++++++++++++ .../src/vdf/wesolowski/fiat_shamir.rs | 30 +-- fastcrypto-vdf/src/vdf/wesolowski/mod.rs | 38 ++-- fastcrypto/src/groups/mod.rs | 9 +- .../src/groups/multiplier/integer_utils.rs | 8 +- fastcrypto/src/groups/ristretto255.rs | 4 +- fastcrypto/src/groups/secp256r1.rs | 2 +- 10 files changed, 277 insertions(+), 69 deletions(-) create mode 100644 fastcrypto-vdf/src/vdf/pietrzak/mod.rs diff --git a/fastcrypto-vdf/src/class_group/mod.rs b/fastcrypto-vdf/src/class_group/mod.rs index 5aebce9fcd..41b13aeeb0 100644 --- a/fastcrypto-vdf/src/class_group/mod.rs +++ b/fastcrypto-vdf/src/class_group/mod.rs @@ -205,26 +205,22 @@ impl QuadraticForm { } impl Doubling for QuadraticForm { - fn double(&self) -> Self { + fn double(self) -> Self { // Slightly optimised version of Algorithm 2 from Jacobson, Jr, Michael & Poorten, Alfred // (2002). "Computational aspects of NUCOMP", Lecture Notes in Computer Science. // (https://www.researchgate.net/publication/221451638_Computational_aspects_of_NUCOMP) // The paragraph numbers and variable names follow the paper. - let u = &self.a; - let v = &self.b; - let w = &self.c; - let EuclideanAlgorithmOutput { gcd: g, x: _, y, a_divided_by_gcd: capital_by, b_divided_by_gcd: capital_dy, - } = extended_euclidean_algorithm(u, v, false); + } = extended_euclidean_algorithm(&self.a, &self.b, false); let (bx, x, by, y, iterated) = partial_xgcd( - (&y * w).mod_floor(&capital_by), + (&y * &self.c).mod_floor(&capital_by), capital_by.clone(), self.partial_gcd_limit(), ); @@ -234,11 +230,11 @@ impl Doubling for QuadraticForm { let mut v3 = -(by * &bx) << 1; if !iterated { - let dx = (&bx * &capital_dy - w) / &capital_by; - v3 += v; + let dx = (&bx * &capital_dy - &self.c) / &capital_by; + v3 += &self.b; w3 -= &g * &dx; } else { - let dx = (&bx * &capital_dy - w * &x) / &capital_by; + let dx = (&bx * &capital_dy - &self.c * &x) / &capital_by; let q1 = &dx * &y; let mut dy = &q1 + &capital_dy; v3 += &g * (&dy + &q1); @@ -295,8 +291,6 @@ impl ParameterizedGroupElement for QuadraticForm { /// The discriminant of a quadratic form defines the class group. type ParameterType = Discriminant; - type ScalarType = BigInt; - fn zero(discriminant: &Self::ParameterType) -> Self { Self::from_a_b_and_discriminant(BigInt::one(), BigInt::one(), discriminant) .expect("Doesn't fail") diff --git a/fastcrypto-vdf/src/math/parameterized_group.rs b/fastcrypto-vdf/src/math/parameterized_group.rs index c2cd40b055..c9b4e1dc5f 100644 --- a/fastcrypto-vdf/src/math/parameterized_group.rs +++ b/fastcrypto-vdf/src/math/parameterized_group.rs @@ -1,10 +1,10 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::ops::{Add, Neg}; - use fastcrypto::error::FastCryptoResult; use fastcrypto::groups::Doubling; +use num_bigint::BigUint; +use std::ops::{Add, Neg}; /// This trait is implemented by types which can be used as parameters for a parameterized group. /// See [ParameterizedGroupElement]. @@ -21,12 +21,59 @@ pub trait ParameterizedGroupElement: /// The type of the parameter which uniquely defines this group. type ParameterType: Parameter; - /// Integer type used for multiplication. - type ScalarType: From; - /// Return an instance of the identity element in this group. fn zero(parameter: &Self::ParameterType) -> Self; /// Returns true if this is an element of the group defined by `parameter`. fn is_in_group(&self, parameter: &Self::ParameterType) -> bool; } + +/// Compute self * scalar using a "Double-and-Add" algorithm for a positive scalar. +pub(crate) fn multiply( + input: &G, + scalar: &BigUint, + parameter: &G::ParameterType, +) -> G { + (0..scalar.bits()) + .rev() + .map(|i| scalar.bit(i)) + .fold(G::zero(parameter), |acc, bit| { + let mut res = acc.double(); + if bit { + res = res + input; + } + res + }) +} + +#[cfg(test)] +mod tests { + use crate::class_group::discriminant::Discriminant; + use crate::class_group::QuadraticForm; + use crate::math::parameterized_group::{multiply, Parameter, ParameterizedGroupElement}; + use num_bigint::BigUint; + use num_traits::{One, Zero}; + + #[test] + fn test_scalar_multiplication() { + let discriminant = Discriminant::from_seed(b"test", 256).unwrap(); + let input = QuadraticForm::generator(&discriminant); + + // Edge cases + assert_eq!( + QuadraticForm::zero(&discriminant), + multiply(&input, &BigUint::zero(), &discriminant) + ); + assert_eq!(input, multiply(&input, &BigUint::one(), &discriminant)); + + let exponent = 12345u64; + let output = multiply(&input, &BigUint::from(exponent), &discriminant); + + // Check alignment with repeated addition. + let mut expected_output = input.clone(); + for _ in 1..exponent { + expected_output = expected_output + &input; + } + assert_eq!(output, expected_output); + } +} diff --git a/fastcrypto-vdf/src/vdf/mod.rs b/fastcrypto-vdf/src/vdf/mod.rs index b6ac138d06..8e960d7422 100644 --- a/fastcrypto-vdf/src/vdf/mod.rs +++ b/fastcrypto-vdf/src/vdf/mod.rs @@ -6,6 +6,7 @@ use fastcrypto::error::FastCryptoResult; +pub mod pietrzak; pub mod wesolowski; /// This represents a Verifiable Delay Function (VDF) construction. diff --git a/fastcrypto-vdf/src/vdf/pietrzak/mod.rs b/fastcrypto-vdf/src/vdf/pietrzak/mod.rs new file mode 100644 index 0000000000..08eee25a9d --- /dev/null +++ b/fastcrypto-vdf/src/vdf/pietrzak/mod.rs @@ -0,0 +1,179 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::math::parameterized_group::{multiply, ParameterizedGroupElement}; +use crate::vdf::VDF; +use fastcrypto::error::FastCryptoError::{InvalidInput, InvalidProof}; +use fastcrypto::error::FastCryptoResult; +use fastcrypto::hash::{HashFunction, Keccak256}; +use num_bigint::BigUint; +use num_integer::Integer; +use serde::Serialize; +use std::mem; + +/// Default size in bytes of the Fiat-Shamir challenge used in proving and verification. +/// +/// This is based on Pietrzak (2018), "Simple Verifiable Delay Functions" (https://eprint.iacr.org/2018/627.pdf) +/// which states that the challenge should be 2^l bits (see section 6), where l is the security +/// parameter. Soundness is proven in section 6.3 in this paper. +pub const DEFAULT_CHALLENGE_SIZE_IN_BYTES: usize = 16; + +/// This implements Pietrzak's VDF construction from https://eprint.iacr.org/2018/627.pdf. +/// +/// The VDF is, as in [crate::vdf::wesolowski::WesolowskisVDF], based on the repeated squaring of an +/// element in a group of unknown order. However, in this construction, proofs are larger and +/// verification is slower than in Wesolowski's construction, but the output of a VDF is unique, +/// assuming that the used group have no small subgroups, and proving is faster for the same number +/// of iterations. +pub struct PietrzaksVDF { + group_parameter: G::ParameterType, + iterations: u64, +} + +impl PietrzaksVDF { + /// Create a new VDF using the group defined by the given group parameter. Evaluating this VDF + /// will require computing `2^iterations * input` which requires `iterations` group operations. + pub fn new(group_parameter: G::ParameterType, iterations: u64) -> Self { + Self { + group_parameter, + iterations, + } + } +} + +impl VDF for PietrzaksVDF +where + G::ParameterType: Serialize, +{ + type InputType = G; + type OutputType = G; + type ProofType = Vec; + + fn evaluate(&self, input: &G) -> FastCryptoResult<(G, Vec)> { + // Proof generation works but is not optimised. + + if !input.is_in_group(&self.group_parameter) || self.iterations == 0 { + return Err(InvalidInput); + } + + // Compute output = 2^iterations * input + let output = input.clone().repeated_doubling(self.iterations); + + let mut x = input.clone(); + let mut y = output.clone(); + let mut t = self.iterations; + + let mut proof = Vec::new(); + + // Compute the full proof. This loop may stop at any time which will give a shorter proof + // that is computationally harder to verify. + while t != 1 { + if check_parity_and_iterate(&mut t) { + y = y.double(); + } + + // TODO: Precompute some of the mu's to speed up the proof generation. + let mu = x.clone().repeated_doubling(t); + + let r = compute_challenge(&x, &y, self.iterations, &mu, &self.group_parameter); + x = multiply(&x, &r, &self.group_parameter) + μ + y = multiply(&mu, &r, &self.group_parameter) + &y; + + proof.push(mu); + } + + Ok((output, proof)) + } + + fn verify(&self, input: &G, output: &G, proof: &Vec) -> FastCryptoResult<()> { + if !input.is_in_group(&self.group_parameter) + || !output.is_in_group(&self.group_parameter) + || !proof.iter().all(|mu| mu.is_in_group(&self.group_parameter)) + || self.iterations == 0 + { + return Err(InvalidInput); + } + + let mut x = input.clone(); + let mut y = output.clone(); + let mut t = self.iterations; + + for mu in proof { + if check_parity_and_iterate(&mut t) { + y = y.double(); + } + + let r = compute_challenge(&x, &y, self.iterations, mu, &self.group_parameter); + x = multiply(&x, &r, &self.group_parameter) + mu; + y = multiply(mu, &r, &self.group_parameter) + y; + } + + // In case the proof is shorter than the full proof, we need to compute the remaining powers. + x = x.repeated_doubling(t); + if x != y { + return Err(InvalidProof); + } + Ok(()) + } +} + +/// Compute the Fiat-Shamir challenge used in Pietrzak's VDF construction. +fn compute_challenge( + input: &G, + output: &G, + iterations: u64, + mu: &G, + group_parameter: &G::ParameterType, +) -> BigUint +where + G::ParameterType: Serialize, +{ + let seed = bcs::to_bytes(&(input, output, iterations, mu, group_parameter)) + .expect("Failed to serialize Fiat-Shamir input."); + let hash = Keccak256::digest(seed); + BigUint::from_bytes_be(&hash.digest[..DEFAULT_CHALLENGE_SIZE_IN_BYTES]) +} + +/// Replace t with (t+1) >> 1 and return true iff the input was odd. +#[inline] +fn check_parity_and_iterate(t: &mut u64) -> bool { + mem::replace(t, (*t >> 1) + (*t & 1)).is_odd() +} + +#[cfg(test)] +mod tests { + use crate::class_group::discriminant::Discriminant; + use crate::class_group::QuadraticForm; + use crate::math::parameterized_group::Parameter; + use crate::vdf::pietrzak::PietrzaksVDF; + use crate::vdf::VDF; + + #[test] + fn test_vdf() { + let iterations = 136u64; + let discriminant = Discriminant::from_seed(&[0, 1, 2], 512).unwrap(); + + let input = QuadraticForm::generator(&discriminant); + + let vdf = PietrzaksVDF::::new(discriminant.clone(), iterations); + let (output, proof) = vdf.evaluate(&input).unwrap(); + + assert!(vdf.verify(&input, &output, &proof).is_ok()); + + let other_input = input.clone() + &input; + assert!(vdf.verify(&other_input, &output, &proof).is_err()) + } + + #[test] + fn test_vdf_edge_cases() { + let discriminant = Discriminant::from_seed(&[0, 1, 2], 512).unwrap(); + let input = QuadraticForm::generator(&discriminant); + + assert!(PietrzaksVDF::::new(discriminant.clone(), 1) + .evaluate(&input) + .is_ok()); + assert!(PietrzaksVDF::::new(discriminant.clone(), 0) + .evaluate(&input) + .is_err()); + } +} diff --git a/fastcrypto-vdf/src/vdf/wesolowski/fiat_shamir.rs b/fastcrypto-vdf/src/vdf/wesolowski/fiat_shamir.rs index 7032750f45..fd2b1e16f5 100644 --- a/fastcrypto-vdf/src/vdf/wesolowski/fiat_shamir.rs +++ b/fastcrypto-vdf/src/vdf/wesolowski/fiat_shamir.rs @@ -1,14 +1,12 @@ // Copyright (c) 2022, Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use crate::class_group::discriminant::Discriminant; use crate::class_group::QuadraticForm; use crate::math::hash_prime::hash_prime; use crate::math::parameterized_group::ParameterizedGroupElement; use crate::vdf::wesolowski::WesolowskisVDF; use fastcrypto::groups::multiplier::ScalarMultiplier; -use num_bigint::BigInt; -use serde::Serialize; +use num_bigint::BigUint; /// Default size in bytes of the Fiat-Shamir challenge used in proving and verification. /// @@ -23,11 +21,11 @@ pub const DEFAULT_CHALLENGE_SIZE_IN_BYTES: usize = 33; pub trait FiatShamir: Sized { /// Compute the prime modulus used in proving and verification. This is a Fiat-Shamir construction /// to make the Wesolowski VDF non-interactive. - fn compute_challenge>( + fn compute_challenge>( vdf: &WesolowskisVDF, input: &G, output: &G, - ) -> G::ScalarType; + ) -> BigUint; } /// Implementation of the Fiat-Shamir challenge generation for usage with Wesolowski's VDF construction. @@ -36,31 +34,17 @@ pub trait FiatShamir: Sized { pub struct StrongFiatShamir {} impl FiatShamir for StrongFiatShamir { - fn compute_challenge>( + fn compute_challenge>( vdf: &WesolowskisVDF, input: &QuadraticForm, output: &QuadraticForm, - ) -> BigInt { - let seed = bcs::to_bytes(&FiatShamirInput { - input, - output, - iterations: vdf.iterations, - group_parameter: &vdf.group_parameter, - }) - .expect("Failed to serialize FiatShamirInput"); + ) -> BigUint { + let seed = bcs::to_bytes(&(input, output, vdf.iterations, &vdf.group_parameter)) + .expect("Failed to serialize Fiat-Shamir input"); hash_prime( &seed, DEFAULT_CHALLENGE_SIZE_IN_BYTES, &[0, 8 * DEFAULT_CHALLENGE_SIZE_IN_BYTES - 1], ) - .into() } } - -#[derive(Serialize)] -struct FiatShamirInput<'a> { - input: &'a QuadraticForm, - output: &'a QuadraticForm, - iterations: u64, - group_parameter: &'a Discriminant, -} diff --git a/fastcrypto-vdf/src/vdf/wesolowski/mod.rs b/fastcrypto-vdf/src/vdf/wesolowski/mod.rs index 05085b42dc..a977e3211d 100644 --- a/fastcrypto-vdf/src/vdf/wesolowski/mod.rs +++ b/fastcrypto-vdf/src/vdf/wesolowski/mod.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; use std::ops::ShlAssign; -use num_bigint::BigInt; +use num_bigint::BigUint; use num_integer::Integer; use fastcrypto::error::FastCryptoError::{InvalidInput, InvalidProof}; @@ -24,7 +24,7 @@ mod fiat_shamir; pub struct WesolowskisVDF< G: ParameterizedGroupElement, F: FiatShamir, - M: ScalarMultiplier, + M: ScalarMultiplier, > { group_parameter: G::ParameterType, iterations: u64, @@ -32,7 +32,7 @@ pub struct WesolowskisVDF< _scalar_multiplier: PhantomData, } -impl, M: ScalarMultiplier> +impl, M: ScalarMultiplier> WesolowskisVDF { /// Create a new VDF using the group defined by the given group parameter. Evaluating this VDF @@ -47,11 +47,8 @@ impl, M: ScalarMultiplier, - F: FiatShamir, - M: ScalarMultiplier, - > VDF for WesolowskisVDF +impl, M: ScalarMultiplier> VDF + for WesolowskisVDF { type InputType = G; type OutputType = G; @@ -63,16 +60,13 @@ impl< } // Compute output = 2^iterations * input - let mut output = input.clone(); - for _ in 0..self.iterations { - output = output.double(); - } + let output = input.clone().repeated_doubling(self.iterations); let multiplier = M::new(input.clone(), G::zero(&self.group_parameter)); // Algorithm from page 3 on https://crypto.stanford.edu/~dabo/pubs/papers/VDFsurvey.pdf let challenge = F::compute_challenge(self, input, &output); - let mut quotient_remainder = (BigInt::from(0), BigInt::from(2)); + let mut quotient_remainder = (BigUint::from(0u8), BigUint::from(2u8)); let mut proof = multiplier.mul("ient_remainder.0); for _ in 1..self.iterations { quotient_remainder.1.shl_assign(1); @@ -92,7 +86,11 @@ impl< } let challenge = F::compute_challenge(self, input, output); - let r = BigInt::modpow(&BigInt::from(2), &BigInt::from(self.iterations), &challenge); + let r = BigUint::modpow( + &BigUint::from(2u8), + &BigUint::from(self.iterations), + &challenge, + ); let multiplier = M::new(input.clone(), G::zero(&self.group_parameter)); if multiplier.two_scalar_mul(&r, proof, &challenge) != *output { @@ -107,7 +105,7 @@ impl< pub type DefaultVDF = WesolowskisVDF< QuadraticForm, StrongFiatShamir, - WindowedScalarMultiplier, + WindowedScalarMultiplier, >; #[cfg(test)] @@ -120,7 +118,7 @@ mod tests { use crate::vdf::VDF; use fastcrypto::groups::multiplier::windowed::WindowedScalarMultiplier; use fastcrypto::groups::multiplier::ScalarMultiplier; - use num_bigint::BigInt; + use num_bigint::{BigInt, BigUint}; use num_traits::Num; use std::str::FromStr; @@ -176,7 +174,7 @@ mod tests { let vdf = WesolowskisVDF::< QuadraticForm, ChiaFiatShamir, - WindowedScalarMultiplier, + WindowedScalarMultiplier, >::new(discriminant.clone(), iterations); assert!(vdf.verify(&input, &output, &proof).is_ok()); @@ -186,13 +184,13 @@ mod tests { struct ChiaFiatShamir {} impl FiatShamir for ChiaFiatShamir { - fn compute_challenge>( + fn compute_challenge>( _vdf: &WesolowskisVDF, _input: &QuadraticForm, _output: &QuadraticForm, - ) -> BigInt { + ) -> BigUint { // Hardcoded challenge for the test vector - BigInt::from_str_radix( + BigUint::from_str_radix( "a8d8728e9942a994a3a1aa3d2fa21549aa1a7b37d3c315c6e705bda590689c640f", 16, ) diff --git a/fastcrypto/src/groups/mod.rs b/fastcrypto/src/groups/mod.rs index e4d0f856d1..f002a6f49a 100644 --- a/fastcrypto/src/groups/mod.rs +++ b/fastcrypto/src/groups/mod.rs @@ -54,9 +54,14 @@ pub trait Scalar: } /// Trait for group elements that has a fast doubling operation. -pub trait Doubling { +pub trait Doubling: Clone { /// Compute 2 * Self = Self + Self. - fn double(&self) -> Self; + fn double(self) -> Self; + + /// Compute input * 2^repetitions by repeated doubling. + fn repeated_doubling(self, repetitions: u64) -> Self { + (0..repetitions).fold(self, |acc, _| acc.double()) + } } pub trait Pairing: GroupElement { diff --git a/fastcrypto/src/groups/multiplier/integer_utils.rs b/fastcrypto/src/groups/multiplier/integer_utils.rs index dda0a12bfa..0c23ac6857 100644 --- a/fastcrypto/src/groups/multiplier/integer_utils.rs +++ b/fastcrypto/src/groups/multiplier/integer_utils.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::groups::multiplier::ToLittleEndianBytes; -use num_bigint::BigInt; +use num_bigint::BigUint; /// Given a binary representation of a number in little-endian format, return the digits of its base /// `2^bits_per_digit` expansion. @@ -116,11 +116,11 @@ pub fn is_power_of_2(x: usize) -> bool { x & (x - 1) == 0 } -// We implementation `ToLittleEndianByteArray` for BigInt in case it needs to be used as scalar for +// We implement `ToLittleEndianByteArray` for `BigUint` in case it needs to be used as scalar for // multi-scalar multiplication. -impl ToLittleEndianBytes for BigInt { +impl ToLittleEndianBytes for BigUint { fn to_le_bytes(&self) -> Vec { - self.to_bytes_le().1 + self.to_bytes_le() } } diff --git a/fastcrypto/src/groups/ristretto255.rs b/fastcrypto/src/groups/ristretto255.rs index b84407d812..9155d48df6 100644 --- a/fastcrypto/src/groups/ristretto255.rs +++ b/fastcrypto/src/groups/ristretto255.rs @@ -58,8 +58,8 @@ impl RistrettoPoint { } impl Doubling for RistrettoPoint { - fn double(&self) -> Self { - Self(self.0.add(&self.0)) + fn double(self) -> Self { + Self(self.0.add(self.0)) } } diff --git a/fastcrypto/src/groups/secp256r1.rs b/fastcrypto/src/groups/secp256r1.rs index b570d02862..d9629f002e 100644 --- a/fastcrypto/src/groups/secp256r1.rs +++ b/fastcrypto/src/groups/secp256r1.rs @@ -38,7 +38,7 @@ impl GroupElement for ProjectivePoint { } impl Doubling for ProjectivePoint { - fn double(&self) -> Self { + fn double(self) -> Self { ProjectivePoint::from(self.0.double()) } }