diff --git a/Cargo.lock b/Cargo.lock index 35acd7dd..dd0f31c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2437,7 +2437,7 @@ dependencies = [ "generalized-bulletproofs-circuit-abstraction", "generalized-bulletproofs-ec-gadgets", "generic-array 1.1.0", - "multiexp", + "pasta_curves", "rand_chacha", "rand_core", "subtle", @@ -5762,8 +5762,7 @@ dependencies = [ [[package]] name = "pasta_curves" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +source = "git+https://github.com/kayabaNerve/pasta_curves?rev=a46b5be95cacbff54d06aad8d3bbcba42e05d616#a46b5be95cacbff54d06aad8d3bbcba42e05d616" dependencies = [ "blake2b_simd", "ff", @@ -5772,6 +5771,7 @@ dependencies = [ "rand", "static_assertions", "subtle", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9ac449f4..a6246fe6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,6 +161,9 @@ matches = { path = "patches/matches" } option-ext = { path = "patches/option-ext" } directories-next = { path = "patches/directories-next" } +# The official pasta_curves repo doesn't support Zeroize +pasta_curves = { git = "https://github.com/kayabaNerve/pasta_curves", rev = "a46b5be95cacbff54d06aad8d3bbcba42e05d616" } + [workspace.lints.clippy] unwrap_or_default = "allow" borrow_as_ptr = "deny" diff --git a/crypto/evrf/Cargo.toml b/crypto/evrf/Cargo.toml index 3ec7897b..88e914a3 100644 --- a/crypto/evrf/Cargo.toml +++ b/crypto/evrf/Cargo.toml @@ -22,11 +22,14 @@ rand_chacha = { version = "0.3", default-features = false, features = ["std"] } generic-array = { version = "1", default-features = false, features = ["alloc"] } blake2 = { version = "0.10", default-features = false, features = ["std"] } - -multiexp = { path = "../multiexp", version = "0.4", default-features = false, features = ["std", "batch"] } ciphersuite = { path = "../ciphersuite", version = "0.4", default-features = false, features = ["std"] } ec-divisors = { path = "./divisors" } generalized-bulletproofs = { path = "./generalized-bulletproofs" } generalized-bulletproofs-circuit-abstraction = { path = "./circuit-abstraction" } generalized-bulletproofs-ec-gadgets = { path = "./ec-gadgets" } + +[dev-dependencies] +generalized-bulletproofs = { path = "./generalized-bulletproofs", features = ["tests"] } +ec-divisors = { path = "./divisors", features = ["pasta"] } +pasta_curves = "0.5" diff --git a/crypto/evrf/divisors/Cargo.toml b/crypto/evrf/divisors/Cargo.toml index c5674fc8..d4e3a2d0 100644 --- a/crypto/evrf/divisors/Cargo.toml +++ b/crypto/evrf/divisors/Cargo.toml @@ -20,6 +20,7 @@ group = "0.13" hex = { version = "0.4", optional = true } dalek-ff-group = { path = "../../dalek-ff-group", features = ["std"], optional = true } +pasta_curves = { version = "0.5", default-features = false, features = ["bits", "alloc"], optional = true } [dev-dependencies] rand_core = { version = "0.6", features = ["getrandom"] } @@ -30,3 +31,4 @@ pasta_curves = { version = "0.5", default-features = false, features = ["bits", [features] ed25519 = ["hex", "dalek-ff-group"] +pasta = ["pasta_curves"] diff --git a/crypto/evrf/divisors/src/lib.rs b/crypto/evrf/divisors/src/lib.rs index 9cdb1a64..ade96cdf 100644 --- a/crypto/evrf/divisors/src/lib.rs +++ b/crypto/evrf/divisors/src/lib.rs @@ -180,6 +180,48 @@ pub fn new_divisor(points: &[C]) -> Option Self::FieldElement { + Self::FieldElement::ZERO + } + fn b() -> Self::FieldElement { + Self::FieldElement::from(5u64) + } + + fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)> { + Option::>::from(point.to_affine().coordinates()) + .map(|coords| (*coords.x(), *coords.y())) + } + } + + impl DivisorCurve for Eq { + type FieldElement = Fq; + + fn a() -> Self::FieldElement { + Self::FieldElement::ZERO + } + fn b() -> Self::FieldElement { + Self::FieldElement::from(5u64) + } + + fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)> { + Option::>::from(point.to_affine().coordinates()) + .map(|coords| (*coords.x(), *coords.y())) + } + } +} + #[cfg(any(test, feature = "ed25519"))] mod ed25519 { use group::{ diff --git a/crypto/evrf/divisors/src/tests/mod.rs b/crypto/evrf/divisors/src/tests/mod.rs index 53916026..bd8de441 100644 --- a/crypto/evrf/divisors/src/tests/mod.rs +++ b/crypto/evrf/divisors/src/tests/mod.rs @@ -1,30 +1,11 @@ use rand_core::OsRng; -use group::{ff::Field, Group, Curve}; +use group::{ff::Field, Group}; use dalek_ff_group::EdwardsPoint; -use pasta_curves::{ - arithmetic::{Coordinates, CurveAffine}, - Ep, Fp, -}; +use pasta_curves::{Ep, Eq}; use crate::{DivisorCurve, Poly, new_divisor}; -impl DivisorCurve for Ep { - type FieldElement = Fp; - - fn a() -> Self::FieldElement { - Self::FieldElement::ZERO - } - fn b() -> Self::FieldElement { - Self::FieldElement::from(5u64) - } - - fn to_xy(point: Self) -> Option<(Self::FieldElement, Self::FieldElement)> { - Option::>::from(point.to_affine().coordinates()) - .map(|coords| (*coords.x(), *coords.y())) - } -} - // Equation 4 in the security proofs fn check_divisor(points: Vec) { // Create the divisor @@ -208,6 +189,13 @@ fn test_divisor_pallas() { test_subset_sum_to_infinity::(); } +#[test] +fn test_divisor_vesta() { + test_divisor::(); + test_same_point::(); + test_subset_sum_to_infinity::(); +} + #[test] fn test_divisor_ed25519() { // Since we're implementing Wei25519 ourselves, check the isomorphism works as expected diff --git a/crypto/evrf/generalized-bulletproofs/src/transcript.rs b/crypto/evrf/generalized-bulletproofs/src/transcript.rs index d8776f84..75ef35c4 100644 --- a/crypto/evrf/generalized-bulletproofs/src/transcript.rs +++ b/crypto/evrf/generalized-bulletproofs/src/transcript.rs @@ -14,6 +14,10 @@ const POINT: u8 = 1; const CHALLENGE: u8 = 2; fn challenge(digest: &mut Blake2b512) -> F { + // Panic if this is such a wide field, we won't successfully perform a reduction into an unbiased + // scalar + debug_assert!((F::NUM_BITS + 128) < 512); + digest.update([CHALLENGE]); let chl = digest.clone().finalize(); @@ -78,14 +82,16 @@ impl Transcript { Self { digest, transcript: Vec::with_capacity(1024) } } - pub(crate) fn push_scalar(&mut self, scalar: impl PrimeField) { + /// Push a scalar onto the transcript. + pub fn push_scalar(&mut self, scalar: impl PrimeField) { self.digest.update([SCALAR]); let bytes = scalar.to_repr(); self.digest.update(bytes); self.transcript.extend(bytes.as_ref()); } - pub(crate) fn push_point(&mut self, point: impl GroupEncoding) { + /// Push a point onto the transcript. + pub fn push_point(&mut self, point: impl GroupEncoding) { self.digest.update([POINT]); let bytes = point.to_bytes(); self.digest.update(bytes); @@ -132,7 +138,8 @@ impl<'a> VerifierTranscript<'a> { Self { digest, transcript: proof } } - pub(crate) fn read_scalar(&mut self) -> io::Result { + /// Read a scalar from the transcript. + pub fn read_scalar(&mut self) -> io::Result { let scalar = C::read_F(&mut self.transcript)?; self.digest.update([SCALAR]); let bytes = scalar.to_repr(); @@ -140,7 +147,8 @@ impl<'a> VerifierTranscript<'a> { Ok(scalar) } - pub(crate) fn read_point(&mut self) -> io::Result { + /// Read a point from the transcript. + pub fn read_point(&mut self) -> io::Result { let point = C::read_G(&mut self.transcript)?; self.digest.update([POINT]); let bytes = point.to_bytes(); @@ -172,4 +180,9 @@ impl<'a> VerifierTranscript<'a> { pub fn challenge(&mut self) -> F { challenge(&mut self.digest) } + + /// Complete the transcript, returning the advanced slice. + pub fn complete(self) -> &'a [u8] { + self.transcript + } } diff --git a/crypto/evrf/src/lib.rs b/crypto/evrf/src/lib.rs index 06ffa35f..b321e5dd 100644 --- a/crypto/evrf/src/lib.rs +++ b/crypto/evrf/src/lib.rs @@ -6,6 +6,7 @@ use rand_chacha::ChaCha20Rng; use generic_array::{typenum::Unsigned, ArrayLength, GenericArray}; +use blake2::{Digest, Blake2s256}; use ciphersuite::{ group::{ ff::{Field, PrimeField, PrimeFieldBits}, @@ -24,6 +25,9 @@ use generalized_bulletproofs_circuit_abstraction::*; use ec_divisors::{DivisorCurve, new_divisor}; use generalized_bulletproofs_ec_gadgets::*; +#[cfg(test)] +mod tests; + /// A curve to perform the eVRF with. pub trait EvrfCurve: Ciphersuite { type EmbeddedCurve: Ciphersuite; @@ -39,7 +43,7 @@ pub struct EvrfProveResult { /// A struct to prove/verify eVRFs with. pub struct Evrf; impl Evrf { - fn seed_to_points(seed: [u8; 32], quantity: usize) -> Vec { + fn transcript_to_points(seed: [u8; 32], quantity: usize) -> Vec { // We need to do two Diffie-Hellman's per point in order to achieve an unbiased result let quantity = 2 * quantity; @@ -58,7 +62,7 @@ impl Evrf { fn point_with_dlogs( quantity: usize, generators_to_use: usize, -) -> Vec> { + ) -> Vec> { let quantity = 2 * quantity; fn read_one_from_tape(generators_to_use: usize, start: &mut usize) -> Variable { @@ -91,8 +95,8 @@ impl Evrf { let dlog = read_from_tape(generators_to_use, &mut start); - let mut res = Vec::with_capacity(quantity); - for _ in 0 .. quantity { + let mut res = Vec::with_capacity(quantity + 1); + let mut read_point_with_dlog = || { let zero = read_one_from_tape(generators_to_use, &mut start); let x_from_power_of_2 = read_from_tape(generators_to_use, &mut start); let yx = read_from_tape(generators_to_use, &mut start); @@ -105,7 +109,14 @@ impl Evrf { ); res.push(PointWithDlog { dlog: dlog.clone(), divisor, point }); + }; + + for _ in 0 .. quantity { + // One for each DH proven + read_point_with_dlog(); } + // And one more for the proof this is the discrete log of the public key + read_point_with_dlog(); res } @@ -175,7 +186,7 @@ impl Evrf { rng: &mut (impl RngCore + CryptoRng), generators: &Generators, evrf_private_key: <::EmbeddedCurve as Ciphersuite>::F, - seed: [u8; 32], + invocation: [u8; 32], quantity: usize, ) -> Result, AcError> where @@ -187,7 +198,19 @@ impl Evrf { b: <::EmbeddedCurve as Ciphersuite>::G::b(), }; - let points = Self::seed_to_points::(seed, quantity); + // Combine the invocation and the public key into a transcript + let transcript = Blake2s256::digest( + [ + invocation.as_slice(), + (<::EmbeddedCurve as Ciphersuite>::generator() * evrf_private_key) + .to_bytes() + .as_ref(), + ] + .concat(), + ) + .into(); + + let points = Self::transcript_to_points::(transcript, quantity); let num_bits: u32 = <::EmbeddedCurve as Ciphersuite>::F::NUM_BITS; @@ -218,6 +241,8 @@ impl Evrf { let mut h_value = dlog[h as usize]; let mut h_prior_value = dlog[(h as usize) - 1]; + // TODO: Squash the following two loops by iterating from the top bit to the bottom bit + let mut prior_scalar = dlog[(h as usize) - 1]; for (i, scalar) in dlog.iter().enumerate().skip(h as usize) { let is_zero = ::F::ZERO.ct_eq(scalar); @@ -367,7 +392,7 @@ impl Evrf { commitments.push(PedersenCommitment { value: **scalar, mask: C::F::random(&mut *rng) }); } - let mut transcript = ProverTranscript::new(seed); + let mut transcript = ProverTranscript::new(transcript); let commited_commitments = transcript.write_commitments( vector_commitments .iter() @@ -383,7 +408,7 @@ impl Evrf { .collect(), ); - let mut circuit = Circuit::prove(vector_commitments, commitments); + let mut circuit = Circuit::prove(vector_commitments, commitments.clone()); Self::circuit::( &curve_spec, evrf_public_key, @@ -402,7 +427,32 @@ impl Evrf { else { panic!("proving yet wasn't yielded the witness"); }; - statement.prove(rng, &mut transcript, witness).unwrap(); + statement.prove(&mut *rng, &mut transcript, witness).unwrap(); + + // Push the reveal onto the transcript + for scalar in &scalars { + transcript.push_point(generators.g() * **scalar); + } + + // Define a weight to aggregate the commitments with + let mut agg_weights = Vec::with_capacity(quantity); + agg_weights.push(C::F::ONE); + while agg_weights.len() < quantity { + agg_weights.push(transcript.challenge::()); + } + let mut x = commitments + .iter() + .zip(&agg_weights) + .map(|(commitment, weight)| commitment.mask * *weight) + .sum::(); + + // Do a Schnorr PoK for the randomness of the aggregated Pedersen commitment + let mut r = C::F::random(&mut *rng); + transcript.push_point(generators.h() * r); + let c = transcript.challenge::(); + transcript.push_scalar(r + (c * x)); + r.zeroize(); + x.zeroize(); Ok(EvrfProveResult { scalars, proof: transcript.complete() }) } @@ -414,7 +464,7 @@ impl Evrf { generators: &Generators, verifier: &mut BatchVerifier, evrf_public_key: <::EmbeddedCurve as Ciphersuite>::G, - seed: [u8; 32], + invocation: [u8; 32], quantity: usize, proof: &[u8], ) -> Result, ()> @@ -427,7 +477,11 @@ impl Evrf { b: <::EmbeddedCurve as Ciphersuite>::G::b(), }; - let points = Self::seed_to_points::(seed, quantity); + let transcript = + Blake2s256::digest([invocation.as_slice(), evrf_public_key.to_bytes().as_ref()].concat()) + .into(); + + let points = Self::transcript_to_points::(transcript, quantity); let mut generator_tables = Vec::with_capacity(1 + (2 * quantity)); for generator in points { @@ -443,14 +497,19 @@ impl Evrf { let (_, generators_to_use) = Self::muls_and_generators_to_use(quantity); - let mut transcript = VerifierTranscript::new(seed, proof); + let mut transcript = VerifierTranscript::new(transcript, proof); - let divisor_len = 1 + ::XCoefficientsMinusOne::USIZE + ::YxCoefficients::USIZE + 1; - let dlog_len = divisor_len + 2; - let vcs = - (::ScalarBits::USIZE + ((1 + (2 * quantity)) * dlog_len)) / (2 * generators_to_use); + let divisor_len = 1 + + ::XCoefficientsMinusOne::USIZE + + ::YxCoefficients::USIZE + + 1; + let dlog_proof_len = divisor_len + 2; + let vcs = (::ScalarBits::USIZE + + ((1 + (2 * quantity)) * dlog_proof_len)) + .div_ceil(2 * generators_to_use); - let commitments = transcript.read_commitments(vcs, quantity).map_err(|_| ())?; + let all_commitments = transcript.read_commitments(vcs, quantity).map_err(|_| ())?; + let commitments = all_commitments.V().to_vec(); let mut circuit = Circuit::verify(); Self::circuit::( @@ -464,14 +523,46 @@ impl Evrf { ); let (statement, None) = - circuit.statement(generators.reduce(generators_to_use).ok_or(())?, commitments).unwrap() + circuit.statement(generators.reduce(generators_to_use).ok_or(())?, all_commitments).unwrap() else { panic!("verifying yet was yielded a witness"); }; statement.verify(rng, verifier, &mut transcript).map_err(|_| ())?; - // TODO: Unblinded PCs - Ok(vec![]) + // Read the unblinded public keys + let mut res = Vec::with_capacity(quantity); + for _ in 0 .. quantity { + res.push(transcript.read_point::().map_err(|_| ())?); + } + + let mut agg_weights = Vec::with_capacity(quantity); + agg_weights.push(C::F::ONE); + while agg_weights.len() < quantity { + agg_weights.push(transcript.challenge::()); + } + + let sum_points = + res.iter().zip(&agg_weights).map(|(point, weight)| *point * *weight).sum::(); + let sum_commitments = + commitments.into_iter().zip(agg_weights).map(|(point, weight)| point * weight).sum::(); + #[allow(non_snake_case)] + let A = sum_commitments - sum_points; + + #[allow(non_snake_case)] + let R = transcript.read_point::().map_err(|_| ())?; + let c = transcript.challenge::(); + let s = transcript.read_scalar::().map_err(|_| ())?; + + // Doesn't batch verify this as we can't access the internals of the GBP batch verifier + if (R + (A * c)) != (generators.h() * s) { + Err(())?; + } + + if !transcript.complete().is_empty() { + Err(())? + }; + + Ok(res) } } diff --git a/crypto/evrf/src/tests.rs b/crypto/evrf/src/tests.rs new file mode 100644 index 00000000..b13112d8 --- /dev/null +++ b/crypto/evrf/src/tests.rs @@ -0,0 +1,88 @@ +use std::time::Instant; + +use rand_core::OsRng; + +use zeroize::Zeroize; +use generic_array::typenum::{Sum, Diff, Quot, U, U1, U2}; +use blake2::{Digest, Blake2b512}; + +use ciphersuite::{ + group::ff::{FromUniformBytes, PrimeField}, + Ciphersuite, +}; +use pasta_curves::{Ep, Eq, Fp, Fq}; + +use generalized_bulletproofs::tests::generators; + +use crate::*; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +struct Pallas; +impl Ciphersuite for Pallas { + type F = Fq; + type G = Ep; + type H = Blake2b512; + const ID: &'static [u8] = b"Pallas"; + fn generator() -> Ep { + Ep::generator() + } + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + // This naive concat may be insecure in a real world deployment + // This is solely test code so it's fine + Self::F::from_uniform_bytes(&Self::H::digest([dst, msg].concat()).into()) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +struct Vesta; +impl Ciphersuite for Vesta { + type F = Fp; + type G = Eq; + type H = Blake2b512; + const ID: &'static [u8] = b"Vesta"; + fn generator() -> Eq { + Eq::generator() + } + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + // This naive concat may be insecure in a real world deployment + // This is solely test code so it's fine + Self::F::from_uniform_bytes(&Self::H::digest([dst, msg].concat()).into()) + } +} + +struct VestaParams; +impl DiscreteLogParameters for VestaParams { + type ScalarBits = U<{ <::F as PrimeField>::NUM_BITS as usize }>; + type XCoefficients = Quot, U2>; + type XCoefficientsMinusOne = Diff; + type YxCoefficients = Diff, U2>, U2>; +} + +impl EvrfCurve for Pallas { + type EmbeddedCurve = Vesta; + type EmbeddedCurveParameters = VestaParams; +} + +#[test] +fn pasta_test() { + let generators = generators(1024); + let vesta_private_key = ::F::random(&mut OsRng); + let time = Instant::now(); + let res = Evrf::prove::(&mut OsRng, &generators, vesta_private_key, [0; 32], 1).unwrap(); + println!("Proving time: {:?}", Instant::now() - time); + + let time = Instant::now(); + let mut verifier = generators.batch_verifier(); + dbg!(Evrf::verify::( + &mut OsRng, + &generators, + &mut verifier, + Vesta::generator() * vesta_private_key, + [0; 32], + 1, + &res.proof, + ) + .unwrap()); + assert!(generators.verify(verifier)); + println!("Verifying time: {:?}", Instant::now() - time); +} diff --git a/deny.toml b/deny.toml index b8da5705..1d855826 100644 --- a/deny.toml +++ b/deny.toml @@ -99,4 +99,5 @@ allow-git = [ "https://github.com/serai-dex/substrate-bip39", "https://github.com/serai-dex/substrate", "https://github.com/orcalabs/dockertest-rs", + "https://github.com/kayabaNerve/pasta_curves", ]