diff --git a/Cargo.lock b/Cargo.lock index 440fcbc2..35acd7dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2426,6 +2426,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "evrf" +version = "0.1.0" +dependencies = [ + "blake2", + "ciphersuite", + "ec-divisors", + "generalized-bulletproofs", + "generalized-bulletproofs-circuit-abstraction", + "generalized-bulletproofs-ec-gadgets", + "generic-array 1.1.0", + "multiexp", + "rand_chacha", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "exit-future" version = "0.2.0" @@ -3015,10 +3033,8 @@ name = "generalized-bulletproofs-ec-gadgets" version = "0.1.0" dependencies = [ "ciphersuite", - "generalized-bulletproofs", "generalized-bulletproofs-circuit-abstraction", "generic-array 1.1.0", - "zeroize", ] [[package]] @@ -3381,7 +3397,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -3911,7 +3927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2c1462f7..9ac449f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "crypto/evrf/circuit-abstraction", "crypto/evrf/divisors", "crypto/evrf/ec-gadgets", + "crypto/evrf", "crypto/dkg", "crypto/frost", diff --git a/crypto/evrf/Cargo.toml b/crypto/evrf/Cargo.toml new file mode 100644 index 00000000..3ec7897b --- /dev/null +++ b/crypto/evrf/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "evrf" +version = "0.1.0" +description = "Implementation of an eVRF premised on Generalized Bulletproofs" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/crypto/evrf" +authors = ["Luke Parker "] +keywords = ["ciphersuite", "ff", "group"] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +subtle = { version = "2", default-features = false, features = ["std"] } +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +rand_core = { version = "0.6", default-features = false, features = ["std"] } +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" } diff --git a/crypto/evrf/LICENSE b/crypto/evrf/LICENSE new file mode 100644 index 00000000..659881f1 --- /dev/null +++ b/crypto/evrf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypto/evrf/README.md b/crypto/evrf/README.md new file mode 100644 index 00000000..a03320e9 --- /dev/null +++ b/crypto/evrf/README.md @@ -0,0 +1,4 @@ +# eVRF + +An implementation of an [eVRF](https://eprint.iacr.org/2024/397.pdf) premised on +[Generalized Bulletproofs](https://repo.getmonero.org/monero-project/ccs-proposals/uploads/a9baa50c38c6312efc0fea5c6a188bb9/gbp.pdf). diff --git a/crypto/evrf/ec-gadgets/Cargo.toml b/crypto/evrf/ec-gadgets/Cargo.toml index f2cdc75f..cbd35639 100644 --- a/crypto/evrf/ec-gadgets/Cargo.toml +++ b/crypto/evrf/ec-gadgets/Cargo.toml @@ -13,11 +13,8 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } - generic-array = { version = "1", default-features = false, features = ["alloc"] } ciphersuite = { path = "../../ciphersuite", version = "0.4", default-features = false, features = ["std"] } -generalized-bulletproofs = { path = "../generalized-bulletproofs" } generalized-bulletproofs-circuit-abstraction = { path = "../circuit-abstraction" } diff --git a/crypto/evrf/ec-gadgets/src/dlog.rs b/crypto/evrf/ec-gadgets/src/dlog.rs index 96fc6502..353efffd 100644 --- a/crypto/evrf/ec-gadgets/src/dlog.rs +++ b/crypto/evrf/ec-gadgets/src/dlog.rs @@ -319,7 +319,7 @@ pub trait EcDlogGadgets { &self, transcript: &mut T, curve: &CurveSpec, - generators: &[&GeneratorTable], + generators: &[GeneratorTable], ) -> (DiscreteLogChallenge, Vec>); /// Prove this point has the specified discrete logarithm over the specified generator. @@ -350,7 +350,7 @@ impl EcDlogGadgets for Circuit { &self, transcript: &mut T, curve: &CurveSpec, - generators: &[&GeneratorTable], + generators: &[GeneratorTable], ) -> (DiscreteLogChallenge, Vec>) { // Get the challenge points // TODO: Implement a proper hash to curve diff --git a/crypto/evrf/src/lib.rs b/crypto/evrf/src/lib.rs new file mode 100644 index 00000000..06ffa35f --- /dev/null +++ b/crypto/evrf/src/lib.rs @@ -0,0 +1,477 @@ +use subtle::*; +use zeroize::{Zeroize, Zeroizing}; + +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +use generic_array::{typenum::Unsigned, ArrayLength, GenericArray}; + +use ciphersuite::{ + group::{ + ff::{Field, PrimeField, PrimeFieldBits}, + Group, GroupEncoding, + }, + Ciphersuite, +}; + +use generalized_bulletproofs::{ + *, + transcript::{Transcript as ProverTranscript, VerifierTranscript}, + arithmetic_circuit_proof::*, +}; +use generalized_bulletproofs_circuit_abstraction::*; + +use ec_divisors::{DivisorCurve, new_divisor}; +use generalized_bulletproofs_ec_gadgets::*; + +/// A curve to perform the eVRF with. +pub trait EvrfCurve: Ciphersuite { + type EmbeddedCurve: Ciphersuite; + type EmbeddedCurveParameters: DiscreteLogParameters; +} + +/// The result of proving for an eVRF. +pub struct EvrfProveResult { + pub scalars: Vec>, + pub proof: Vec, +} + +/// A struct to prove/verify eVRFs with. +pub struct Evrf; +impl Evrf { + fn seed_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; + + let mut rng = ChaCha20Rng::from_seed(seed); + let mut res = Vec::with_capacity(quantity); + while res.len() < quantity { + let mut repr = ::Repr::default(); + rng.fill_bytes(repr.as_mut()); + if let Ok(point) = C::read_G(&mut repr.as_ref()) { + res.push(point); + } + } + res + } + + fn point_with_dlogs( + quantity: usize, + generators_to_use: usize, +) -> Vec> { + let quantity = 2 * quantity; + + fn read_one_from_tape(generators_to_use: usize, start: &mut usize) -> Variable { + let commitment = *start / (2 * generators_to_use); + let index = *start % generators_to_use; + let res = if (*start / generators_to_use) % 2 == 0 { + Variable::CG { commitment, index } + } else { + Variable::CH { commitment, index } + }; + *start += 1; + res + } + fn read_from_tape( + generators_to_use: usize, + start: &mut usize, + ) -> GenericArray { + let mut buf = Vec::with_capacity(N::USIZE); + for _ in 0 .. N::USIZE { + buf.push(read_one_from_tape(generators_to_use, start)); + } + GenericArray::from_slice(&buf).clone() + } + + // We define a serialized tape of the discrete logarithm, then for each divisor/point: + // zero, x**i, y x**i, y, x_coord, y_coord + // We then chunk that into vector commitments + // Here, we take the assumed layout and generate the expected `Variable`s for this layout + let mut start = 0; + + let dlog = read_from_tape(generators_to_use, &mut start); + + let mut res = Vec::with_capacity(quantity); + for _ in 0 .. quantity { + 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); + let y = read_one_from_tape(generators_to_use, &mut start); + let divisor = Divisor { zero, x_from_power_of_2, yx, y }; + + let point = ( + read_one_from_tape(generators_to_use, &mut start), + read_one_from_tape(generators_to_use, &mut start), + ); + + res.push(PointWithDlog { dlog: dlog.clone(), divisor, point }); + } + res + } + + fn muls_and_generators_to_use(quantity: usize) -> (usize, usize) { + let expected_muls = 7 * (1 + (2 * quantity)); + let generators_to_use = { + let mut padded_pow_of_2 = 1; + while padded_pow_of_2 < expected_muls { + padded_pow_of_2 <<= 1; + } + // This may as small as 16, which would create an excessive amount of vector commitments + // We set a floor of 1024 rows for bandwidth reasons + padded_pow_of_2.max(1024) + }; + (expected_muls, generators_to_use) + } + + fn circuit( + curve_spec: &CurveSpec, + evrf_public_key: (C::F, C::F), + quantity: usize, + generator_tables: &[GeneratorTable], + circuit: &mut Circuit, + transcript: &mut impl Transcript, + ) { + let (expected_muls, generators_to_use) = Self::muls_and_generators_to_use(quantity); + let (challenge, challenged_generators) = + circuit.discrete_log_challenge(transcript, curve_spec, generator_tables); + + let mut point_with_dlogs = + Self::point_with_dlogs::(quantity, generators_to_use).into_iter(); + + // Verify the DLog claims for the sampled points + for (i, pair) in challenged_generators.chunks(2).take(quantity).enumerate() { + let mut lincomb = LinComb::empty(); + debug_assert_eq!(pair.len(), 2); + for challenged_generator in pair { + let point = circuit.discrete_log( + curve_spec, + point_with_dlogs.next().unwrap(), + &challenge, + challenged_generator, + ); + // For each point in this pair, add its x coordinate to a lincomb + lincomb = lincomb.term(C::F::ONE, point.x()); + } + // Constrain the sum of the two x coordinates to be equal to the value in the Pedersen + // commitment + circuit.equality(lincomb, &LinComb::from(Variable::V(i))); + } + + let point = circuit.discrete_log( + curve_spec, + point_with_dlogs.next().unwrap(), + &challenge, + challenged_generators.last().unwrap(), + ); + circuit.equality(LinComb::from(point.x()), &LinComb::empty().constant(evrf_public_key.0)); + circuit.equality(LinComb::from(point.y()), &LinComb::empty().constant(evrf_public_key.1)); + + debug_assert_eq!(expected_muls, circuit.muls()); + debug_assert!(point_with_dlogs.next().is_none()); + } + + /// Prove a point on an elliptic curve had its discrete logarithm generated via an eVRF. + pub fn prove( + rng: &mut (impl RngCore + CryptoRng), + generators: &Generators, + evrf_private_key: <::EmbeddedCurve as Ciphersuite>::F, + seed: [u8; 32], + quantity: usize, + ) -> Result, AcError> + where + <::EmbeddedCurve as Ciphersuite>::G: + DivisorCurve::F>, + { + let curve_spec = CurveSpec { + a: <::EmbeddedCurve as Ciphersuite>::G::a(), + b: <::EmbeddedCurve as Ciphersuite>::G::b(), + }; + + let points = Self::seed_to_points::(seed, quantity); + + let num_bits: u32 = <::EmbeddedCurve as Ciphersuite>::F::NUM_BITS; + + // Obtain the bits of the private key + let mut sum_of_coefficients: u32 = 0; + let mut dlog = vec![::F::ZERO; num_bits as usize]; + for (i, bit) in evrf_private_key.to_le_bits().into_iter().take(num_bits as usize).enumerate() { + let bit = Choice::from(u8::from(bit)); + dlog[i] = + <_>::conditional_select(&::F::ZERO, &::F::ONE, bit); + sum_of_coefficients += u32::conditional_select(&0, &1, bit); + } + + /* + Now that we have the discrete logarithm as the coefficients 0/1 for a polynomial of 2**i, we + want to malleate it such that the sum of its coefficients is NUM_BITS. The divisor + calculcation is a non-trivial amount of work and would be extremely vulnerable to timing + attacks without such efforts. + + We find the highest non-0 coefficient, decrement it, and increase the prior coefficient by 2. + This increase the sum of the coefficients by 1. + */ + let two = ::F::ONE.double(); + for _ in 0 .. num_bits { + // Find the highest coefficient currently non-zero + let mut h = 1u32; + // The value of this highest coefficient, and the coefficient prior to it + let mut h_value = dlog[h as usize]; + let mut h_prior_value = dlog[(h as usize) - 1]; + + 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); + + // Set `h_*` if this value is non-0 + h = u32::conditional_select(&h, &(i as u32), !is_zero); + h_value = ::F::conditional_select(&h_value, scalar, !is_zero); + h_prior_value = + ::F::conditional_select(&h_prior_value, &prior_scalar, !is_zero); + + // Update prior_scalar + prior_scalar = *scalar; + } + + // We should not have selected a value equivalent to 0 + // TODO: Ban evrf keys < NUM_BITS and accordingly unable to be so coerced + // TODO: Preprocess this decomposition of the eVRF key? + assert!(!bool::from(h_value.ct_eq(&::F::ZERO))); + + // Update h_value, h_prior_value as necessary + h_value -= ::F::ONE; + h_prior_value += two; + + // Now, set these values if we should + let should_set = !sum_of_coefficients.ct_eq(&num_bits); + sum_of_coefficients += u32::conditional_select(&0, &1, should_set); + for (i, scalar) in dlog.iter_mut().enumerate() { + let this_is_prior = (i as u32).ct_eq(&(h - 1)); + let this_is_high = (i as u32).ct_eq(&h); + + *scalar = <_>::conditional_select(scalar, &h_prior_value, should_set & this_is_prior); + *scalar = <_>::conditional_select(scalar, &h_value, should_set & this_is_high); + } + } + debug_assert!(bool::from( + dlog + .iter() + .sum::<::F>() + .ct_eq(&::F::from(u64::from(num_bits))) + )); + + // A tape of the discrete logarithm, then [zero, x**i, y x**i, y, x_coord, y_coord] + let mut vector_commitment_tape = vec![]; + + // Start by pushing the discrete logarithm onto the tape + for coefficient in &dlog { + vector_commitment_tape.push(*coefficient); + } + + let mut generator_tables = Vec::with_capacity(1 + (2 * quantity)); + + // A function to calculate a divisor and push it onto the tape + // This defines a vec, divisor_points, outside of the fn to reuse its allocation + let mut divisor_points = Vec::with_capacity((num_bits as usize) + 1); + let mut divisor = |mut generator: <::EmbeddedCurve as Ciphersuite>::G| { + { + let (x, y) = ::G::to_xy(generator).unwrap(); + generator_tables.push(GeneratorTable::new(&curve_spec, x, y)); + } + + let dh = generator * evrf_private_key; + { + for coefficient in &dlog { + let mut coefficient = *coefficient; + while coefficient != ::F::ZERO { + coefficient -= ::F::ONE; + divisor_points.push(generator); + } + generator = generator.double(); + } + } + divisor_points.push(-dh); + let mut divisor = new_divisor(&divisor_points).unwrap().normalize_x_coefficient(); + divisor_points.zeroize(); + + vector_commitment_tape.push(divisor.zero_coefficient); + + for coefficient in divisor.x_coefficients.iter().skip(1) { + vector_commitment_tape.push(*coefficient); + } + for _ in divisor.x_coefficients.len() .. + ::XCoefficientsMinusOne::USIZE + { + vector_commitment_tape.push(::F::ZERO); + } + + for coefficient in divisor.yx_coefficients.first().unwrap_or(&vec![]) { + vector_commitment_tape.push(*coefficient); + } + for _ in divisor.yx_coefficients.first().unwrap_or(&vec![]).len() .. + ::YxCoefficients::USIZE + { + vector_commitment_tape.push(::F::ZERO); + } + + vector_commitment_tape + .push(divisor.y_coefficients.first().cloned().unwrap_or(::F::ZERO)); + + divisor.zeroize(); + drop(divisor); + + let (x, y) = ::G::to_xy(dh).unwrap(); + vector_commitment_tape.push(x); + vector_commitment_tape.push(y); + + (x, y) + }; + + // Push a divisor for each point we use in the eVRF + let mut scalars = Vec::with_capacity(quantity); + for pair in points.chunks(2) { + let mut res = Zeroizing::new(C::F::ZERO); + for point in pair { + let (dh_x, _) = divisor(*point); + *res += dh_x; + } + scalars.push(res); + } + debug_assert_eq!(scalars.len(), quantity); + + // Also push a divisor for proving that we're using the correct scalar + let evrf_public_key = divisor(<::EmbeddedCurve as Ciphersuite>::generator()); + + dlog.zeroize(); + drop(dlog); + + // Now that we have the vector commitment tape, chunk it + let (_, generators_to_use) = Self::muls_and_generators_to_use(quantity); + + let mut vector_commitments = + Vec::with_capacity(vector_commitment_tape.len().div_ceil(generators_to_use)); + for chunk in vector_commitment_tape.chunks(generators_to_use * 2) { + let g_values = chunk[.. generators_to_use].to_vec().into(); + let h_values = chunk[generators_to_use ..].to_vec().into(); + vector_commitments.push(PedersenVectorCommitment { + g_values, + h_values, + mask: C::F::random(&mut *rng), + }); + } + + vector_commitment_tape.zeroize(); + drop(vector_commitment_tape); + + let mut commitments = Vec::with_capacity(quantity); + for scalar in &scalars { + commitments.push(PedersenCommitment { value: **scalar, mask: C::F::random(&mut *rng) }); + } + + let mut transcript = ProverTranscript::new(seed); + let commited_commitments = transcript.write_commitments( + vector_commitments + .iter() + .map(|commitment| { + commitment + .commit(generators.g_bold_slice(), generators.h_bold_slice(), generators.h()) + .ok_or(AcError::NotEnoughGenerators) + }) + .collect::>()?, + commitments + .iter() + .map(|commitment| commitment.commit(generators.g(), generators.h())) + .collect(), + ); + + let mut circuit = Circuit::prove(vector_commitments, commitments); + Self::circuit::( + &curve_spec, + evrf_public_key, + quantity, + &generator_tables, + &mut circuit, + &mut transcript, + ); + + let (statement, Some(witness)) = circuit + .statement( + generators.reduce(generators_to_use).ok_or(AcError::NotEnoughGenerators)?, + commited_commitments, + ) + .unwrap() + else { + panic!("proving yet wasn't yielded the witness"); + }; + statement.prove(rng, &mut transcript, witness).unwrap(); + + Ok(EvrfProveResult { scalars, proof: transcript.complete() }) + } + + // TODO: Dedicated error + /// Verify an eVRF proof, returning the commitments output. + pub fn verify( + rng: &mut (impl RngCore + CryptoRng), + generators: &Generators, + verifier: &mut BatchVerifier, + evrf_public_key: <::EmbeddedCurve as Ciphersuite>::G, + seed: [u8; 32], + quantity: usize, + proof: &[u8], + ) -> Result, ()> + where + <::EmbeddedCurve as Ciphersuite>::G: + DivisorCurve::F>, + { + let curve_spec = CurveSpec { + a: <::EmbeddedCurve as Ciphersuite>::G::a(), + b: <::EmbeddedCurve as Ciphersuite>::G::b(), + }; + + let points = Self::seed_to_points::(seed, quantity); + let mut generator_tables = Vec::with_capacity(1 + (2 * quantity)); + + for generator in points { + let (x, y) = ::G::to_xy(generator).unwrap(); + generator_tables.push(GeneratorTable::new(&curve_spec, x, y)); + } + { + let (x, y) = + ::G::to_xy(::generator()) + .unwrap(); + generator_tables.push(GeneratorTable::new(&curve_spec, x, y)); + } + + let (_, generators_to_use) = Self::muls_and_generators_to_use(quantity); + + let mut transcript = VerifierTranscript::new(seed, 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 commitments = transcript.read_commitments(vcs, quantity).map_err(|_| ())?; + + let mut circuit = Circuit::verify(); + Self::circuit::( + &curve_spec, + // TODO: Use a better error here + ::G::to_xy(evrf_public_key).ok_or(())?, + quantity, + &generator_tables, + &mut circuit, + &mut transcript, + ); + + let (statement, None) = + circuit.statement(generators.reduce(generators_to_use).ok_or(())?, commitments).unwrap() + else { + panic!("verifying yet was yielded a witness"); + }; + + statement.verify(rng, verifier, &mut transcript).map_err(|_| ())?; + + // TODO: Unblinded PCs + Ok(vec![]) + } +}