use core::fmt; use std_shims::{vec, vec::Vec}; use ciphersuite::{ group::ff::{Field, PrimeField, BatchInverter}, Ciphersuite, }; use generalized_bulletproofs_circuit_abstraction::*; use crate::*; /// Parameters for a discrete logarithm proof. pub trait DiscreteLogParameters { /// The amount of bits used to represent a scalar. type ScalarBits: ArrayLength; /// The amount of x**i coefficients in a divisor. /// /// This is the amount of points in a divisor (the amount of bits in a scalar, plus one) divided /// by two. type XCoefficients: ArrayLength; /// The amount of x**i coefficients in a divisor, minus one. type XCoefficientsMinusOne: ArrayLength; /// The amount of y x**i coefficients in a divisor. /// /// This is the amount of points in a divisor (the amount of bits in a scalar, plus one) divided /// by two, minus two. type YxCoefficients: ArrayLength; } /// A tabled generator for proving/verifying discrete logarithm claims. #[derive(Clone)] pub struct GeneratorTable( GenericArray<(F, F), Parameters::ScalarBits>, ); impl fmt::Debug for GeneratorTable { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt .debug_struct("GeneratorTable") .field("x", &self.0[0].0) .field("y", &self.0[0].1) .finish_non_exhaustive() } } impl GeneratorTable { /// Create a new table for this generator. /// /// The generator is assumed to be well-formed and on-curve. This function may panic if it's not. pub fn new(curve: &CurveSpec, generator_x: F, generator_y: F) -> Self { // mdbl-2007-bl fn dbl(a: F, x1: F, y1: F) -> (F, F) { let xx = x1 * x1; let w = a + (xx + xx.double()); let y1y1 = y1 * y1; let r = y1y1 + y1y1; let sss = (y1 * r).double().double(); let rr = r * r; let b = x1 + r; let b = (b * b) - xx - rr; let h = (w * w) - b.double(); let x3 = h.double() * y1; let y3 = (w * (b - h)) - rr.double(); let z3 = sss; // Normalize from XYZ to XY let z3_inv = z3.invert().unwrap(); let x3 = x3 * z3_inv; let y3 = y3 * z3_inv; (x3, y3) } let mut res = Self(GenericArray::default()); res.0[0] = (generator_x, generator_y); for i in 1 .. Parameters::ScalarBits::USIZE { let last = res.0[i - 1]; res.0[i] = dbl(curve.a, last.0, last.1); } res } } /// A representation of the divisor. /// /// The coefficient for x**1 is explicitly excluded as it's expected to be normalized to 1. #[derive(Clone)] pub struct Divisor { /// The coefficient for the `y` term of the divisor. /// /// There is never more than one `y**i x**0` coefficient as the leading term of the modulus is /// `y**2`. It's assumed the coefficient is non-zero (and present) as it will be for any divisor /// exceeding trivial complexity. pub y: Variable, /// The coefficients for the `y**1 x**i` terms of the polynomial. pub yx: GenericArray, /// The coefficients for the `x**i` terms of the polynomial, skipping x**1. /// /// x**1 is skipped as it's expected to be normalized to 1, and therefore constant, in order to /// ensure the divisor is non-zero (as necessary for the proof to be complete). // Subtract 1 from the length due to skipping the coefficient for x**1 pub x_from_power_of_2: GenericArray, /// The constant term in the polynomial (alternatively, the coefficient for y**0 x**0). pub zero: Variable, } /// A point, its discrete logarithm, and the divisor to prove it. #[derive(Clone)] pub struct PointWithDlog { /// The point which is supposedly the result of scaling the generator by the discrete logarithm. pub point: (Variable, Variable), /// The discrete logarithm, represented as coefficients of a polynomial of 2**i. pub dlog: GenericArray, /// The divisor interpolating the relevant doublings of generator with the inverse of the point. pub divisor: Divisor, } /// A struct containing a point used for the evaluation of a divisor. /// /// Preprocesses and caches as much of the calculation as possible to minimize work upon reuse of /// challenge points. struct ChallengePoint { y: F, yx: GenericArray, x: GenericArray, p_0_n_0: F, x_p_0_n_0: GenericArray, p_1_n: F, p_1_d: F, } impl ChallengePoint { fn new( curve: &CurveSpec, // The slope between all of the challenge points slope: F, // The x and y coordinates x: F, y: F, // The inversion of twice the y coordinate // We accept this as an argument so that the caller can calculcate these with a batch inversion inv_two_y: F, ) -> Self { // Powers of x, skipping x**0 let divisor_x_len = Parameters::XCoefficients::USIZE; let mut x_pows = GenericArray::default(); x_pows[0] = x; for i in 1 .. divisor_x_len { let last = x_pows[i - 1]; x_pows[i] = last * x; } // Powers of x multiplied by y let divisor_yx_len = Parameters::YxCoefficients::USIZE; let mut yx = GenericArray::default(); // Skips x**0 yx[0] = y * x; for i in 1 .. divisor_yx_len { let last = yx[i - 1]; yx[i] = last * x; } let x_sq = x.square(); let three_x_sq = x_sq.double() + x_sq; let three_x_sq_plus_a = three_x_sq + curve.a; let two_y = y.double(); // p_0_n_0 from `DivisorChallenge` let p_0_n_0 = three_x_sq_plus_a * inv_two_y; let mut x_p_0_n_0 = GenericArray::default(); // Since this iterates over x, which skips x**0, this also skips p_0_n_0 x**0 for (i, x) in x_pows.iter().take(divisor_yx_len).enumerate() { x_p_0_n_0[i] = p_0_n_0 * x; } // p_1_n from `DivisorChallenge` let p_1_n = two_y; // p_1_d from `DivisorChallenge` let p_1_d = (-slope * p_1_n) + three_x_sq_plus_a; ChallengePoint { x: x_pows, y, yx, p_0_n_0, x_p_0_n_0, p_1_n, p_1_d } } } // `DivisorChallenge` from the section `Discrete Log Proof` fn divisor_challenge_eval( circuit: &mut Circuit, divisor: &Divisor, challenge: &ChallengePoint, ) -> Variable { // The evaluation of the divisor differentiated by y, further multiplied by p_0_n_0 // Differentation drops everything without a y coefficient, and drops what remains by a power // of y // (y**1 -> y**0, yx**i -> x**i) // This aligns with p_0_n_1 from `DivisorChallenge` let p_0_n_1 = { let mut p_0_n_1 = LinComb::empty().term(challenge.p_0_n_0, divisor.y); for (j, var) in divisor.yx.iter().enumerate() { // This does not raise by `j + 1` as x_p_0_n_0 omits x**0 p_0_n_1 = p_0_n_1.term(challenge.x_p_0_n_0[j], *var); } p_0_n_1 }; // The evaluation of the divisor differentiated by x // This aligns with p_0_n_2 from `DivisorChallenge` let p_0_n_2 = { // The coefficient for x**1 is 1, so 1 becomes the new zero coefficient let mut p_0_n_2 = LinComb::empty().constant(C::F::ONE); // Handle the new y coefficient p_0_n_2 = p_0_n_2.term(challenge.y, divisor.yx[0]); // Handle the new yx coefficients for (j, yx) in divisor.yx.iter().enumerate().skip(1) { // For the power which was shifted down, we multiply this coefficient // 3 x**2 -> 2 * 3 x**1 let original_power_of_x = C::F::from(u64::try_from(j + 1).unwrap()); // `j - 1` so `j = 1` indexes yx[0] as yx[0] is the y x**1 // (yx omits y x**0) let this_weight = original_power_of_x * challenge.yx[j - 1]; p_0_n_2 = p_0_n_2.term(this_weight, *yx); } // Handle the x coefficients // We don't skip the first one as `x_from_power_of_2` already omits x**1 for (i, x) in divisor.x_from_power_of_2.iter().enumerate() { // i + 2 as the paper expects i to start from 1 and be + 1, yet we start from 0 let original_power_of_x = C::F::from(u64::try_from(i + 2).unwrap()); // Still x[i] as x[0] is x**1 let this_weight = original_power_of_x * challenge.x[i]; p_0_n_2 = p_0_n_2.term(this_weight, *x); } p_0_n_2 }; // p_0_n from `DivisorChallenge` let p_0_n = p_0_n_1 + &p_0_n_2; // Evaluation of the divisor // p_0_d from `DivisorChallenge` let p_0_d = { let mut p_0_d = LinComb::empty().term(challenge.y, divisor.y); for (var, c_yx) in divisor.yx.iter().zip(&challenge.yx) { p_0_d = p_0_d.term(*c_yx, *var); } for (i, var) in divisor.x_from_power_of_2.iter().enumerate() { // This `i+1` is preserved, despite most not being as x omits x**0, as this assumes we // start with `i=1` p_0_d = p_0_d.term(challenge.x[i + 1], *var); } // Adding x effectively adds a `1 x` term, ensuring the divisor isn't 0 p_0_d.term(C::F::ONE, divisor.zero).constant(challenge.x[0]) }; // Calculate the joint numerator // p_n from `DivisorChallenge` let p_n = p_0_n * challenge.p_1_n; // Calculate the joint denominator // p_d from `DivisorChallenge` let p_d = p_0_d * challenge.p_1_d; // We want `n / d = o` // `n / d = o` == `n = d * o` // These are safe unwraps as they're solely done by the prover and should always be non-zero let witness = circuit.eval(&p_d).map(|p_d| (p_d, circuit.eval(&p_n).unwrap() * p_d.invert().unwrap())); let (_l, o, n_claim) = circuit.mul(Some(p_d), None, witness); circuit.equality(p_n, &n_claim.into()); o } /// A challenge to evaluate divisors with. /// /// This challenge must be sampled after writing the commitments to the transcript. This challenge /// is reusable across various divisors. pub struct DiscreteLogChallenge { c0: ChallengePoint, c1: ChallengePoint, c2: ChallengePoint, slope: F, intercept: F, } /// A generator which has been challenged and is ready for use in evaluating discrete logarithm /// claims. pub struct ChallengedGenerator( GenericArray, ); /// Gadgets for proving the discrete logarithm of points on an elliptic curve defined over the /// scalar field of the curve of the Bulletproof. pub trait EcDlogGadgets { /// Sample a challenge for a series of discrete logarithm claims. /// /// This must be called after writing the commitments to the transcript. /// /// The generators are assumed to be non-empty. They are not transcripted. If your generators are /// dynamic, they must be properly transcripted into the context. /// /// May panic/have undefined behavior if an assumption is broken. #[allow(clippy::type_complexity)] fn discrete_log_challenge( &self, transcript: &mut T, curve: &CurveSpec, generators: &[&GeneratorTable], ) -> (DiscreteLogChallenge, Vec>); /// Prove this point has the specified discrete logarithm over the specified generator. /// /// The discrete logarithm is not validated to be in a canonical form. The only guarantee made on /// it is that it's a consistent representation of _a_ discrete logarithm (reuse won't enable /// re-interpretation as a distinct discrete logarithm). /// /// This does ensure the point is on-curve. /// /// This MUST only be called with `Variable`s present within commitments. /// /// May panic/have undefined behavior if an assumption is broken, or if passed an invalid /// witness. fn discrete_log( &mut self, curve: &CurveSpec, point: PointWithDlog, challenge: &DiscreteLogChallenge, challenged_generator: &ChallengedGenerator, ) -> OnCurve; } impl EcDlogGadgets for Circuit { // This is part of `DiscreteLog` from `Discrete Log Proof`, specifically, the challenges and // the calculations dependent solely on them fn discrete_log_challenge( &self, transcript: &mut T, curve: &CurveSpec, generators: &[&GeneratorTable], ) -> (DiscreteLogChallenge, Vec>) { // Get the challenge points let sign_of_points = transcript.challenge_bytes(); let sign_of_point_0 = (sign_of_points[0] & 1) == 1; let sign_of_point_1 = ((sign_of_points[0] >> 1) & 1) == 1; let (c0_x, c0_y) = loop { let c0_x = transcript.challenge::(); let Some(c0_y) = Option::::from(((c0_x.square() * c0_x) + (curve.a * c0_x) + curve.b).sqrt()) else { continue; }; // Takes the even y coordinate as to not be dependent on whatever root the above sqrt // happens to returns break (c0_x, if bool::from(c0_y.is_odd()) != sign_of_point_0 { -c0_y } else { c0_y }); }; let (c1_x, c1_y) = loop { let c1_x = transcript.challenge::(); let Some(c1_y) = Option::::from(((c1_x.square() * c1_x) + (curve.a * c1_x) + curve.b).sqrt()) else { continue; }; break (c1_x, if bool::from(c1_y.is_odd()) != sign_of_point_1 { -c1_y } else { c1_y }); }; // mmadd-1998-cmo fn incomplete_add(x1: F, y1: F, x2: F, y2: F) -> Option<(F, F)> { if x1 == x2 { None? } let u = y2 - y1; let uu = u * u; let v = x2 - x1; let vv = v * v; let vvv = v * vv; let r = vv * x1; let a = uu - vvv - r.double(); let x3 = v * a; let y3 = (u * (r - a)) - (vvv * y1); let z3 = vvv; // Normalize from XYZ to XY let z3_inv = Option::::from(z3.invert())?; let x3 = x3 * z3_inv; let y3 = y3 * z3_inv; Some((x3, y3)) } let (c2_x, c2_y) = incomplete_add::(c0_x, c0_y, c1_x, c1_y) .expect("randomly selected points shared an x coordinate"); // We want C0, C1, C2 = -(C0 + C1) let c2_y = -c2_y; // Calculate the slope and intercept // Safe invert as these x coordinates must be distinct due to passing the above incomplete_add let slope = (c1_y - c0_y) * (c1_x - c0_x).invert().unwrap(); let intercept = c0_y - (slope * c0_x); // Calculate the inversions for 2 c_y (for each c) and all of the challenged generators let mut inversions = vec![C::F::ZERO; 3 + (generators.len() * Parameters::ScalarBits::USIZE)]; // Needed for the left-hand side eval { inversions[0] = c0_y.double(); inversions[1] = c1_y.double(); inversions[2] = c2_y.double(); } // Perform the inversions for the generators for (i, generator) in generators.iter().enumerate() { // Needed for the right-hand side eval for (j, generator) in generator.0.iter().enumerate() { // `DiscreteLog` has weights of `(mu - (G_i.y + (slope * G_i.x)))**-1` in its last line inversions[3 + (i * Parameters::ScalarBits::USIZE) + j] = intercept - (generator.1 - (slope * generator.0)); } } for challenge_inversion in &inversions { // This should be unreachable barring negligible probability if challenge_inversion.is_zero().into() { panic!("trying to invert 0"); } } let mut scratch = vec![C::F::ZERO; inversions.len()]; let _ = BatchInverter::invert_with_external_scratch(&mut inversions, &mut scratch); let mut inversions = inversions.into_iter(); let inv_c0_two_y = inversions.next().unwrap(); let inv_c1_two_y = inversions.next().unwrap(); let inv_c2_two_y = inversions.next().unwrap(); let c0 = ChallengePoint::new(curve, slope, c0_x, c0_y, inv_c0_two_y); let c1 = ChallengePoint::new(curve, slope, c1_x, c1_y, inv_c1_two_y); let c2 = ChallengePoint::new(curve, slope, c2_x, c2_y, inv_c2_two_y); // Fill in the inverted values let mut challenged_generators = Vec::with_capacity(generators.len()); for _ in 0 .. generators.len() { let mut challenged_generator = GenericArray::default(); for i in 0 .. Parameters::ScalarBits::USIZE { challenged_generator[i] = inversions.next().unwrap(); } challenged_generators.push(ChallengedGenerator(challenged_generator)); } (DiscreteLogChallenge { c0, c1, c2, slope, intercept }, challenged_generators) } // `DiscreteLog` from `Discrete Log Proof` fn discrete_log( &mut self, curve: &CurveSpec, point: PointWithDlog, challenge: &DiscreteLogChallenge, challenged_generator: &ChallengedGenerator, ) -> OnCurve { let PointWithDlog { divisor, dlog, point } = point; // Ensure this is being safely called let arg_iter = [point.0, point.1, divisor.y, divisor.zero]; let arg_iter = arg_iter.iter().chain(divisor.yx.iter()); let arg_iter = arg_iter.chain(divisor.x_from_power_of_2.iter()); let arg_iter = arg_iter.chain(dlog.iter()); for variable in arg_iter { debug_assert!( matches!(variable, Variable::CG { .. } | Variable::V(_)), "discrete log proofs requires all arguments belong to commitments", ); } // Check the point is on curve let point = self.on_curve(curve, point); // The challenge has already been sampled so those lines aren't necessary // lhs from the paper, evaluating the divisor let lhs_eval = LinComb::from(divisor_challenge_eval(self, &divisor, &challenge.c0)) + &LinComb::from(divisor_challenge_eval(self, &divisor, &challenge.c1)) + &LinComb::from(divisor_challenge_eval(self, &divisor, &challenge.c2)); // Interpolate the doublings of the generator let mut rhs_eval = LinComb::empty(); // We call this `bit` yet it's not constrained to being a bit // It's presumed to be yet may be malleated for (bit, weight) in dlog.into_iter().zip(&challenged_generator.0) { rhs_eval = rhs_eval.term(*weight, bit); } // Interpolate the output point // intercept - (y - (slope * x)) // intercept - y + (slope * x) // -y + (slope * x) + intercept // EXCEPT the output point we're proving the discrete log for isn't the one interpolated // Its negative is, so -y becomes y // y + (slope * x) + intercept let output_interpolation = LinComb::empty() .constant(challenge.intercept) .term(C::F::ONE, point.y) .term(challenge.slope, point.x); let output_interpolation_eval = self.eval(&output_interpolation); let (_output_interpolation, inverse) = self.inverse(Some(output_interpolation), output_interpolation_eval); rhs_eval = rhs_eval.term(C::F::ONE, inverse); self.equality(lhs_eval, &rhs_eval); point } }