diff --git a/g16ckt/src/gadgets/bn254/fq.rs b/g16ckt/src/gadgets/bn254/fq.rs index 8f1cb04..73d13c2 100644 --- a/g16ckt/src/gadgets/bn254/fq.rs +++ b/g16ckt/src/gadgets/bn254/fq.rs @@ -297,6 +297,15 @@ impl Fq { &BigUint::from_str(Self::MODULUS_ADD_1_DIV_4).unwrap(), ) } + + /// Return a>b in standard form given inputs in montgomery form + pub fn greater_than(circuit: &mut C, a: &Fq, b: &Fq) -> WireId { + // First convert the inputs 'a' and 'b' back to standard form + let a = Fq::mul_by_constant_montgomery(circuit, a, &ark_bn254::Fq::ONE); + let b = Fq::mul_by_constant_montgomery(circuit, b, &ark_bn254::Fq::ONE); + // only now perform comparison + bigint::greater_than(circuit, &a, &b) + } } #[cfg(test)] @@ -304,7 +313,8 @@ pub(super) mod tests { use std::{array, iter}; use ark_ff::AdditiveGroup; - use rand::Rng; + use rand::{Rng, SeedableRng}; + use rand_chacha::ChaCha20Rng; use test_log::test; use tracing::trace; @@ -680,6 +690,29 @@ pub(super) mod tests { assert_eq!(result.output_value.value, expected_c); } + #[test] + fn test_fq_sqrt_montgomery_roundtrip() { + let mut rng = ChaCha20Rng::seed_from_u64(42); + for _ in 0..5 { + let aa_v = Fq::random(&mut rng); + let sqrt_exists = aa_v.sqrt().is_some(); + + let aa_montgomery = Fq::as_montgomery(aa_v); + let input = FqInput::new([aa_montgomery]); + + let result = + CircuitBuilder::streaming_execute::<_, _, FqOutput>(input, 10_000, |ctx, input| { + let [aa_wire] = input; + let sqrt = Fq::sqrt_montgomery(ctx, aa_wire); + Fq::square_montgomery(ctx, &sqrt) + }); + + let calc_aa_montgomery = result.output_value.value; + + assert_eq!(sqrt_exists, calc_aa_montgomery == aa_montgomery); + } + } + #[test] fn test_fq_multiplexer() { let w = 1; diff --git a/g16ckt/src/gadgets/bn254/fq2.rs b/g16ckt/src/gadgets/bn254/fq2.rs index d1f6100..726ad09 100644 --- a/g16ckt/src/gadgets/bn254/fq2.rs +++ b/g16ckt/src/gadgets/bn254/fq2.rs @@ -15,7 +15,8 @@ use crate::{ CircuitContext, Gate, WireId, circuit::WiresObject, gadgets::{ - bigint::{BigIntWires, select}, + basic, + bigint::{self, BigIntWires, select}, bn254::{fp254impl::Fp254Impl, fq::Fq}, }, }; @@ -444,6 +445,14 @@ impl Fq2 { Fq2::from_components(c0_final, c1_final) } + + /// Return a>b in standard form given inputs in montgomery form + pub fn greater_than(circuit: &mut C, a: &Fq2, b: &Fq2) -> WireId { + let c1_equal = bigint::equal(circuit, a.c1(), b.c1()); + let c1_greater = Fq::greater_than(circuit, a.c1(), b.c1()); + let c0_greater = Fq::greater_than(circuit, a.c0(), b.c0()); + basic::selector(circuit, c0_greater, c1_greater, c1_equal) + } } #[cfg(test)] diff --git a/g16ckt/src/gadgets/bn254/g1.rs b/g16ckt/src/gadgets/bn254/g1.rs index faf6f58..941f004 100644 --- a/g16ckt/src/gadgets/bn254/g1.rs +++ b/g16ckt/src/gadgets/bn254/g1.rs @@ -1,12 +1,17 @@ use std::{cmp::min, collections::HashMap, iter}; -use ark_ff::Zero; +use ark_ec::short_weierstrass::SWCurveConfig; +use ark_ff::{Field, PrimeField, Zero}; use circuit_component_macro::component; +use num_bigint::BigUint; use crate::{ CircuitContext, WireId, - circuit::{FromWires, WiresObject}, - gadgets::bn254::{fp254impl::Fp254Impl, fq::Fq, fr::Fr}, + circuit::{FromWires, TRUE_WIRE, WiresArity, WiresObject}, + gadgets::{ + bigint::{self, BigIntWires}, + bn254::{fp254impl::Fp254Impl, fq::Fq, fr::Fr}, + }, }; #[derive(Clone, Debug)] @@ -407,12 +412,199 @@ impl G1Projective { z: p.z.clone(), } } + + /// check whether or not the point is on the curve or not + /// checks y^2=x^3+3z^6 (Jacobian projective coordinates) + #[component] + pub fn is_on_curve(circuit: &mut C, p: &G1Projective) -> WireId { + let x2 = Fq::square_montgomery(circuit, &p.x); + let x3 = Fq::mul_montgomery(circuit, &p.x, &x2); + let y2 = Fq::square_montgomery(circuit, &p.y); + let z2 = Fq::square_montgomery(circuit, &p.z); + let z4 = Fq::square_montgomery(circuit, &z2); + let z6 = Fq::mul_montgomery(circuit, &z2, &z4); + let triplez6 = Fq::triple(circuit, &z6); // because ark_bn254::g1::Config::COEFF_B is 3 + let temp = Fq::add(circuit, &x3, &triplez6); + let should_be_zero = Fq::sub(circuit, &y2, &temp); + bigint::equal_zero(circuit, &should_be_zero.0) + } + + /// Deserialize into G1Projective from its 32 byte serialized bit representation. + // Follows arkworks implementation here: + // https://github.com/arkworks-rs/algebra/blob/v0.5.0/ec/src/models/short_weierstrass/mod.rs#L145 + pub fn deserialize_checked( + circuit: &mut C, + serialized_bits: [WireId; 32 * 8], + ) -> DecompressedG1Wires { + let (x_m, is_x_m_valid, flag) = { + let (num, flag) = serialized_bits.split_at(Fq::N_BITS); + let a = BigIntWires { bits: num.to_vec() }; + // check BigUint is a valid fq + let r: BigUint = ark_bn254::Fq::MODULUS.into(); + let valid_fq = bigint::less_than_constant(circuit, &a, &r); + // convert input field element in standard form into montgomery form + let r = Fq::as_montgomery(ark_bn254::Fq::ONE); + let a_mont = Fq::mul_by_constant_montgomery(circuit, &Fq(a), &r.square()); + // flag_0 is lsb, flag 1 is msb + (a_mont, valid_fq, [flag[0], flag[1]]) + }; + + // Part 1: Extract Flags + + let is_y_positive = { + // In arkworks, given: + // const Y_IS_POSITIVE: u8 = 0; + let flag_or = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag[0], + wire_b: flag[1], + wire_c: flag_or, + gate_type: crate::GateType::Or, + }); + let flag_nor = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag_or, + wire_b: TRUE_WIRE, + wire_c: flag_nor, + gate_type: crate::GateType::Xor, + }); + flag_nor + }; + + let is_y_negative = { + // In arkworks, given: + // const Y_IS_NEGATIVE: u8 = 1 << 7; + let tmp0 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag[0], + wire_b: TRUE_WIRE, + wire_c: tmp0, + gate_type: crate::GateType::Xor, + }); + let tmp1 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag[1], + wire_b: tmp0, + wire_c: tmp1, + gate_type: crate::GateType::And, + }); + tmp1 + }; + + // rest of the flags (11 and 01) represent identity and None, so are invalid flags + let flags_is_valid = { + let tmp0 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: is_y_positive, + wire_b: is_y_negative, + wire_c: tmp0, + gate_type: crate::GateType::Or, + }); + tmp0 + }; + + // Part 2: compute (X, Y, Z) + + // rhs = x^3 + b (Montgomery domain) + let x2 = Fq::square_montgomery(circuit, &x_m); + let x3 = Fq::mul_montgomery(circuit, &x2, &x_m); + let b_m = Fq::as_montgomery(ark_bn254::g1::Config::COEFF_B); + let rhs = Fq::add_constant(circuit, &x3, &b_m); + + // sy = sqrt(rhs) in Montgomery domain + let sy = Fq::sqrt_montgomery(circuit, &rhs); + let rhs_is_qr = { + // if sqrt doesn't exist, sy squared is not equal to the original number + // this means (x, y) doesn't yield a point in the curve + let sy_sy = Fq::square_montgomery(circuit, &sy); + bigint::equal(circuit, &sy_sy, &rhs) + }; + + // analogous to get_point_from_x_unchecked + let sy_neg = Fq::neg(circuit, &sy); + let sy_neg_greater = Fq::greater_than(circuit, &sy_neg, &sy); + let tsy = bigint::select(circuit, &sy, &sy_neg, sy_neg_greater); + let tsy_neg = bigint::select(circuit, &sy_neg, &sy, sy_neg_greater); + let y_bits = bigint::select(circuit, &tsy_neg, &tsy, is_y_negative); + let y = Fq(y_bits); + + // z = 1 in Montgomery + let one_m = Fq::as_montgomery(ark_bn254::Fq::ONE); + let z = Fq::new_constant(&one_m).expect("const one mont"); + + let input_is_valid = { + // Input is invalid if input is not a valid point in the curve or deserialization error + // valid only if both crieterion is met + let tmp0 = circuit.issue_wire(); + let tmp1 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: rhs_is_qr, + wire_b: flags_is_valid, + wire_c: tmp0, + gate_type: crate::GateType::And, + }); + circuit.add_gate(crate::Gate { + wire_a: tmp0, + wire_b: is_x_m_valid, + wire_c: tmp1, + gate_type: crate::GateType::And, + }); + tmp1 + }; + + DecompressedG1Wires { + point: G1Projective { + x: x_m.clone(), + y, + z, + }, + is_valid: input_is_valid, + } + } +} + +/// Analogous to Option where is_valid acts similar to is_some() +#[derive(Debug, Clone)] +pub struct DecompressedG1Wires { + pub point: G1Projective, + pub is_valid: WireId, +} + +impl WiresObject for DecompressedG1Wires { + fn to_wires_vec(&self) -> Vec { + let mut wires = Vec::new(); + wires.extend(self.point.to_wires_vec()); + wires.push(self.is_valid); + wires + } + + fn clone_from(&self, mut wire_gen: &mut impl FnMut() -> WireId) -> Self { + Self { + point: self.point.clone_from(&mut wire_gen), + is_valid: wire_gen(), + } + } +} + +impl FromWires for DecompressedG1Wires { + fn from_wires(wires: &[WireId]) -> Option { + assert_eq!(wires.len(), DecompressedG1Wires::ARITY); + Some(Self { + point: G1Projective::from_wires(&wires[0..G1Projective::N_BITS])?, + is_valid: wires[G1Projective::N_BITS], + }) + } +} + +impl WiresArity for DecompressedG1Wires { + const ARITY: usize = G1Projective::N_BITS + 1; } #[cfg(test)] mod tests { - use ark_ec::{CurveGroup, VariableBaseMSM}; + use ark_ec::{CurveGroup, PrimeGroup, VariableBaseMSM}; use ark_ff::UniformRand; + use ark_serialize::CanonicalSerialize; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -851,4 +1043,106 @@ mod tests { let actual_result = G1Projective::from_bits_unchecked(result.output_value.clone()); assert_eq!(actual_result, neg_a_mont); } + + #[test] + fn test_g1_compress_decompress_matches() { + let mut rng = ChaCha20Rng::seed_from_u64(111); + let r = ark_bn254::Fr::rand(&mut rng); + let p = (ark_bn254::G1Projective::generator() * r).into_affine(); + + let mut p_bytes = Vec::new(); + p.serialize_compressed(&mut p_bytes).unwrap(); + let input: Vec = p_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + let input: [bool; 256] = input.try_into().unwrap(); + + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { + let res = G1Projective::deserialize_checked(ctx, *wires); + let dec = res.point; + + let exp = G1Projective::as_montgomery(p.into()); + let x_ok = Fq::equal_constant(ctx, &dec.x, &exp.x); + let z_ok = Fq::equal_constant(ctx, &dec.z, &exp.z); + // Compare y up to sign by checking y^2 equality + let y_sq = Fq::square_montgomery(ctx, &dec.y); + let exp_y_std = Fq::from_montgomery(exp.y); + let exp_y_sq_m = Fq::as_montgomery(exp_y_std.square()); + let y_ok = Fq::equal_constant(ctx, &y_sq, &exp_y_sq_m); + vec![x_ok, y_ok, z_ok, res.is_valid] + }); + + assert!(out.output_value.iter().all(|&b| b)); + } + + #[test] + fn test_g1_decompress_failure() { + let mut rng = ChaCha20Rng::seed_from_u64(112); + for _ in 0..5 { + // sufficient sample size to sample both valid and invalid points + let x = ark_bn254::Fq::rand(&mut rng); + let a1 = ark_bn254::Fq::sqrt(&((x * x * x) + ark_bn254::Fq::from(3))); + let (y, ref_is_valid) = if let Some(a1) = a1 { + // if it is possible to take square root, you have found correct y, + (a1, true) + } else { + // else generate some random value + (ark_bn254::Fq::rand(&mut rng), false) + }; + let pt = ark_bn254::G1Affine::new_unchecked(x, y); + + let mut p_bytes = Vec::new(); + pt.serialize_compressed(&mut p_bytes).unwrap(); + let input: Vec = p_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + let input: [bool; 256] = input.try_into().unwrap(); + + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { + let dec = G1Projective::deserialize_checked(ctx, *wires); + vec![dec.is_valid] + }); + let calc_is_valid = out.output_value[0]; + + assert_eq!(calc_is_valid, ref_is_valid); + assert_eq!(calc_is_valid, pt.is_on_curve()); + } + } + + #[test] + fn test_g1_is_on_curve() { + let mut rng = ChaCha20Rng::seed_from_u64(111); + let r = ark_bn254::G1Projective::rand(&mut rng); + let ref_is_on_curve = r.into_affine().is_on_curve(); + let input = G1Input { + points: [G1Projective::as_montgomery(r)], + }; + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { + let is_on_curve = G1Projective::is_on_curve(ctx, &wires.points[0]); + vec![is_on_curve] + }); + assert_eq!(out.output_value[0], ref_is_on_curve); + + // not a point on curve + let r = ark_bn254::G1Projective::new_unchecked( + ark_bn254::Fq::rand(&mut rng), + ark_bn254::Fq::rand(&mut rng), + ark_bn254::Fq::rand(&mut rng), + ); + let input = G1Input { + points: [G1Projective::as_montgomery(r)], + }; + let ref_is_on_curve = r.into_affine().is_on_curve(); + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { + let is_on_curve = G1Projective::is_on_curve(ctx, &wires.points[0]); + vec![is_on_curve] + }); + assert_eq!(out.output_value[0], ref_is_on_curve); + } } diff --git a/g16ckt/src/gadgets/bn254/g2.rs b/g16ckt/src/gadgets/bn254/g2.rs index 441a9e8..6533597 100644 --- a/g16ckt/src/gadgets/bn254/g2.rs +++ b/g16ckt/src/gadgets/bn254/g2.rs @@ -1,13 +1,15 @@ use std::{cmp::min, collections::HashMap, iter::zip}; -use ark_ff::Zero; +use ark_ec::short_weierstrass::SWCurveConfig; +use ark_ff::{AdditiveGroup, Field, PrimeField, Zero}; use circuit_component_macro::component; +use num_bigint::BigUint; use crate::{ CircuitContext, WireId, - circuit::{FromWires, WiresObject}, + circuit::{FromWires, TRUE_WIRE, WiresArity, WiresObject}, gadgets::{ - bigint::Error, + bigint::{self, BigIntWires, Error}, bn254::{fp254impl::Fp254Impl, fq::Fq, fq2::Fq2, fr::Fr}, }, }; @@ -524,18 +526,299 @@ impl G2Projective { z: p.z.clone(), } } + + /// check whether or not the point is on the curve or not + /// checks y^2=x^3+Bz^6 (Jacobian projective coordinates) + #[component] + pub fn is_on_curve(circuit: &mut C, p: &G2Projective) -> WireId { + let x2 = Fq2::square_montgomery(circuit, &p.x); + let x3 = Fq2::mul_montgomery(circuit, &p.x, &x2); + let y2 = Fq2::square_montgomery(circuit, &p.y); + let z2 = Fq2::square_montgomery(circuit, &p.z); + let z4 = Fq2::square_montgomery(circuit, &z2); + let z6 = Fq2::mul_montgomery(circuit, &z2, &z4); + let b_z6 = Fq2::mul_by_constant_montgomery( + circuit, + &z6, + &Fq2::as_montgomery(ark_bn254::g2::Config::COEFF_B), + ); + let temp = Fq2::add(circuit, &x3, &b_z6); + let should_be_zero = Fq2::sub(circuit, &y2, &temp); + { + let c0 = bigint::equal_zero(circuit, should_be_zero.c0()); + let c1 = bigint::equal_zero(circuit, should_be_zero.c1()); + let is_zero = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: c0, + wire_b: c1, + wire_c: is_zero, + gate_type: crate::GateType::And, + }); + is_zero + } + } + + /// Deserialize into G2Projective from its 64 byte serialized bit representation. + // Follows arkworks implementation here: + // https://github.com/arkworks-rs/algebra/blob/v0.5.0/ec/src/models/short_weierstrass/mod.rs#L145 + pub fn deserialize_checked( + circuit: &mut C, + serialized_bits: [WireId; 64 * 8], + ) -> DecompressedG2Wires { + let (x, is_x_valid, flag) = { + let (num1, num2, flag) = ( + &serialized_bits[0..Fq::N_BITS], + &serialized_bits[32 * 8..32 * 8 + Fq::N_BITS], + &serialized_bits[32 * 8 + Fq::N_BITS..], + ); + let a0 = BigIntWires { + bits: num1.to_vec(), + }; + let a1 = BigIntWires { + bits: num2.to_vec(), + }; + let r: BigUint = ark_bn254::Fq::MODULUS.into(); + let valid_fq = { + let valid_a0 = bigint::less_than_constant(circuit, &a0, &r); + let valid_a1 = bigint::less_than_constant(circuit, &a1, &r); + let valid_fq = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: valid_a0, + wire_b: valid_a1, + wire_c: valid_fq, + gate_type: crate::GateType::And, + }); + valid_fq + }; + + // convert input field element in standard form into montgomery form + let r = Fq::as_montgomery(ark_bn254::Fq::ONE); + let a_mont_x = Fq::mul_by_constant_montgomery(circuit, &Fq(a0), &r.square()); + let r = Fq::as_montgomery(ark_bn254::Fq::ONE); + let a_mont_y = Fq::mul_by_constant_montgomery(circuit, &Fq(a1), &r.square()); + + // flag_0 is lsb, flag 1 is msb + (Fq2([a_mont_x, a_mont_y]), valid_fq, [flag[0], flag[1]]) + }; + + // Part 1: Extract Flags + + let is_y_positive = { + // In arkworks, given: + // const Y_IS_POSITIVE: u8 = 0; + let flag_or = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag[0], + wire_b: flag[1], + wire_c: flag_or, + gate_type: crate::GateType::Or, + }); + let flag_nor = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag_or, + wire_b: TRUE_WIRE, + wire_c: flag_nor, + gate_type: crate::GateType::Xor, + }); + flag_nor + }; + + let is_y_negative = { + // In arkworks, given: + // const Y_IS_NEGATIVE: u8 = 1 << 7; + let tmp0 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag[0], + wire_b: TRUE_WIRE, + wire_c: tmp0, + gate_type: crate::GateType::Xor, + }); + let tmp1 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: flag[1], + wire_b: tmp0, + wire_c: tmp1, + gate_type: crate::GateType::And, + }); + tmp1 + }; + + // rest of the flags (11 and 01) represent identity and None, so are invalid flags + let flags_is_valid = { + let tmp0 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: is_y_positive, + wire_b: is_y_negative, + wire_c: tmp0, + gate_type: crate::GateType::Or, + }); + tmp0 + }; + + // Part 2: compute (X, Y, Z) + + let x2 = Fq2::square_montgomery(circuit, &x); + let x3 = Fq2::mul_montgomery(circuit, &x2, &x); + let y2 = Fq2::add_constant( + circuit, + &x3, + &Fq2::as_montgomery(ark_bn254::g2::Config::COEFF_B), + ); + + let y = Fq2::sqrt_general_montgomery(circuit, &y2); + let rhs_is_qr = { + // check if y * y == y2 to ensure rhs was a quadratic residue to begin with, + // if it was not, then it means (x,y) is not a point on the curve + let y_y = Fq2::square_montgomery(circuit, &y); + + let match_c0 = bigint::equal(circuit, y2.c0(), y_y.c0()); + let match_c1 = bigint::equal(circuit, y2.c1(), y_y.c1()); + let match_c0_and_c1 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: match_c0, + wire_b: match_c1, + wire_c: match_c0_and_c1, + gate_type: crate::GateType::And, + }); + match_c0_and_c1 + }; + + // analogous to get_point_from_x_unchecked + let neg_y = Fq2::neg(circuit, y.clone()); + let y_neg_greater = Fq2::greater_than(circuit, &neg_y, &y); + let tsy = { + let tsy_c0 = bigint::select(circuit, y.c0(), neg_y.c0(), y_neg_greater); + let tsy_c1 = bigint::select(circuit, y.c1(), neg_y.c1(), y_neg_greater); + Fq2([Fq(tsy_c0), Fq(tsy_c1)]) + }; + let tsy_neg = { + let tsy_neg_c0 = bigint::select(circuit, neg_y.c0(), y.c0(), y_neg_greater); + let tsy_neg_c1 = bigint::select(circuit, neg_y.c1(), y.c1(), y_neg_greater); + Fq2([Fq(tsy_neg_c0), Fq(tsy_neg_c1)]) + }; + + let final_y_0 = bigint::select(circuit, tsy_neg.c0(), tsy.c0(), is_y_negative); + let final_y_1 = bigint::select(circuit, tsy_neg.c1(), tsy.c1(), is_y_negative); + + // z = 1 in Montgomery + let one_m = Fq::as_montgomery(ark_bn254::Fq::ONE); + let zero_m = Fq::as_montgomery(ark_bn254::Fq::ZERO); + + let input_is_valid = { + // Input is invalid if input is not a valid point in the curve or deserialization error + // valid only if both crieterion is met + let tmp0 = circuit.issue_wire(); + let tmp1 = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: rhs_is_qr, + wire_b: flags_is_valid, + wire_c: tmp0, + gate_type: crate::GateType::And, + }); + circuit.add_gate(crate::Gate { + wire_a: tmp0, + wire_b: is_x_valid, + wire_c: tmp1, + gate_type: crate::GateType::And, + }); + tmp1 + }; + + DecompressedG2Wires { + point: G2Projective { + x: x.clone(), + y: Fq2([Fq(final_y_0), Fq(final_y_1)]), + // In Fq2, ONE is (c0=1, c1=0). Use Montgomery representation. + z: Fq2([ + Fq::new_constant(&one_m).unwrap(), + Fq::new_constant(&zero_m).unwrap(), + ]), + }, + is_valid: input_is_valid, + } + } +} + +#[derive(Debug, Clone)] +pub struct CompressedG2Wires { + pub p: Fq2, + pub y_flag: WireId, +} + +impl CompressedG2Wires { + pub fn new(mut issue: impl FnMut() -> WireId) -> Self { + Self { + p: Fq2::new(&mut issue), + y_flag: issue(), + } + } +} + +impl WiresObject for CompressedG2Wires { + fn to_wires_vec(&self) -> Vec { + let Self { p, y_flag } = self; + + let mut v = p.to_wires_vec(); + v.push(*y_flag); + v + } + + fn clone_from(&self, wire_gen: &mut impl FnMut() -> WireId) -> Self { + Self { + p: self.p.clone_from(wire_gen), + y_flag: self.y_flag.clone_from(wire_gen), + } + } +} + +#[derive(Debug, Clone)] +pub struct DecompressedG2Wires { + pub point: G2Projective, + pub is_valid: WireId, +} + +impl WiresObject for DecompressedG2Wires { + fn to_wires_vec(&self) -> Vec { + let mut wires = Vec::new(); + wires.extend(self.point.to_wires_vec()); + wires.push(self.is_valid); + wires + } + + fn clone_from(&self, mut wire_gen: &mut impl FnMut() -> WireId) -> Self { + Self { + point: self.point.clone_from(&mut wire_gen), + is_valid: wire_gen(), + } + } +} + +impl FromWires for DecompressedG2Wires { + fn from_wires(wires: &[WireId]) -> Option { + assert_eq!(wires.len(), DecompressedG2Wires::ARITY); + Some(Self { + point: G2Projective::from_wires(&wires[0..G2Projective::N_BITS])?, + is_valid: wires[G2Projective::N_BITS], + }) + } +} + +impl WiresArity for DecompressedG2Wires { + const ARITY: usize = G2Projective::N_BITS + 1; } #[cfg(test)] mod tests { - use ark_ec::{CurveGroup, VariableBaseMSM}; - use ark_ff::UniformRand; + use ark_ec::{CurveGroup, PrimeGroup, VariableBaseMSM}; + use ark_ff::{Field, UniformRand}; + use ark_serialize::CanonicalSerialize; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use super::*; use crate::{ circuit::{CircuitBuilder, CircuitInput, EncodeInput, modes::CircuitMode}, + gadgets::bn254::pairing::double_in_place, test_utils::trng, }; @@ -692,6 +975,33 @@ mod tests { assert_eq!(actual_result, c_mont); } + #[test] + fn test_double_in_place() { + use ark_ec::CurveGroup; + + let mut rng = ChaCha20Rng::seed_from_u64(42); + + // a is in Jacobian + let a = ark_bn254::G2Projective::rand(&mut rng); + + // Jacobian doubling via library, then to affine + let b_aff = (a + a).into_affine(); + + // Start from affine (x,y,1) but run HOMOGENEOUS doubling + let a_aff = a.into_affine(); + let mut r = ark_bn254::G2Projective::new(a_aff.x, a_aff.y, ark_bn254::Fq2::ONE); + double_in_place(&mut r); // r = (X,Y,Z) in HOMOGENEOUS + + // Convert HOMOGENEOUS -> JACOBIAN expected by arkworks: + r.x *= r.z; // X' = X*Z + let z2 = r.z.square(); + r.y *= z2; // Y' = Y*Z^2 + // Z' = Z + + let r_aff = r.into_affine(); // now safe to normalize + assert_eq!(b_aff, r_aff); + } + #[test] fn test_g2p_neg() { // Generate random G2 point @@ -873,4 +1183,106 @@ mod tests { let actual_result = G2Projective::from_bits_unchecked(circuit_result.output_value.clone()); assert_eq!(actual_result, G2Projective::as_montgomery(result)); } + + #[test] + fn test_g2_compress_decompress_matches() { + let mut rng = ChaCha20Rng::seed_from_u64(222); + let r = ark_bn254::Fr::rand(&mut rng); + let p = (ark_bn254::G2Projective::generator() * r).into_affine(); + + let mut p_bytes = Vec::new(); + p.serialize_compressed(&mut p_bytes).unwrap(); + let input: Vec = p_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + let input: [bool; 64 * 8] = input.try_into().unwrap(); + + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 20_000, |ctx, wires| { + let res = G2Projective::deserialize_checked(ctx, *wires); + let dec = res.point; + + let exp = G2Projective::as_montgomery(p.into()); + let x_ok = Fq2::equal_constant(ctx, &dec.x, &exp.x); + let z_ok = Fq2::equal_constant(ctx, &dec.z, &exp.z); + // Compare y up to sign by checking y^2 equality + let y_sq = Fq2::square_montgomery(ctx, &dec.y); + let exp_y_std = Fq2::from_montgomery(exp.y); + let exp_y_sq_m = Fq2::as_montgomery(exp_y_std.square()); + let y_ok = Fq2::equal_constant(ctx, &y_sq, &exp_y_sq_m); + vec![x_ok, y_ok, z_ok, res.is_valid] + }); + + assert!(out.output_value.iter().all(|&b| b)); + } + + #[test] + fn test_g2_decompress_failure() { + let mut rng = ChaCha20Rng::seed_from_u64(112); + for _ in 0..5 { + // sufficient sample size to sample both valid and invalid points + let x = ark_bn254::Fq2::rand(&mut rng); + let a1 = ark_bn254::Fq2::sqrt(&((x * x * x) + ark_bn254::g2::Config::COEFF_B)); + let (y, ref_is_valid) = if let Some(a1) = a1 { + // if it is possible to take square root, you have found correct y, + (a1, true) + } else { + // else generate some random value + (ark_bn254::Fq2::rand(&mut rng), false) + }; + let pt = ark_bn254::G2Affine::new_unchecked(x, y); + + let mut p_bytes = Vec::new(); + pt.serialize_compressed(&mut p_bytes).unwrap(); + let input: Vec = p_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + let input: [bool; 64 * 8] = input.try_into().unwrap(); + + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { + let dec = G2Projective::deserialize_checked(ctx, *wires); + vec![dec.is_valid] + }); + let calc_is_valid = out.output_value[0]; + + assert_eq!(calc_is_valid, ref_is_valid); + assert_eq!(calc_is_valid, pt.is_on_curve()); + } + } + + #[test] + fn test_g2_is_on_curve() { + let mut rng = ChaCha20Rng::seed_from_u64(111); + let r = ark_bn254::G2Projective::rand(&mut rng); + let ref_is_on_curve = r.into_affine().is_on_curve(); + let input = G2Input { + points: [G2Projective::as_montgomery(r)], + }; + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { + let is_on_curve = G2Projective::is_on_curve(ctx, &wires.points[0]); + vec![is_on_curve] + }); + assert_eq!(out.output_value[0], ref_is_on_curve); + + // not a point on curve + let r = ark_bn254::G2Projective::new_unchecked( + ark_bn254::Fq2::rand(&mut rng), + ark_bn254::Fq2::rand(&mut rng), + ark_bn254::Fq2::rand(&mut rng), + ); + let input = G2Input { + points: [G2Projective::as_montgomery(r)], + }; + let ref_is_on_curve = r.into_affine().is_on_curve(); + let out: crate::circuit::StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { + let is_on_curve = G2Projective::is_on_curve(ctx, &wires.points[0]); + vec![is_on_curve] + }); + assert_eq!(out.output_value[0], ref_is_on_curve); + } } diff --git a/g16ckt/src/gadgets/groth16.rs b/g16ckt/src/gadgets/groth16.rs index 80f0d57..83f07b1 100644 --- a/g16ckt/src/gadgets/groth16.rs +++ b/g16ckt/src/gadgets/groth16.rs @@ -5,16 +5,18 @@ //! where `msm = vk.gamma_abc_g1[0] + sum_i(public[i] * vk.gamma_abc_g1[i+1])`. use ark_bn254::Bn254; -use ark_ec::{AffineRepr, CurveGroup, models::short_weierstrass::SWCurveConfig, pairing::Pairing}; -use ark_ff::{AdditiveGroup, Field}; +use ark_ec::{AffineRepr, pairing::Pairing}; +use ark_ff::{Field, PrimeField}; use ark_groth16::VerifyingKey; +use ark_serialize::CanonicalSerialize; use circuit_component_macro::component; +use num_bigint::BigUint; use crate::{ - CircuitContext, Fq2Wire, WireId, - circuit::{CircuitInput, CircuitMode, EncodeInput, WiresObject}, + CircuitContext, WireId, + circuit::{CircuitInput, CircuitMode, EncodeInput, TRUE_WIRE, WiresObject}, gadgets::{ - bigint, + bigint::{self, BigIntWires}, bn254::{ G2Projective, final_exponentiation::final_exponentiation_montgomery, fq::Fq, fq12::Fq12, fr::Fr, g1::G1Projective, @@ -66,6 +68,89 @@ pub fn groth16_verify( vk, } = input; + let is_valid_field_and_group = { + let is_valid_fr = { + let mut and_all_wires = TRUE_WIRE; + public.iter().for_each(|pubinp| { + let r: BigUint = ark_bn254::Fr::MODULUS.into(); + let valid_pubinp = bigint::less_than_constant(circuit, pubinp, &r); + let new_wire = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: and_all_wires, + wire_b: valid_pubinp, + wire_c: new_wire, + gate_type: crate::GateType::And, + }); + and_all_wires = new_wire; + }); + and_all_wires + }; + + // Verify that all group elements of input proof include valid base field elements + let is_valid_fq = { + let elems = [ + &a.x, &a.y, &a.z, &b.x.0[0], &b.x.0[1], &b.y.0[0], &b.y.0[1], &b.z.0[0], &b.z.0[1], + &c.x, &c.y, &c.z, + ]; + let mut and_all_wires = TRUE_WIRE; + elems.iter().for_each(|pubinp| { + let r: BigUint = ark_bn254::Fq::MODULUS.into(); + let valid_fq = bigint::less_than_constant(circuit, pubinp, &r); + let new_wire = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: and_all_wires, + wire_b: valid_fq, + wire_c: new_wire, + gate_type: crate::GateType::And, + }); + and_all_wires = new_wire; + }); + and_all_wires + }; + + let is_on_curve = { + let a_is_on_curve = G1Projective::is_on_curve(circuit, a); + let b_is_on_curve = G2Projective::is_on_curve(circuit, b); + let c_is_on_curve = G1Projective::is_on_curve(circuit, c); + + // valid group check + let tmp0 = circuit.issue_wire(); + let is_on_curve = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: a_is_on_curve, + wire_b: b_is_on_curve, + wire_c: tmp0, + gate_type: crate::GateType::And, + }); + circuit.add_gate(crate::Gate { + wire_a: tmp0, + wire_b: c_is_on_curve, + wire_c: is_on_curve, + gate_type: crate::GateType::And, + }); + is_on_curve + }; + + // valid fq and fr + let is_valid_field = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: is_valid_fr, + wire_b: is_valid_fq, + wire_c: is_valid_field, + gate_type: crate::GateType::And, + }); + + // valid field and group check + let is_valid_field_and_group = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: is_valid_field, + wire_b: is_on_curve, + wire_c: is_valid_field_and_group, + gate_type: crate::GateType::And, + }); + is_valid_field_and_group + }; + // Standard verification with public inputs // MSM: sum_i public[i] * gamma_abc_g1[i+1] let bases: Vec = vk @@ -106,165 +191,44 @@ pub fn groth16_verify( let f = final_exponentiation_montgomery(circuit, &f); - Fq12::equal_constant(circuit, &f, &Fq12::as_montgomery(alpha_beta)) -} - -/// Decompress a compressed G1 point (x, sign bit) into projective wires with z = 1 (Montgomery domain). -/// - `x_m`: x-coordinate in Montgomery form wires -/// - `y_flag`: boolean wire selecting the correct sqrt branch for y -#[component] -pub fn decompress_g1_from_compressed( - circuit: &mut C, - compressed: &CompressedG1Wires, -) -> G1Projective { - let CompressedG1Wires { x_m, y_flag } = compressed.clone(); - - // rhs = x^3 + b (Montgomery domain) - let x2 = Fq::square_montgomery(circuit, &x_m); - let x3 = Fq::mul_montgomery(circuit, &x2, &x_m); - let b_m = Fq::as_montgomery(ark_bn254::g1::Config::COEFF_B); - let rhs = Fq::add_constant(circuit, &x3, &b_m); - - // sy = sqrt(rhs) in Montgomery domain - let sy = Fq::sqrt_montgomery(circuit, &rhs); - let sy_neg = Fq::neg(circuit, &sy); - let y_bits = bigint::select(circuit, &sy.0, &sy_neg.0, y_flag); - let y = Fq(y_bits); - - // z = 1 in Montgomery - let one_m = Fq::as_montgomery(ark_bn254::Fq::ONE); - let z = Fq::new_constant(&one_m).expect("const one mont"); - - G1Projective { - x: x_m.clone(), - y, - z, - } -} - -#[component] -pub fn decompress_g2_from_compressed( - circuit: &mut C, - compressed: &CompressedG2Wires, -) -> G2Projective { - let CompressedG2Wires { p: x, y_flag } = compressed; - - let x2 = Fq2Wire::square_montgomery(circuit, x); - - let x3 = Fq2Wire::mul_montgomery(circuit, &x2, x); - - let y2 = Fq2Wire::add_constant( - circuit, - &x3, - &Fq2Wire::as_montgomery(ark_bn254::g2::Config::COEFF_B), - ); - - let y = Fq2Wire::sqrt_general_montgomery(circuit, &y2); - - let neg_y = Fq2Wire::neg(circuit, y.clone()); - - let final_y_0 = bigint::select(circuit, y.c0(), neg_y.c0(), *y_flag); - let final_y_1 = bigint::select(circuit, y.c1(), neg_y.c1(), *y_flag); - - // z = 1 in Montgomery - let one_m = Fq::as_montgomery(ark_bn254::Fq::ONE); - let zero_m = Fq::as_montgomery(ark_bn254::Fq::ZERO); + let finexp_match = Fq12::equal_constant(circuit, &f, &Fq12::as_montgomery(alpha_beta)); - G2Projective { - x: x.clone(), - y: Fq2Wire([Fq(final_y_0), Fq(final_y_1)]), - // In Fq2, ONE is (c0=1, c1=0). Use Montgomery representation. - z: Fq2Wire([ - Fq::new_constant(&one_m).unwrap(), - Fq::new_constant(&zero_m).unwrap(), - ]), - } -} - -#[derive(Clone, Debug)] -pub struct CompressedG1Wires { - pub x_m: Fq, - pub y_flag: WireId, -} - -impl CompressedG1Wires { - pub fn new(mut issue: impl FnMut() -> WireId) -> Self { - Self { - x_m: Fq::new(&mut issue), - y_flag: issue(), - } - } + let valid = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: finexp_match, + wire_b: is_valid_field_and_group, + wire_c: valid, + gate_type: crate::GateType::And, + }); + valid } -impl WiresObject for CompressedG1Wires { - fn to_wires_vec(&self) -> Vec { - let Self { x_m: p, y_flag } = self; - - let mut v = p.to_wires_vec(); - v.push(*y_flag); - v - } - - fn clone_from(&self, wire_gen: &mut impl FnMut() -> WireId) -> Self { - Self { - x_m: self.x_m.clone_from(wire_gen), - y_flag: self.y_flag.clone_from(wire_gen), - } - } -} - -#[derive(Debug, Clone)] -pub struct CompressedG2Wires { - pub p: Fq2Wire, - pub y_flag: WireId, -} - -impl CompressedG2Wires { - pub fn new(mut issue: impl FnMut() -> WireId) -> Self { - Self { - p: Fq2Wire::new(&mut issue), - y_flag: issue(), - } - } -} - -impl WiresObject for CompressedG2Wires { - fn to_wires_vec(&self) -> Vec { - let Self { p, y_flag } = self; - - let mut v = p.to_wires_vec(); - v.push(*y_flag); - v - } - - fn clone_from(&self, wire_gen: &mut impl FnMut() -> WireId) -> Self { - Self { - p: self.p.clone_from(wire_gen), - y_flag: self.y_flag.clone_from(wire_gen), - } - } -} - -/// Convenience wrapper: verify using compressed A and C (x, y_flag). B remains host-provided `G2Affine`. -/// Includes optimization for empty public inputs to avoid unnecessary MSM computation. +/// Verify a 128-byte compressed serialized groth16 proof using public inputs pub fn groth16_verify_compressed( circuit: &mut C, input: &Groth16VerifyCompressedInputWires, ) -> crate::WireId { - let a = decompress_g1_from_compressed(circuit, &input.a); - let b = decompress_g2_from_compressed(circuit, &input.b); - let c = decompress_g1_from_compressed(circuit, &input.c); + let proof = input.proof.deserialize_checked(circuit); - groth16_verify( + let verified_res = groth16_verify( circuit, &Groth16VerifyInputWires { public: input.public.clone(), - a, - b, - c, + a: proof.a, + b: proof.b, + c: proof.c, vk: input.vk.clone(), }, - ) + ); + + let valid = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: verified_res, // proof verification was successful + wire_b: proof.valid, // input is valid + wire_c: valid, + gate_type: crate::GateType::And, + }); + valid } #[derive(Debug, Clone)] @@ -370,21 +334,109 @@ impl> EncodeInput for Groth16VerifyInput { impl Groth16VerifyInput { pub fn compress(self) -> Groth16VerifyCompressedInput { - Groth16VerifyCompressedInput(self) + let public = self.public.into_iter().map(|x| x.into()).collect(); + let mut proof_bytes = Vec::new(); + ark_groth16::Proof:: { + a: self.a.into(), + b: self.b.into(), + c: self.c.into(), + } + .serialize_compressed(&mut proof_bytes) + .expect("serialize proof"); + let proof_bits: Vec = proof_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + Groth16VerifyCompressedInput { + public, + proof: proof_bits.try_into().unwrap(), + vk: self.vk, + } } } -pub struct Groth16VerifyCompressedInput(pub Groth16VerifyInput); +type SerializedCompressedProof = [bool; 128 * 8]; +/// Compressed representation of groth16 proof +pub struct Groth16VerifyCompressedInput { + pub public: Vec, + pub proof: SerializedCompressedProof, // 128 byte proof + pub vk: VerifyingKey, +} +/// Compressed groth16 proof with public inputs #[derive(Debug)] pub struct Groth16VerifyCompressedInputWires { pub public: Vec, - pub a: CompressedG1Wires, - pub b: CompressedG2Wires, - pub c: CompressedG1Wires, + pub proof: SerializedCompressedProofWires, pub vk: VerifyingKey, } +/// Serialized 128 byte representation of groth16 proof in arkworks' serialized format +#[derive(Debug, Clone, Copy)] +pub struct SerializedCompressedProofWires(pub [WireId; 128 * 8]); + +/// Groth16 Proof elements `{a, b, c}` along with `valid` flag indicating whether the +/// input (i.e. `SerializedCompressedProofWires`), from which this was obtained, was valid to begin with. +#[derive(Debug, Clone)] +pub struct DeserializedCompressedProofWires { + a: G1Projective, + b: G2Projective, + c: G1Projective, + valid: WireId, +} + +impl SerializedCompressedProofWires { + pub fn new(issue: &mut impl FnMut() -> WireId) -> Self { + let v: Vec = std::iter::repeat_with(issue).take(128 * 8).collect(); + let v: [WireId; 128 * 8] = v.try_into().unwrap(); + Self(v) + } + pub fn deserialize_checked( + &self, + circuit: &mut C, + ) -> DeserializedCompressedProofWires { + let compressed_a: [WireId; 32 * 8] = self.0[0..32 * 8].try_into().unwrap(); + let compressed_b: [WireId; 64 * 8] = self.0[32 * 8..96 * 8].try_into().unwrap(); + let compressed_c: [WireId; 32 * 8] = self.0[96 * 8..].try_into().unwrap(); + + let a_decomp = G1Projective::deserialize_checked(circuit, compressed_a); + let b_decomp = G2Projective::deserialize_checked(circuit, compressed_b); + let c_decomp = G1Projective::deserialize_checked(circuit, compressed_c); + + let ab_valid = circuit.issue_wire(); + let abc_valid = circuit.issue_wire(); + + circuit.add_gate(crate::Gate { + wire_a: a_decomp.is_valid, + wire_b: b_decomp.is_valid, + wire_c: ab_valid, + gate_type: crate::GateType::And, + }); + circuit.add_gate(crate::Gate { + wire_a: ab_valid, + wire_b: c_decomp.is_valid, + wire_c: abc_valid, + gate_type: crate::GateType::And, + }); + + DeserializedCompressedProofWires { + a: a_decomp.point, + b: b_decomp.point, + c: c_decomp.point, + valid: abc_valid, + } + } +} + +impl WiresObject for SerializedCompressedProofWires { + fn to_wires_vec(&self) -> Vec { + self.0.to_vec() + } + fn clone_from(&self, wire_gen: &mut impl FnMut() -> WireId) -> Self { + SerializedCompressedProofWires::new(wire_gen) + } +} + impl WiresObject for Groth16VerifyCompressedInputWires { fn to_wires_vec(&self) -> Vec { Groth16VerifyCompressedInput::collect_wire_ids(self) @@ -393,9 +445,7 @@ impl WiresObject for Groth16VerifyCompressedInputWires { fn clone_from(&self, mut issue: &mut impl FnMut() -> WireId) -> Self { Groth16VerifyCompressedInputWires { public: self.public.iter().map(|_| Fr::new(&mut issue)).collect(), - a: CompressedG1Wires::new(&mut issue), - b: CompressedG2Wires::new(&mut issue), - c: CompressedG1Wires::new(&mut issue), + proof: self.proof.clone_from(issue), vk: self.vk.clone(), } } @@ -406,11 +456,9 @@ impl CircuitInput for Groth16VerifyCompressedInput { fn allocate(&self, mut issue: impl FnMut() -> WireId) -> Self::WireRepr { Groth16VerifyCompressedInputWires { - public: self.0.public.iter().map(|_| Fr::new(&mut issue)).collect(), - a: CompressedG1Wires::new(&mut issue), - b: CompressedG2Wires::new(&mut issue), - c: CompressedG1Wires::new(&mut issue), - vk: self.0.vk.clone(), + public: self.public.iter().map(|_| Fr::new(&mut issue)).collect(), + proof: SerializedCompressedProofWires::new(&mut issue), + vk: self.vk.clone(), } } fn collect_wire_ids(repr: &Self::WireRepr) -> Vec { @@ -418,9 +466,7 @@ impl CircuitInput for Groth16VerifyCompressedInput { for s in &repr.public { ids.extend(s.to_wires_vec()); } - ids.extend(repr.a.to_wires_vec()); - ids.extend(repr.b.to_wires_vec()); - ids.extend(repr.c.to_wires_vec()); + ids.extend(repr.proof.to_wires_vec()); ids } } @@ -428,8 +474,8 @@ impl CircuitInput for Groth16VerifyCompressedInput { impl> EncodeInput for Groth16VerifyCompressedInput { fn encode(&self, repr: &Groth16VerifyCompressedInputWires, cache: &mut M) { // Encode public scalars - for (w, v) in repr.public.iter().zip(self.0.public.iter()) { - let fr_fn = Fr::get_wire_bits_fn(w, v).unwrap(); + for (w, v) in repr.public.iter().zip(self.public.iter()) { + let fr_fn = BigIntWires::get_wire_bits_fn(w, v).unwrap(); for &wire in w.iter() { if let Some(bit) = fr_fn(wire) { @@ -438,73 +484,32 @@ impl> EncodeInput for Groth16VerifyCompresse } } - // Compute compression from standard affine coords; feed Montgomery x + flag - let a_aff_std = self.0.a.into_affine(); - let b_aff_std = self.0.b.into_affine(); - let c_aff_std = self.0.c.into_affine(); - - let a_flag = (a_aff_std.y.square()) - .sqrt() - .expect("y^2 must be QR") - .eq(&a_aff_std.y); - let b_flag = (b_aff_std.y.square()) - .sqrt() - .expect("y^2 must be QR in Fq2") - .eq(&b_aff_std.y); - let c_flag = (c_aff_std.y.square()) - .sqrt() - .expect("y^2 must be QR") - .eq(&c_aff_std.y); - - let a_x_m = Fq::as_montgomery(a_aff_std.x); - let b_x_m = Fq2Wire::as_montgomery(b_aff_std.x); - let c_x_m = Fq::as_montgomery(c_aff_std.x); - - // Feed A.x (Montgomery) bits and flag - let a_x_fn = Fq::get_wire_bits_fn(&repr.a.x_m, &a_x_m).unwrap(); - for &wire_id in repr.a.x_m.iter() { - if let Some(bit) = a_x_fn(wire_id) { - cache.feed_wire(wire_id, bit); - } - } - cache.feed_wire(repr.a.y_flag, a_flag); - - // Feed B.x (Montgomery) bits and flag (Fq2 as c0||c1) - let b_x_fn = Fq2Wire::get_wire_bits_fn(&repr.b.p, &b_x_m).unwrap(); - for &wire_id in repr.b.p.iter() { - if let Some(bit) = b_x_fn(wire_id) { - cache.feed_wire(wire_id, bit); - } - } - cache.feed_wire(repr.b.y_flag, b_flag); - - // Feed C.x (Montgomery) bits and flag - let c_x_fn = Fq::get_wire_bits_fn(&repr.c.x_m, &c_x_m).unwrap(); - for &wire_id in repr.c.x_m.iter() { - if let Some(bit) = c_x_fn(wire_id) { - cache.feed_wire(wire_id, bit); - } - } - cache.feed_wire(repr.c.y_flag, c_flag); + self.proof.iter().zip(repr.proof.0).for_each(|(x, y)| { + cache.feed_wire(y, *x); + }); } } #[cfg(test)] mod tests { - use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; + use ark_ec::{AffineRepr, CurveGroup, PrimeGroup, short_weierstrass::SWCurveConfig}; use ark_ff::UniformRand; use ark_groth16::Groth16; use ark_relations::{ lc, r1cs::{ConstraintSynthesizer, ConstraintSystemRef, SynthesisError}, }; + use ark_serialize::CanonicalDeserialize; use ark_snark::{CircuitSpecificSetupSNARK, SNARK}; - use rand::SeedableRng; + use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; use test_log::test; use super::*; - use crate::circuit::{CircuitBuilder, CircuitMode, EncodeInput, StreamingResult}; + use crate::{ + Fq2Wire, + circuit::{CircuitBuilder, StreamingResult}, + }; // Helper to reduce duplication across bitflip tests for A, B, and C fn run_false_bitflip_test(seed: u64, mutate: impl FnOnce(&mut Groth16VerifyInput)) { @@ -821,112 +826,37 @@ mod tests { assert!(!out.output_value); } - // Minimal harnesses that allocate compressed wires and feed them directly - struct OnlyCompressedG1Input(ark_bn254::G1Affine); - impl crate::circuit::CircuitInput for OnlyCompressedG1Input { - type WireRepr = CompressedG1Wires; - fn allocate(&self, mut issue: impl FnMut() -> WireId) -> Self::WireRepr { - CompressedG1Wires::new(&mut issue) - } - fn collect_wire_ids(repr: &Self::WireRepr) -> Vec { - repr.to_wires_vec() - } - } - impl> EncodeInput for OnlyCompressedG1Input { - fn encode(&self, repr: &CompressedG1Wires, cache: &mut M) { - let p = self.0; - let x_m = Fq::as_montgomery(p.x); - let s = (p.y.square()).sqrt().expect("y^2 must be QR"); - let y_flag = s == p.y; - - let x_fn = Fq::get_wire_bits_fn(&repr.x_m, &x_m).unwrap(); - for &w in repr.x_m.iter() { - if let Some(bit) = x_fn(w) { - cache.feed_wire(w, bit); - } - } - cache.feed_wire(repr.y_flag, y_flag); - } - } - - struct OnlyCompressedG2Input(ark_bn254::G2Affine); - impl crate::circuit::CircuitInput for OnlyCompressedG2Input { - type WireRepr = CompressedG2Wires; - fn allocate(&self, mut issue: impl FnMut() -> WireId) -> Self::WireRepr { - CompressedG2Wires::new(&mut issue) - } - fn collect_wire_ids(repr: &Self::WireRepr) -> Vec { - repr.to_wires_vec() - } - } - impl> EncodeInput for OnlyCompressedG2Input { - fn encode(&self, repr: &CompressedG2Wires, cache: &mut M) { - let p = self.0; - let x_m = Fq2Wire::as_montgomery(p.x); - // Simple off-circuit compression flag: sqrt(y^2) == y - let s = (p.y.square()).sqrt().expect("y^2 must be QR in Fq2"); - let y_flag = s == p.y; - - let x_fn = Fq2Wire::get_wire_bits_fn(&repr.p, &x_m).unwrap(); - for &w in repr.p.iter() { - if let Some(bit) = x_fn(w) { - cache.feed_wire(w, bit); - } - } - cache.feed_wire(repr.y_flag, y_flag); - } - } - #[test] - fn test_g1_compress_decompress_matches() { - let mut rng = ChaCha20Rng::seed_from_u64(111); - let r = ark_bn254::Fr::rand(&mut rng); - let p = (ark_bn254::G1Projective::generator() * r).into_affine(); - - let input = OnlyCompressedG1Input(p); - - let out: crate::circuit::StreamingResult<_, _, Vec> = - CircuitBuilder::streaming_execute(input, 10_000, |ctx, wires| { - let dec = decompress_g1_from_compressed(ctx, wires); - - let exp = G1Projective::as_montgomery(p.into_group()); - let x_ok = Fq::equal_constant(ctx, &dec.x, &exp.x); - let z_ok = Fq::equal_constant(ctx, &dec.z, &exp.z); - // Compare y up to sign by checking y^2 equality - let y_sq = Fq::square_montgomery(ctx, &dec.y); - let exp_y_std = Fq::from_montgomery(exp.y); - let exp_y_sq_m = Fq::as_montgomery(exp_y_std.square()); - let y_ok = Fq::equal_constant(ctx, &y_sq, &exp_y_sq_m); - vec![x_ok, y_ok, z_ok] - }); - - assert!(out.output_value.iter().all(|&b| b)); - } - - #[test] - fn test_g2_compress_decompress_matches() { - let mut rng = ChaCha20Rng::seed_from_u64(222); - let r = ark_bn254::Fr::rand(&mut rng); - let p = (ark_bn254::G2Projective::generator() * r).into_affine(); - - let input = OnlyCompressedG2Input(p); - - let out: crate::circuit::StreamingResult<_, _, Vec> = - CircuitBuilder::streaming_execute(input, 20_000, |ctx, wires| { - let dec = decompress_g2_from_compressed(ctx, wires); - - let exp = G2Projective::as_montgomery(p.into_group()); - let x_ok = Fq2Wire::equal_constant(ctx, &dec.x, &exp.x); - let z_ok = Fq2Wire::equal_constant(ctx, &dec.z, &exp.z); - // Compare y up to sign by checking y^2 equality - let y_sq = Fq2Wire::square_montgomery(ctx, &dec.y); - let exp_y_std = Fq2Wire::from_montgomery(exp.y); - let exp_y_sq_m = Fq2Wire::as_montgomery(exp_y_std.square()); - let y_ok = Fq2Wire::equal_constant(ctx, &y_sq, &exp_y_sq_m); - vec![x_ok, y_ok, z_ok] - }); - - assert!(out.output_value.iter().all(|&b| b)); + fn test_cofactor_clearing() { + let mut rng = ChaCha20Rng::seed_from_u64(112); + for _ in 0..5 { + // sufficient sample size to sample both valid and invalid points + let x = ark_bn254::Fq2::rand(&mut rng); + let a1 = ark_bn254::Fq2::sqrt(&((x * x * x) + ark_bn254::g2::Config::COEFF_B)); + let (y, ref_is_valid) = if let Some(a1) = a1 { + // if it is possible to take square root, you have found correct y, + (a1, true) + } else { + // else generate some random value + (ark_bn254::Fq2::rand(&mut rng), false) + }; + let pt = ark_bn254::G2Affine::new_unchecked(x, y); + + let pt = pt.into_group(); + const COFACTOR: &[u64] = &[ + 0x345f2299c0f9fa8d, + 0x06ceecda572a2489, + 0xb85045b68181585e, + 0x30644e72e131a029, + ]; + let pt = pt.mul_bigint(COFACTOR); + let pt = pt.into_affine(); + // if it's a valid point, it should be on curve and subgroup (after cofactor clearing) + assert_eq!( + ref_is_valid, + pt.is_on_curve() && pt.is_in_correct_subgroup_assuming_on_curve() + ); + } } #[test] @@ -942,19 +872,22 @@ mod tests { let (pk, vk) = Groth16::::setup(circuit, &mut rng).unwrap(); let proof = Groth16::::prove(&pk, circuit, &mut rng).unwrap(); - let inputs = Groth16VerifyCompressedInput(Groth16VerifyInput { + let inputs = Groth16VerifyInput { public: vec![ark_bn254::Fr::from(0u64)], // unused here a: proof.a.into_group(), b: proof.b.into_group(), c: proof.c.into_group(), vk, - }); + } + .compress(); let out: crate::circuit::StreamingResult<_, _, Vec> = CircuitBuilder::streaming_execute(inputs, 80_000, |ctx, wires| { - let a_dec = decompress_g1_from_compressed(ctx, &wires.a); - let b_dec = decompress_g2_from_compressed(ctx, &wires.b); - let c_dec = decompress_g1_from_compressed(ctx, &wires.c); + let proof_dec = wires.proof.deserialize_checked(ctx); + + let a_dec = proof_dec.a; + let b_dec = proof_dec.b; + let c_dec = proof_dec.c; let a_exp = G1Projective::as_montgomery(proof.a.into_group()); let b_exp = G2Projective::as_montgomery(proof.b.into_group()); @@ -973,13 +906,90 @@ mod tests { let c_z_ok = Fq::equal_constant(ctx, &c_dec.z, &c_exp.z); vec![ - a_x_ok, a_y_ok, a_z_ok, b_x_ok, b_y_ok, b_z_ok, c_x_ok, c_y_ok, c_z_ok, + a_x_ok, + a_y_ok, + a_z_ok, + b_x_ok, + b_y_ok, + b_z_ok, + c_x_ok, + c_y_ok, + c_z_ok, + proof_dec.valid, ] }); assert!(out.output_value.iter().all(|&b| b)); } + #[test] + fn test_invalid_groth16_verify_compressed_true_small() { + // get valid point in curve that is not in subgroup + fn random_g2_affine_sg(rng: &mut impl Rng) -> ark_bn254::G2Affine { + let mut pt = ark_bn254::G2Affine::identity(); + for _ in 0..5 { + // sufficient sample size to sample both valid and invalid points + let x = ark_bn254::Fq2::rand(rng); + let a1 = ark_bn254::Fq2::sqrt(&((x * x * x) + ark_bn254::g2::Config::COEFF_B)); + let (y, ref_is_valid) = if let Some(a1) = a1 { + // if it is possible to take square root, you have found correct y, + (a1, true) + } else { + // else generate some random value + (ark_bn254::Fq2::rand(rng), false) + }; + if ref_is_valid { + pt = ark_bn254::G2Affine::new_unchecked(x, y); + break; + } + } + pt + } + + let k = 4; // circuit size; pairing cost dominates anyway + let mut rng = ChaCha20Rng::seed_from_u64(33333); + let circuit = DummyCircuit:: { + a: Some(ark_bn254::Fr::rand(&mut rng)), + b: Some(ark_bn254::Fr::rand(&mut rng)), + num_variables: 8, + num_constraints: 1 << k, + }; + let (pk, vk) = Groth16::::setup(circuit, &mut rng).unwrap(); + let c_val = circuit.a.unwrap() * circuit.b.unwrap(); + let proof = Groth16::::prove(&pk, circuit, &mut rng).unwrap(); + + // Case 1: Check that the proof is correct to begin with + let inputs = Groth16VerifyInput { + public: vec![c_val], + a: proof.a.into_group(), + b: proof.b.into_group(), + c: proof.c.into_group(), + vk: vk.clone(), + } + .compress(); + + let out: crate::circuit::StreamingResult<_, _, bool> = + CircuitBuilder::streaming_execute(inputs, 160_000, groth16_verify_compressed); + + assert!(out.output_value, "should pass"); + + // Case 2: Check for invalid proof + let inputs = Groth16VerifyInput { + public: vec![c_val], + + a: proof.a.into_group(), + b: random_g2_affine_sg(&mut rng).into_group(), // proof.b.into_group() + c: proof.c.into_group(), + vk, + } + .compress(); + + let out: crate::circuit::StreamingResult<_, _, bool> = + CircuitBuilder::streaming_execute(inputs, 160_000, groth16_verify_compressed); + + assert!(!out.output_value, "should fail because invalid G2"); + } + // Full end-to-end compressed Groth16 verification. This is heavy because it // runs Miller loop + final exponentiation in-circuit. Kept for completeness // but ignored by default; run explicitly when needed. @@ -1121,4 +1131,46 @@ mod tests { inputs.c.x += ark_bn254::Fq::ONE; })); } + + #[test] + fn test_ark_proof_decompress_matches() { + let ark_proof_bytes: [u8; 128] = [ + 55, 126, 31, 52, 68, 72, 45, 185, 179, 42, 69, 122, 227, 134, 234, 167, 80, 68, 65, + 142, 134, 133, 97, 24, 194, 180, 193, 213, 111, 19, 12, 42, 142, 193, 123, 63, 163, 6, + 122, 100, 126, 178, 41, 127, 97, 82, 169, 2, 30, 190, 130, 153, 110, 203, 2, 95, 89, + 162, 70, 74, 63, 232, 176, 42, 39, 119, 13, 172, 154, 135, 98, 126, 217, 67, 36, 222, + 136, 93, 161, 93, 1, 196, 101, 172, 163, 240, 105, 124, 107, 93, 222, 133, 118, 94, + 161, 14, 165, 232, 61, 136, 121, 145, 0, 171, 184, 234, 57, 160, 1, 248, 7, 195, 124, + 95, 50, 113, 24, 203, 211, 73, 196, 40, 173, 148, 179, 126, 12, 131, + ]; + let ark_proof = ark_groth16::Proof::::deserialize_compressed_unchecked( + &ark_proof_bytes[..], + ) + .unwrap(); + let ark_proof_bits: Vec = ark_proof_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + + let inputs = Groth16VerifyCompressedInput { + public: vec![], + proof: ark_proof_bits.try_into().unwrap(), + vk: ark_groth16::VerifyingKey::default(), + }; + + let result: StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(inputs, 80_000, |ctx, wires| { + let result_wires = wires.proof.deserialize_checked(ctx).b; + let mut output_ids = Vec::new(); + output_ids.extend(result_wires.x.iter()); + output_ids.extend(result_wires.y.iter()); + output_ids.extend(result_wires.z.iter()); + output_ids + }); + + let actual_result = G2Projective::from_bits_unchecked(result.output_value.clone()); + + let ark_proof_a_mont = G2Projective::as_montgomery(ark_proof.b.into()); + assert_eq!(actual_result, ark_proof_a_mont); + } } diff --git a/g16gen/src/passes/input_bits.rs b/g16gen/src/passes/input_bits.rs index 6543929..0cc60a2 100644 --- a/g16gen/src/passes/input_bits.rs +++ b/g16gen/src/passes/input_bits.rs @@ -4,13 +4,9 @@ use std::{ }; use g16ckt::{ - Fq2Wire, WireId, - ark::{CurveGroup, Field}, + WireId, circuit::CircuitInput, - gadgets::{ - bn254::{fq::Fq, fr::Fr}, - groth16::Groth16VerifyCompressedInput, - }, + gadgets::{bigint::BigIntWires, groth16::Groth16VerifyCompressedInput}, }; const INPUT_BITS_FILE: &str = "inputs.txt"; @@ -28,8 +24,8 @@ pub fn write_input_bits(inputs: &Groth16VerifyCompressedInput) -> std::io::Resul let mut bits = Vec::with_capacity(wire_ids.len()); // Extract public field element bits - for (wire_repr, value) in input_wires.public.iter().zip(inputs.0.public.iter()) { - let bits_fn = Fr::get_wire_bits_fn(wire_repr, value) + for (wire_repr, value) in input_wires.public.iter().zip(inputs.public.iter()) { + let bits_fn = BigIntWires::get_wire_bits_fn(wire_repr, value) .expect("Failed to get bits function for public input"); for &wire_id in wire_repr.iter() { @@ -39,59 +35,7 @@ pub fn write_input_bits(inputs: &Groth16VerifyCompressedInput) -> std::io::Resul } } - // Extract compressed point A (x-coordinate + y-flag) - let a_aff_std = inputs.0.a.into_affine(); - let a_x_m = Fq::as_montgomery(a_aff_std.x); - let a_flag = (a_aff_std.y.square()) - .sqrt() - .expect("y^2 must be QR") - .eq(&a_aff_std.y); - - let a_x_fn = Fq::get_wire_bits_fn(&input_wires.a.x_m, &a_x_m) - .expect("Failed to get bits function for point A x-coordinate"); - - for &wire_id in input_wires.a.x_m.iter() { - if let Some(bit) = a_x_fn(wire_id) { - bits.push(bit); - } - } - bits.push(a_flag); - - // Extract compressed point B (x-coordinate + y-flag) - let b_aff_std = inputs.0.b.into_affine(); - let b_x_m = Fq2Wire::as_montgomery(b_aff_std.x); - let b_flag = (b_aff_std.y.square()) - .sqrt() - .expect("y^2 must be QR in Fq2") - .eq(&b_aff_std.y); - - let b_x_fn = Fq2Wire::get_wire_bits_fn(&input_wires.b.p, &b_x_m) - .expect("Failed to get bits function for point B x-coordinate"); - - for &wire_id in input_wires.b.p.iter() { - if let Some(bit) = b_x_fn(wire_id) { - bits.push(bit); - } - } - bits.push(b_flag); - - // Extract compressed point C (x-coordinate + y-flag) - let c_aff_std = inputs.0.c.into_affine(); - let c_x_m = Fq::as_montgomery(c_aff_std.x); - let c_flag = (c_aff_std.y.square()) - .sqrt() - .expect("y^2 must be QR") - .eq(&c_aff_std.y); - - let c_x_fn = Fq::get_wire_bits_fn(&input_wires.c.x_m, &c_x_m) - .expect("Failed to get bits function for point C x-coordinate"); - - for &wire_id in input_wires.c.x_m.iter() { - if let Some(bit) = c_x_fn(wire_id) { - bits.push(bit); - } - } - bits.push(c_flag); + bits.extend_from_slice(&inputs.proof); // Verify we extracted the expected number of bits assert_eq!(