diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7fde427..0f6e521 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -77,4 +77,4 @@ jobs: - name: Run codespell run: | - codespell --skip="*.lock,./target" --ignore-words=.codespellignore + codespell --skip="*.lock,./target,blake3_test_vectors.json" --ignore-words=.codespellignore 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..a05f508 100644 --- a/g16ckt/src/gadgets/bn254/g1.rs +++ b/g16ckt/src/gadgets/bn254/g1.rs @@ -1,12 +1,16 @@ use std::{cmp::min, collections::HashMap, iter}; -use ark_ff::Zero; +use ark_ec::short_weierstrass::SWCurveConfig; +use ark_ff::{Field, Zero}; use circuit_component_macro::component; 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 +411,189 @@ 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, flag) = { + let (num, flag) = serialized_bits.split_at(Fq::N_BITS); + let a = Fq(BigIntWires { bits: num.to_vec() }); + // 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, &a, &r.square()); + // flag_0 is lsb, flag 1 is msb + (a_mont, [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(); + circuit.add_gate(crate::Gate { + wire_a: rhs_is_qr, + wire_b: flags_is_valid, + wire_c: tmp0, + gate_type: crate::GateType::And, + }); + tmp0 + }; + + 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 +1032,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..0ad22ad 100644 --- a/g16ckt/src/gadgets/bn254/g2.rs +++ b/g16ckt/src/gadgets/bn254/g2.rs @@ -1,13 +1,14 @@ use std::{cmp::min, collections::HashMap, iter::zip}; -use ark_ff::Zero; +use ark_ec::short_weierstrass::SWCurveConfig; +use ark_ff::{AdditiveGroup, Field, Zero}; use circuit_component_macro::component; 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 +525,281 @@ 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, 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 a = Fq2([ + Fq(BigIntWires { + bits: num1.to_vec(), + }), + Fq(BigIntWires { + bits: num2.to_vec(), + }), + ]); + + // 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, a.c0(), &r.square()); + let r = Fq::as_montgomery(ark_bn254::Fq::ONE); + let a_mont_y = Fq::mul_by_constant_montgomery(circuit, a.c1(), &r.square()); + + // flag_0 is lsb, flag 1 is msb + (Fq2([a_mont_x, a_mont_y]), [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(); + circuit.add_gate(crate::Gate { + wire_a: rhs_is_qr, + wire_b: flags_is_valid, + wire_c: tmp0, + gate_type: crate::GateType::And, + }); + tmp0 + }; + + 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 +956,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 +1164,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..5e23224 100644 --- a/g16ckt/src/gadgets/groth16.rs +++ b/g16ckt/src/gadgets/groth16.rs @@ -5,21 +5,24 @@ //! 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; 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, Fp254Impl, WireId, + circuit::{CircuitInput, CircuitMode, EncodeInput, FALSE_WIRE, WiresObject}, gadgets::{ - bigint, + bigint::BigIntWires, bn254::{ G2Projective, final_exponentiation::final_exponentiation_montgomery, fq::Fq, fq12::Fq12, fr::Fr, g1::G1Projective, pairing::multi_miller_loop_groth16_evaluate_montgomery_fast, }, + hash::blake3::{HashOutputWires, InputMessage, InputMessageWires, blake3_hash}, }, }; @@ -66,6 +69,28 @@ pub fn groth16_verify( vk, } = input; + let proof_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); + + let tmp0 = circuit.issue_wire(); + let tmp1 = 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: tmp1, + gate_type: crate::GateType::And, + }); + tmp1 + }; + // Standard verification with public inputs // MSM: sum_i public[i] * gamma_abc_g1[i+1] let bases: Vec = vk @@ -106,165 +131,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); - - 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 finexp_match = Fq12::equal_constant(circuit, &f, &Fq12::as_montgomery(alpha_beta)); -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, + let valid = circuit.issue_wire(); + circuit.add_gate(crate::Gate { + wire_a: finexp_match, + wire_b: proof_is_on_curve, + wire_c: valid, + gate_type: crate::GateType::And, + }); + valid } -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 +274,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 +385,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 +396,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 +406,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 +414,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 +424,124 @@ 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); + self.proof.iter().zip(repr.proof.0).for_each(|(x, y)| { + cache.feed_wire(y, *x); + }); + } +} - // 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); - } +/// Compressed representation of groth16 proof +pub struct Groth16VerifyCompressedRawInput { + pub public: InputMessage, + pub proof: SerializedCompressedProof, // 128 byte proof + pub vk: VerifyingKey, +} + +/// Compressed groth16 proof with public inputs +#[derive(Debug)] +pub struct Groth16VerifyCompressedRawInputWires { + pub public: InputMessageWires, + pub proof: SerializedCompressedProofWires, + pub vk: VerifyingKey, +} + +impl WiresObject for Groth16VerifyCompressedRawInputWires { + fn to_wires_vec(&self) -> Vec { + Groth16VerifyCompressedRawInput::collect_wire_ids(self) + } + + fn clone_from(&self, issue: &mut impl FnMut() -> WireId) -> Self { + Groth16VerifyCompressedRawInputWires { + public: self.public.clone_from(issue), + proof: self.proof.clone_from(issue), + vk: self.vk.clone(), } - 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); - } +impl CircuitInput for Groth16VerifyCompressedRawInput { + type WireRepr = Groth16VerifyCompressedRawInputWires; + + fn allocate(&self, mut issue: impl FnMut() -> WireId) -> Self::WireRepr { + Groth16VerifyCompressedRawInputWires { + public: InputMessageWires::new(&mut issue), + proof: SerializedCompressedProofWires::new(&mut issue), + vk: self.vk.clone(), } - cache.feed_wire(repr.c.y_flag, c_flag); } + fn collect_wire_ids(repr: &Self::WireRepr) -> Vec { + let mut ids = Vec::new(); + ids.extend(repr.public.to_wires_vec()); + ids.extend(repr.proof.to_wires_vec()); + ids + } +} + +impl> EncodeInput + for Groth16VerifyCompressedRawInput +{ + fn encode(&self, repr: &Groth16VerifyCompressedRawInputWires, cache: &mut M) { + self.public.encode(&repr.public, cache); + self.proof.iter().zip(repr.proof.0).for_each(|(x, y)| { + cache.feed_wire(y, *x); + }); + } +} + +// A way is to truncate the top 3 bits to make the hash fit in the scalar field +fn convert_hash_to_bigint_wires(out_hash: HashOutputWires) -> Vec { + let mut out_hash = out_hash.value; + // mask top 3 bits by taking the first byte of hash output and masking its top 3 bit + out_hash[0].0[5] = FALSE_WIRE; + out_hash[0].0[6] = FALSE_WIRE; + out_hash[0].0[7] = FALSE_WIRE; + // big endian to little endian ordering of BigIntWires + out_hash.reverse(); + let out_hash: Vec = out_hash.into_iter().flat_map(|x| x.0).collect(); + let out_hash = BigIntWires { + bits: out_hash[0..Fr::N_BITS].to_vec(), + }; + vec![Fr(out_hash)] +} + +pub fn groth16_verify_compressed_over_raw_public_input( + circuit: &mut C, + input: &Groth16VerifyCompressedRawInputWires, +) -> crate::WireId { + // convert InputMessage to scalar field elements + let out_hash = blake3_hash(circuit, input.public); + let hash_fr = convert_hash_to_bigint_wires(out_hash); + + let input_wires = Groth16VerifyCompressedInputWires { + public: hash_fr, + proof: input.proof, + vk: input.vk.clone(), + }; + groth16_verify_compressed(circuit, &input_wires) } #[cfg(test)] mod tests { - use ark_ec::{AffineRepr, CurveGroup, PrimeGroup}; - use ark_ff::UniformRand; + use std::str::FromStr; + + use ark_ec::{AffineRepr, CurveGroup, PrimeGroup, short_weierstrass::SWCurveConfig}; + use ark_ff::{PrimeField, 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 +858,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 +904,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 +938,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 +1163,188 @@ 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); + } + + fn convert_hash_to_bigint(raw_public_input_hash: blake3::Hash) -> ark_bn254::Fr { + let mut raw_public_input_hash = *raw_public_input_hash.as_bytes(); + raw_public_input_hash[0] &= 0b00011111; // mask top 3 bits to fit within scalar field + let c_val = ark_bn254::Fr::from_be_bytes_mod_order(&raw_public_input_hash); + c_val + } + + // verify groth16 proof end-to-end + // use raw public input to generate groth16-public-input + // meant to mimic how public inputs are handled by zkvms + #[test] + fn test_groth16_verify_compressed_true_small_for_raw_public_input() { + let mut rng = ChaCha20Rng::seed_from_u64(33333); + + // prover computes groth-public-input directly using raw_public_input + // in practice, these random bytes could be values like deposit index, public key + let raw_public_input: [u8; 40] = std::array::from_fn(|_| rng.r#gen()); + + let raw_public_input_hash = blake3::hash(&raw_public_input); + let c_val = convert_hash_to_bigint(raw_public_input_hash); + + let b = ark_bn254::Fr::rand(&mut rng); + let binv = b.inverse().unwrap(); + let a = c_val * binv; // should satisfy constraint: a * b = c + + let k = 4; + let circuit = DummyCircuit:: { + a: Some(a), + b: Some(b), + num_variables: 8, + num_constraints: 1 << k, + }; + let (pk, vk) = Groth16::::setup(circuit, &mut rng).unwrap(); + + let proof = Groth16::::prove(&pk, circuit, &mut rng).unwrap(); + + let ark_proof_bits: Vec = { + let mut proof_bytes = Vec::new(); + ark_groth16::Proof:: { + a: proof.a.into(), + b: proof.b.into(), + c: proof.c.into(), + } + .serialize_compressed(&mut proof_bytes) + .expect("serialize proof"); + let ark_proof_bits: Vec = proof_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + ark_proof_bits + }; + + let inputs = Groth16VerifyCompressedRawInput { + public: InputMessage { + byte_arr: raw_public_input, + }, + proof: ark_proof_bits.try_into().unwrap(), + vk, + }; + + let out: StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(inputs, 160_000, |ctx, wires| { + let ok = groth16_verify_compressed_over_raw_public_input(ctx, &wires); + vec![ok] + }); + + assert!(out.output_value[0]); + } + + // mock proof, vk and public keys imported from sp1 fibonacci program (alpenlabs/sp1:feat/export_proof_for_bin_ckt:test_e2e_prove_groth16) + #[test] + fn test_groth16_verify_compressed_true_small_using_mock_sp1_proof() { + let raw_public_input: [u8; 36] = [ + 55, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 13, 0, 0, 0, 21, 0, 0, 0, 34, 0, 0, 0, + 55, 0, 0, 0, 89, 0, 0, 0, + ]; + + let proof_bytes: [u8; 128] = [ + 208, 124, 179, 175, 21, 109, 24, 174, 150, 229, 234, 62, 194, 4, 178, 72, 237, 224, 28, + 240, 223, 242, 46, 98, 134, 7, 212, 187, 186, 1, 30, 152, 162, 105, 9, 230, 188, 90, + 150, 105, 239, 11, 254, 197, 77, 229, 17, 104, 247, 229, 212, 209, 88, 90, 133, 132, + 175, 43, 172, 181, 74, 147, 202, 38, 75, 78, 145, 234, 133, 96, 253, 250, 248, 2, 59, + 202, 187, 178, 32, 199, 140, 232, 113, 158, 164, 26, 223, 17, 145, 34, 161, 94, 193, + 33, 130, 151, 78, 88, 178, 191, 7, 214, 91, 3, 11, 103, 63, 176, 177, 27, 144, 186, + 169, 10, 87, 121, 60, 201, 242, 216, 3, 58, 87, 164, 184, 136, 147, 10, + ]; + + let vk_bytes: [u8; 328] = [ + 226, 242, 109, 190, 162, 153, 245, 34, 59, 100, 108, 177, 251, 51, 234, 219, 5, 157, + 148, 7, 85, 157, 116, 65, 223, 217, 2, 227, 167, 154, 77, 45, 171, 183, 61, 193, 127, + 188, 19, 2, 30, 36, 113, 224, 192, 139, 214, 125, 132, 1, 245, 43, 115, 214, 208, 116, + 131, 121, 76, 173, 71, 120, 24, 14, 12, 6, 243, 59, 188, 76, 121, 169, 202, 222, 242, + 83, 166, 128, 132, 211, 130, 241, 119, 136, 248, 133, 201, 175, 209, 118, 247, 203, 47, + 3, 103, 137, 237, 246, 146, 217, 92, 189, 222, 70, 221, 218, 94, 247, 212, 34, 67, 103, + 121, 68, 92, 94, 102, 0, 106, 66, 118, 30, 31, 18, 239, 222, 0, 24, 194, 18, 243, 174, + 183, 133, 228, 151, 18, 231, 169, 53, 51, 73, 170, 241, 37, 93, 251, 49, 183, 191, 96, + 114, 58, 72, 13, 146, 147, 147, 142, 25, 237, 34, 1, 251, 191, 54, 215, 39, 179, 99, + 122, 119, 118, 59, 61, 248, 184, 228, 40, 77, 53, 39, 175, 44, 254, 55, 12, 186, 244, + 65, 255, 3, 230, 116, 95, 132, 105, 130, 153, 33, 69, 2, 32, 192, 12, 94, 134, 224, 54, + 210, 70, 155, 204, 30, 240, 33, 95, 103, 21, 231, 141, 203, 199, 156, 3, 0, 0, 0, 0, 0, + 0, 0, 142, 117, 169, 138, 181, 40, 29, 69, 76, 115, 219, 51, 146, 119, 36, 245, 235, + 67, 55, 205, 148, 166, 160, 78, 138, 173, 176, 175, 28, 30, 9, 38, 76, 251, 81, 137, + 196, 193, 55, 229, 85, 135, 135, 236, 198, 54, 237, 80, 167, 204, 144, 208, 39, 194, 7, + 38, 93, 162, 61, 253, 208, 63, 28, 6, 28, 231, 41, 209, 79, 99, 32, 224, 222, 40, 96, + 161, 81, 236, 253, 79, 236, 178, 208, 234, 226, 224, 224, 127, 129, 121, 138, 56, 65, + 178, 234, 4, + ]; + let mut vk: ark_groth16::VerifyingKey = + ark_groth16::VerifyingKey::deserialize_compressed_unchecked(&vk_bytes[..]).unwrap(); + const SP1_VKEY_HASH: &str = + "71453366410619949346755464650355874340577815840172820125756847035126066871"; + let sp1_vkey_hash = BigUint::from_str(SP1_VKEY_HASH).unwrap(); + let sp1_vkey_hash: ark_bn254::Fr = sp1_vkey_hash.into(); + let sp1_vk_gamma = vk.gamma_abc_g1[0] + vk.gamma_abc_g1[1] * sp1_vkey_hash; + vk.gamma_abc_g1[0] = sp1_vk_gamma.into_affine(); + let _ = vk.gamma_abc_g1.remove(1); + + let ark_proof_bits: Vec = { + let ark_proof_bits: Vec = proof_bytes + .iter() + .flat_map(|&b| (0..8).map(move |i| ((b >> i) & 1) == 1)) + .collect(); + ark_proof_bits + }; + + let inputs = Groth16VerifyCompressedRawInput { + public: InputMessage { + byte_arr: raw_public_input, + }, + proof: ark_proof_bits.try_into().unwrap(), + vk, + }; + + let out: StreamingResult<_, _, Vec> = + CircuitBuilder::streaming_execute(inputs, 160_000, |ctx, wires| { + let ok = groth16_verify_compressed_over_raw_public_input(ctx, &wires); + vec![ok] + }); + + assert!(out.output_value[0]); + } } diff --git a/g16ckt/src/gadgets/hash/blake3.rs b/g16ckt/src/gadgets/hash/blake3.rs new file mode 100644 index 0000000..3e0d736 --- /dev/null +++ b/g16ckt/src/gadgets/hash/blake3.rs @@ -0,0 +1,653 @@ +//! Binary Circuit Implementation of Blake3 Hash +//! Supports input message of size less than or equals 1024 bytes only. +//! This limited range is sufficient for usecases concerning garbled circuit inputs + +use core::cmp::min; + +use ark_std::iter; +use num_bigint::BigUint; + +use crate::{ + CircuitContext, Gate, WireId, + circuit::{ + CircuitInput, CircuitMode, CircuitOutput, EncodeInput, ExecuteMode, FALSE_WIRE, WiresObject, + }, + gadgets::{basic::full_adder, bigint::BigIntWires}, +}; + +const OUT_LEN: usize = 32; +const BLOCK_LEN: usize = 64; +const CHUNK_LEN: usize = 1024; + +const CHUNK_START: u32 = 1 << 0; +const CHUNK_END: u32 = 1 << 1; +const ROOT: u32 = 1 << 3; + +#[derive(Debug, Clone, Copy)] +struct U32([WireId; 32]); + +impl U32 { + fn from_constant(n: u32) -> U32 { + let wires: Vec = BigIntWires::new_constant(32, &BigUint::from(n)) + .unwrap() + .bits; + U32(wires.try_into().unwrap()) + } + + fn xor(circuit: &mut C, a: U32, b: U32) -> U32 { + let c: Vec = (0..32) + .map(|i| { + let res = circuit.issue_wire(); + circuit.add_gate(Gate::xor(a.0[i], b.0[i], res)); + res + }) + .collect(); + U32(c.try_into().unwrap()) + } + + fn or(circuit: &mut C, a: U32, b: U32) -> U32 { + let c: Vec = (0..32) + .map(|i| { + let res = circuit.issue_wire(); + circuit.add_gate(Gate::or(a.0[i], b.0[i], res)); + res + }) + .collect(); + U32(c.try_into().unwrap()) + } + + fn rotate_right(value: U32, n: u32) -> U32 { + let mut result = [FALSE_WIRE; 32]; + let shift = (n % 32) as usize; + + for (i, result_i) in result.iter_mut().enumerate() { + // Compute the new position using modular arithmetic + let from_index = (i + shift) % 32; + *result_i = value.0[from_index]; + } + + U32(result) + } + + fn wrapping_add(circuit: &mut C, a: U32, b: U32) -> U32 { + let mut result = [FALSE_WIRE; 32]; + let mut carry = FALSE_WIRE; + + for (i, result_i) in result.iter_mut().enumerate() { + let ai = a.0[i]; + let bi = b.0[i]; + (*result_i, carry) = full_adder(circuit, ai, bi, carry); + } + + U32(result) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct U8(pub [WireId; 8]); + +impl U8 { + pub fn new(issue: impl FnMut() -> WireId) -> Self { + let v: Vec = iter::repeat_with(issue).take(8).collect(); + let v: U8 = U8(v.try_into().unwrap()); + v + } +} + +// Input Message is a byte array of size 'N' for N < 1024 +#[derive(Debug, Clone, Copy)] +pub struct InputMessage { + pub byte_arr: [u8; N], +} + +// Input Message is a byte array of size 'N' for N < 1024 +#[derive(Debug, Clone, Copy)] +pub struct InputMessageWires { + pub byte_arr: [U8; N], +} + +impl InputMessageWires { + pub fn new(mut issue: impl FnMut() -> WireId) -> Self { + let wires: Vec = std::array::from_fn::<_, N, _>(|_| U8::new(&mut issue)).to_vec(); + let wires: [U8; N] = wires.try_into().unwrap(); + InputMessageWires { byte_arr: wires } + } +} + +impl WiresObject for InputMessageWires { + fn to_wires_vec(&self) -> Vec { + self.byte_arr + .iter() + .flat_map(|fq| fq.0.iter().copied()) + .collect() + } + + fn clone_from(&self, wire_gen: &mut impl FnMut() -> WireId) -> Self { + InputMessageWires::new(wire_gen) + } +} + +impl CircuitInput for InputMessage { + type WireRepr = InputMessageWires; + + fn allocate(&self, mut issue: impl FnMut() -> WireId) -> Self::WireRepr { + InputMessageWires::new(&mut issue) + } + + fn collect_wire_ids(repr: &Self::WireRepr) -> Vec { + repr.byte_arr + .iter() + .flat_map(|fq| fq.0.iter().copied()) + .collect() + } +} + +impl> EncodeInput for InputMessage { + fn encode(&self, repr: &Self::WireRepr, cache: &mut M) { + self.byte_arr + .iter() + .zip(repr.byte_arr.iter()) + .for_each(|(x, y)| { + for (i, y_i) in y.0.iter().enumerate() { + let x_i = ((*x >> i) & 1) != 0; + cache.feed_wire(*y_i, x_i); + } + }); + } +} + +// 32 byte message hash +pub struct HashOutput { + pub value: [u8; 32], +} + +#[derive(Clone, Debug)] +pub struct HashOutputWires { + pub value: [U8; 32], +} + +impl HashOutputWires { + pub fn new(mut issue: impl FnMut() -> WireId) -> Self { + let wires: Vec = std::array::from_fn::<_, 32, _>(|_| U8::new(&mut issue)).to_vec(); + let wires: [U8; 32] = wires.try_into().unwrap(); + HashOutputWires { value: wires } + } +} + +impl WiresObject for HashOutputWires { + fn clone_from(&self, wire_gen: &mut impl FnMut() -> WireId) -> Self { + HashOutputWires::new(wire_gen) + } + fn to_wires_vec(&self) -> Vec { + self.value.into_iter().flat_map(|x| x.0).collect() + } +} + +impl CircuitOutput for HashOutput { + type WireRepr = HashOutputWires; // [U8; 32] + + fn decode(wires: Self::WireRepr, cache: &mut ExecuteMode) -> Self { + let wires = wires.to_wires_vec(); + + let bit_len = wires.len(); + + let mut bytes = vec![0u8; bit_len.div_ceil(8)]; + + for (i, w) in wires.iter().enumerate() { + let bit = cache.lookup_wire(*w).expect("missing wire value"); + if bit { + bytes[i / 8] |= 1u8 << (i % 8); + } + } + let bytes: [u8; 32] = bytes.try_into().unwrap(); + HashOutput { value: bytes } + } +} + +fn get_iv() -> [U32; 8] { + let iv2: [U32; 8] = [ + U32::from_constant(0x6A09E667), + U32::from_constant(0xBB67AE85), + U32::from_constant(0x3C6EF372), + U32::from_constant(0xA54FF53A), + U32::from_constant(0x510E527F), + U32::from_constant(0x9B05688C), + U32::from_constant(0x1F83D9AB), + U32::from_constant(0x5BE0CD19), + ]; + iv2 +} + +const MSG_PERMUTATION: [u8; 16] = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8]; + +// The mixing function, G, which mixes either a column or a diagonal. +#[allow(clippy::too_many_arguments)] +fn g( + circuit: &mut C, + state: &mut [U32; 16], + a: usize, + b: usize, + c: usize, + d: usize, + mx: U32, + my: U32, +) { + let tmp0 = U32::wrapping_add(circuit, state[a], state[b]); + state[a] = U32::wrapping_add(circuit, tmp0, mx); + state[d] = U32::rotate_right(U32::xor(circuit, state[d], state[a]), 16); + state[c] = U32::wrapping_add(circuit, state[c], state[d]); + state[b] = U32::rotate_right(U32::xor(circuit, state[b], state[c]), 12); + + let tmp0 = U32::wrapping_add(circuit, state[a], state[b]); + state[a] = U32::wrapping_add(circuit, tmp0, my); + state[d] = U32::rotate_right(U32::xor(circuit, state[d], state[a]), 8); + state[c] = U32::wrapping_add(circuit, state[c], state[d]); + state[b] = U32::rotate_right(U32::xor(circuit, state[b], state[c]), 7); +} + +fn round(circuit: &mut C, state: &mut [U32; 16], m: &[U32; 16]) { + // Mix the columns. + g(circuit, state, 0, 4, 8, 12, m[0], m[1]); + g(circuit, state, 1, 5, 9, 13, m[2], m[3]); + g(circuit, state, 2, 6, 10, 14, m[4], m[5]); + g(circuit, state, 3, 7, 11, 15, m[6], m[7]); + // Mix the diagonals. + g(circuit, state, 0, 5, 10, 15, m[8], m[9]); + g(circuit, state, 1, 6, 11, 12, m[10], m[11]); + g(circuit, state, 2, 7, 8, 13, m[12], m[13]); + g(circuit, state, 3, 4, 9, 14, m[14], m[15]); +} + +fn permute(m: &mut [U32; 16]) { + let mut permuted = [U32([FALSE_WIRE; 32]); 16]; + for i in 0..16 { + permuted[i] = m[MSG_PERMUTATION[i] as usize]; + } + *m = permuted; +} + +fn compress( + circuit: &mut C, + chaining_value: &[U32; 8], + block_words: &[U32; 16], + counter: u64, + block_len: U32, + flags: U32, +) -> [U32; 16] { + let counter_low = U32::from_constant(counter as u32); + let counter_high = U32::from_constant((counter >> 32) as u32); + #[rustfmt::skip] + let iv: [U32; 8] = get_iv(); + let mut state = [ + chaining_value[0], + chaining_value[1], + chaining_value[2], + chaining_value[3], + chaining_value[4], + chaining_value[5], + chaining_value[6], + chaining_value[7], + iv[0], + iv[1], + iv[2], + iv[3], + counter_low, + counter_high, + block_len, + flags, + ]; + + let mut block = *block_words; + + round(circuit, &mut state, &block); // round 1 + permute(&mut block); + round(circuit, &mut state, &block); // round 2 + permute(&mut block); + round(circuit, &mut state, &block); // round 3 + permute(&mut block); + round(circuit, &mut state, &block); // round 4 + permute(&mut block); + round(circuit, &mut state, &block); // round 5 + permute(&mut block); + round(circuit, &mut state, &block); // round 6 + permute(&mut block); + round(circuit, &mut state, &block); // round 7 + + for i in 0..8 { + state[i] = U32::xor(circuit, state[i], state[i + 8]); + state[i + 8] = U32::xor(circuit, state[i + 8], chaining_value[i]); + } + state +} + +fn first_8_words(compression_output: [U32; 16]) -> [U32; 8] { + compression_output[0..8].try_into().unwrap() +} + +fn words_from_little_endian_bytes(bytes: &[U8], words: &mut [U32]) { + debug_assert_eq!(bytes.len(), 4 * words.len()); + for (four_bytes, word) in bytes.chunks_exact(4).zip(words) { + let wire_vec: Vec = four_bytes.iter().flat_map(|x| x.0).collect(); + let app_four_bytes: U32 = U32(wire_vec.try_into().unwrap()); + *word = app_four_bytes; + } +} + +struct Output { + input_chaining_value: [U32; 8], + block_words: [U32; 16], + block_len: U32, + flags: U32, +} + +impl Output { + fn root_output_bytes(&self, circuit: &mut C, out_slice: &mut [U8]) { + let root = U32::from_constant(ROOT); + for (output_block_counter, out_block) in out_slice.chunks_mut(2 * OUT_LEN).enumerate() { + let flags = U32::or(circuit, self.flags, root); + let words = compress( + circuit, + &self.input_chaining_value, + &self.block_words, + output_block_counter as u64, + self.block_len, + flags, + ); + for (word_bits, out_word_bits) in words.iter().zip(out_block.chunks_mut(4)) { + for (i, byte_bits) in out_word_bits.iter_mut().enumerate() { + let arr: U8 = U8(word_bits.0[8 * i..(i + 1) * 8].try_into().unwrap()); + *byte_bits = arr; + } + } + } + } +} + +struct ChunkState { + chaining_value: [U32; 8], + chunk_counter: u64, + block: [U8; BLOCK_LEN], + block_len: u8, + blocks_compressed: u8, + flags: U32, +} + +impl ChunkState { + fn new(key_words: [U32; 8], chunk_counter: u64, flags: U32) -> Self { + Self { + chaining_value: key_words, + chunk_counter, + block: [U8([FALSE_WIRE; 8]); BLOCK_LEN], + block_len: 0, + blocks_compressed: 0, + flags, + } + } + + fn len(&self) -> usize { + BLOCK_LEN * self.blocks_compressed as usize + self.block_len as usize + } + + fn start_flag(&self) -> U32 { + let r = if self.blocks_compressed == 0 { + CHUNK_START + } else { + 0 + }; + U32::from_constant(r) + } + + fn update(&mut self, circuit: &mut C, mut input: &[U8]) { + let zero_gate = FALSE_WIRE; + let block_len = U32::from_constant(BLOCK_LEN as u32); + while !input.is_empty() { + // If the block buffer is full, compress it and clear it. More + // input is coming, so this compression is not CHUNK_END. + if self.block_len as usize == BLOCK_LEN { + let mut block_words = [U32([zero_gate; 32]); 16]; + words_from_little_endian_bytes(&self.block, &mut block_words); + let start_flag = self.start_flag(); + let flags = U32::or(circuit, self.flags, start_flag); + let cmp = compress( + circuit, + &self.chaining_value, + &block_words, + self.chunk_counter, + block_len, + flags, + ); + self.chaining_value = first_8_words(cmp); + self.blocks_compressed += 1; + self.block = [U8([zero_gate; 8]); BLOCK_LEN]; + self.block_len = 0; + } + + // Copy input bytes into the block buffer. + let want = BLOCK_LEN - self.block_len as usize; + let take = min(want, input.len()); + self.block[self.block_len as usize..][..take].copy_from_slice(&input[..take]); + self.block_len += take as u8; + input = &input[take..]; + } + } + + fn output(&self, circuit: &mut C) -> Output { + let zero_gate = FALSE_WIRE; + let mut block_words = [U32([zero_gate; 32]); 16]; + words_from_little_endian_bytes(&self.block, &mut block_words); + let start_flag = self.start_flag(); + let flags = U32::or(circuit, self.flags, start_flag); + let chunk_end = U32::from_constant(CHUNK_END); + let flags = U32::or(circuit, flags, chunk_end); + + Output { + input_chaining_value: self.chaining_value, + block_words, + block_len: U32::from_constant(self.block_len as u32), + flags, + } + } +} + +/// An incremental hasher that can accept any number of writes. +pub(crate) struct Hasher { + chunk_state: ChunkState, +} + +impl Hasher { + fn new_internal(key_words: [U32; 8], flags: U32) -> Self { + Self { + chunk_state: ChunkState::new(key_words, 0, flags), + } + } + + /// Construct a new `Hasher` for the regular hash function. + pub(crate) fn new() -> Self { + let zero_gate = FALSE_WIRE; + let iv = get_iv(); + let zero = U32([zero_gate; 32]); + Self::new_internal(iv, zero) + } + + /// Add input to the hash state. This can be called any number of times. + pub(crate) fn update(&mut self, circuit: &mut C, mut input: &[U8]) { + while !input.is_empty() { + // Compress input bytes into the current chunk state. + let want = CHUNK_LEN - self.chunk_state.len(); + let take = min(want, input.len()); + self.chunk_state.update(circuit, &input[..take]); + input = &input[take..]; + } + } + + /// Finalize the hash and write any number of output bytes. + pub(crate) fn finalize(&self, circuit: &mut C, out_slice: &mut [U8]) { + let output = self.chunk_state.output(circuit); + output.root_output_bytes(circuit, out_slice); + } +} + +/// The function generates 32 byte output hash for given input message +pub fn blake3_hash( + circuit: &mut C, + input_message_bytes: InputMessageWires, +) -> HashOutputWires { + assert!( + input_message_bytes.byte_arr.len() <= 1024, + "This BLAKE3 implementation doesn't support messages longer than 1024 bytes" + ); + let mut hasher = Hasher::new(); + hasher.update(circuit, &input_message_bytes.byte_arr); + + let mut hash = [U8([FALSE_WIRE; 8]); 32]; + hasher.finalize(circuit, &mut hash); + HashOutputWires { value: hash } +} + +#[cfg(test)] +mod test { + + use std::{fs::File, io::BufReader, str::FromStr}; + + use blake3::CHUNK_LEN; + use rand::Rng; + + use super::blake3_hash; + use crate::{ + circuit::CircuitBuilder, + gadgets::hash::blake3::{HashOutput, InputMessage}, + }; + + fn validate_blake3_hash_for_input(inputs: InputMessage) { + let mut ref_hasher = blake3::Hasher::new(); + ref_hasher.update(&inputs.byte_arr); + let ref_hash = ref_hasher.finalize(); + let ref_hash = ref_hash.as_bytes(); + + let calc_hash = + CircuitBuilder::streaming_execute::<_, _, HashOutput>(inputs, 10_000, |ctx, input| { + let r = blake3_hash(ctx, *input); + r + }); + + assert_eq!(calc_hash.output_value.value, *ref_hash); + } + + #[test] + fn test_blake3_hash_for_finite_len_random_input() { + let mut byte_arr = [0u8; 32]; + rand::thread_rng().try_fill(&mut byte_arr[..]).unwrap(); + + let inputs = InputMessage { byte_arr }; + validate_blake3_hash_for_input(inputs); + } + + #[test] + fn test_zero_length() { + let inputs = InputMessage { byte_arr: [] }; + validate_blake3_hash_for_input(inputs); + } + + #[test] + fn test_max_length() { + let inputs = InputMessage { + byte_arr: [0; CHUNK_LEN], + }; + + validate_blake3_hash_for_input(inputs); + } + + #[test] + #[should_panic( + expected = "This BLAKE3 implementation doesn't support messages longer than 1024 bytes" + )] + fn test_message_too_long() { + let inputs = InputMessage { + byte_arr: [0; CHUNK_LEN + 1], + }; + + validate_blake3_hash_for_input(inputs); + } + + #[test] + fn test_vectors() { + use serde::Deserialize; + + #[derive(Debug, Deserialize)] + struct TestVectors { + cases: Vec, + } + + #[derive(Debug, Deserialize)] + struct TestVector { + input_len: usize, + hash: String, + } + + fn read_test_vectors() -> Vec<(Vec, String)> { + let path = "src/gadgets/hash/blake3_test_vectors.json"; + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + + let test_vectors: TestVectors = serde_json::from_reader(reader).unwrap(); + test_vectors + .cases + .iter() + .filter(|vector| vector.input_len <= 1024) + .map(|vector| { + let message = (0..251u8).cycle().take(vector.input_len).collect(); + let expected_hash = String::from_str(&vector.hash[0..64]).unwrap(); + (message, expected_hash) + }) + .collect() + } + + fn validate_blake3_hash_for_input_given_hash( + inputs: InputMessage, + ref_hash: String, + ) { + fn bytes_to_hex(bytes: [u8; 32]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() + } + + let calc_hash = CircuitBuilder::streaming_execute::<_, _, HashOutput>( + inputs, + 10_000, + |ctx, input| { + let r = blake3_hash(ctx, *input); + r + }, + ); + + let calc_hash = bytes_to_hex(calc_hash.output_value.value); + + assert_eq!(calc_hash, ref_hash); + } + + // Dispatcher: second argument must be an array literal of consts + macro_rules! dispatch_input { + ($bytes:expr, $expected_hash:expr, vec![$($n:literal),* $(,)?]) => {{ + match $bytes.len() { + $( + $n => { + let arr: [u8; $n] = $bytes.as_slice().try_into().unwrap(); + let msg = InputMessage::<$n> { byte_arr: arr }; + validate_blake3_hash_for_input_given_hash(msg, $expected_hash); + } + )* + _ => { panic!("unexpected length of input") } + } + }}; + } + + for (input_bytes, expected_hash) in read_test_vectors() { + dispatch_input!( + input_bytes, + expected_hash, + vec![ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 63, 64, 65, 127, 128, 129, 1023, 1024 + ] + ); + } + } +} diff --git a/g16ckt/src/gadgets/hash/blake3_test_vectors.json b/g16ckt/src/gadgets/hash/blake3_test_vectors.json new file mode 100644 index 0000000..8ce8f6d --- /dev/null +++ b/g16ckt/src/gadgets/hash/blake3_test_vectors.json @@ -0,0 +1,217 @@ +{ + "_comment": "Each test is an input length and three outputs, one for each of the hash, keyed_hash, and derive_key modes. The input in each case is filled with a repeating sequence of 251 bytes: 0, 1, 2, ..., 249, 250, 0, 1, ..., and so on. The key used with keyed_hash is the 32-byte ASCII string \"whats the Elvish word for friend\", also given in the `key` field below. The context string used with derive_key is the ASCII string \"BLAKE3 2019-12-27 16:29:52 test vectors context\", also given in the `context_string` field below. Outputs are encoded as hexadecimal. Each case is an extended output, and implementations should also check that the first 32 bytes match their default-length output.", + "key": "whats the Elvish word for friend", + "context_string": "BLAKE3 2019-12-27 16:29:52 test vectors context", + "cases": [ + { + "input_len": 0, + "hash": "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d", + "keyed_hash": "92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26b18171a2f22a4b94822c701f107153dba24918c4bae4d2945c20ece13387627d3b73cbf97b797d5e59948c7ef788f54372df45e45e4293c7dc18c1d41144a9758be58960856be1eabbe22c2653190de560ca3b2ac4aa692a9210694254c371e851bc8f", + "derive_key": "2cc39783c223154fea8dfb7c1b1660f2ac2dcbd1c1de8277b0b0dd39b7e50d7d905630c8be290dfcf3e6842f13bddd573c098c3f17361f1f206b8cad9d088aa4a3f746752c6b0ce6a83b0da81d59649257cdf8eb3e9f7d4998e41021fac119deefb896224ac99f860011f73609e6e0e4540f93b273e56547dfd3aa1a035ba6689d89a0" + }, + { + "input_len": 1, + "hash": "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213c3a6cb8bf623e20cdb535f8d1a5ffb86342d9c0b64aca3bce1d31f60adfa137b358ad4d79f97b47c3d5e79f179df87a3b9776ef8325f8329886ba42f07fb138bb502f4081cbcec3195c5871e6c23e2cc97d3c69a613eba131e5f1351f3f1da786545e5", + "keyed_hash": "6d7878dfff2f485635d39013278ae14f1454b8c0a3a2d34bc1ab38228a80c95b6568c0490609413006fbd428eb3fd14e7756d90f73a4725fad147f7bf70fd61c4e0cf7074885e92b0e3f125978b4154986d4fb202a3f331a3fb6cf349a3a70e49990f98fe4289761c8602c4e6ab1138d31d3b62218078b2f3ba9a88e1d08d0dd4cea11", + "derive_key": "b3e2e340a117a499c6cf2398a19ee0d29cca2bb7404c73063382693bf66cb06c5827b91bf889b6b97c5477f535361caefca0b5d8c4746441c57617111933158950670f9aa8a05d791daae10ac683cbef8faf897c84e6114a59d2173c3f417023a35d6983f2c7dfa57e7fc559ad751dbfb9ffab39c2ef8c4aafebc9ae973a64f0c76551" + }, + { + "input_len": 2, + "hash": "7b7015bb92cf0b318037702a6cdd81dee41224f734684c2c122cd6359cb1ee63d8386b22e2ddc05836b7c1bb693d92af006deb5ffbc4c70fb44d0195d0c6f252faac61659ef86523aa16517f87cb5f1340e723756ab65efb2f91964e14391de2a432263a6faf1d146937b35a33621c12d00be8223a7f1919cec0acd12097ff3ab00ab1", + "keyed_hash": "5392ddae0e0a69d5f40160462cbd9bd889375082ff224ac9c758802b7a6fd20a9ffbf7efd13e989a6c246f96d3a96b9d279f2c4e63fb0bdff633957acf50ee1a5f658be144bab0f6f16500dee4aa5967fc2c586d85a04caddec90fffb7633f46a60786024353b9e5cebe277fcd9514217fee2267dcda8f7b31697b7c54fab6a939bf8f", + "derive_key": "1f166565a7df0098ee65922d7fea425fb18b9943f19d6161e2d17939356168e6daa59cae19892b2d54f6fc9f475d26031fd1c22ae0a3e8ef7bdb23f452a15e0027629d2e867b1bb1e6ab21c71297377750826c404dfccc2406bd57a83775f89e0b075e59a7732326715ef912078e213944f490ad68037557518b79c0086de6d6f6cdd2" + }, + { + "input_len": 3, + "hash": "e1be4d7a8ab5560aa4199eea339849ba8e293d55ca0a81006726d184519e647f5b49b82f805a538c68915c1ae8035c900fd1d4b13902920fd05e1450822f36de9454b7e9996de4900c8e723512883f93f4345f8a58bfe64ee38d3ad71ab027765d25cdd0e448328a8e7a683b9a6af8b0af94fa09010d9186890b096a08471e4230a134", + "keyed_hash": "39e67b76b5a007d4921969779fe666da67b5213b096084ab674742f0d5ec62b9b9142d0fab08e1b161efdbb28d18afc64d8f72160c958e53a950cdecf91c1a1bbab1a9c0f01def762a77e2e8545d4dec241e98a89b6db2e9a5b070fc110caae2622690bd7b76c02ab60750a3ea75426a6bb8803c370ffe465f07fb57def95df772c39f", + "derive_key": "440aba35cb006b61fc17c0529255de438efc06a8c9ebf3f2ddac3b5a86705797f27e2e914574f4d87ec04c379e12789eccbfbc15892626042707802dbe4e97c3ff59dca80c1e54246b6d055154f7348a39b7d098b2b4824ebe90e104e763b2a447512132cede16243484a55a4e40a85790038bb0dcf762e8c053cabae41bbe22a5bff7" + }, + { + "input_len": 4, + "hash": "f30f5ab28fe047904037f77b6da4fea1e27241c5d132638d8bedce9d40494f328f603ba4564453e06cdcee6cbe728a4519bbe6f0d41e8a14b5b225174a566dbfa61b56afb1e452dc08c804f8c3143c9e2cc4a31bb738bf8c1917b55830c6e65797211701dc0b98daa1faeaa6ee9e56ab606ce03a1a881e8f14e87a4acf4646272cfd12", + "keyed_hash": "7671dde590c95d5ac9616651ff5aa0a27bee5913a348e053b8aa9108917fe070116c0acff3f0d1fa97ab38d813fd46506089118147d83393019b068a55d646251ecf81105f798d76a10ae413f3d925787d6216a7eb444e510fd56916f1d753a5544ecf0072134a146b2615b42f50c179f56b8fae0788008e3e27c67482349e249cb86a", + "derive_key": "f46085c8190d69022369ce1a18880e9b369c135eb93f3c63550d3e7630e91060fbd7d8f4258bec9da4e05044f88b91944f7cab317a2f0c18279629a3867fad0662c9ad4d42c6f27e5b124da17c8c4f3a94a025ba5d1b623686c6099d202a7317a82e3d95dae46a87de0555d727a5df55de44dab799a20dffe239594d6e99ed17950910" + }, + { + "input_len": 5, + "hash": "b40b44dfd97e7a84a996a91af8b85188c66c126940ba7aad2e7ae6b385402aa2ebcfdac6c5d32c31209e1f81a454751280db64942ce395104e1e4eaca62607de1c2ca748251754ea5bbe8c20150e7f47efd57012c63b3c6a6632dc1c7cd15f3e1c999904037d60fac2eb9397f2adbe458d7f264e64f1e73aa927b30988e2aed2f03620", + "keyed_hash": "73ac69eecf286894d8102018a6fc729f4b1f4247d3703f69bdc6a5fe3e0c84616ab199d1f2f3e53bffb17f0a2209fe8b4f7d4c7bae59c2bc7d01f1ff94c67588cc6b38fa6024886f2c078bfe09b5d9e6584cd6c521c3bb52f4de7687b37117a2dbbec0d59e92fa9a8cc3240d4432f91757aabcae03e87431dac003e7d73574bfdd8218", + "derive_key": "1f24eda69dbcb752847ec3ebb5dd42836d86e58500c7c98d906ecd82ed9ae47f6f48a3f67e4e43329c9a89b1ca526b9b35cbf7d25c1e353baffb590fd79be58ddb6c711f1a6b60e98620b851c688670412fcb0435657ba6b638d21f0f2a04f2f6b0bd8834837b10e438d5f4c7c2c71299cf7586ea9144ed09253d51f8f54dd6bff719d" + }, + { + "input_len": 6, + "hash": "06c4e8ffb6872fad96f9aaca5eee1553eb62aed0ad7198cef42e87f6a616c844611a30c4e4f37fe2fe23c0883cde5cf7059d88b657c7ed2087e3d210925ede716435d6d5d82597a1e52b9553919e804f5656278bd739880692c94bff2824d8e0b48cac1d24682699e4883389dc4f2faa2eb3b4db6e39debd5061ff3609916f3e07529a", + "keyed_hash": "82d3199d0013035682cc7f2a399d4c212544376a839aa863a0f4c91220ca7a6dc2ffb3aa05f2631f0fa9ac19b6e97eb7e6669e5ec254799350c8b8d189e8807800842a5383c4d907c932f34490aaf00064de8cdb157357bde37c1504d2960034930887603abc5ccb9f5247f79224baff6120a3c622a46d7b1bcaee02c5025460941256", + "derive_key": "be96b30b37919fe4379dfbe752ae77b4f7e2ab92f7ff27435f76f2f065f6a5f435ae01a1d14bd5a6b3b69d8cbd35f0b01ef2173ff6f9b640ca0bd4748efa398bf9a9c0acd6a66d9332fdc9b47ffe28ba7ab6090c26747b85f4fab22f936b71eb3f64613d8bd9dfabe9bb68da19de78321b481e5297df9e40ec8a3d662f3e1479c65de0" + }, + { + "input_len": 7, + "hash": "3f8770f387faad08faa9d8414e9f449ac68e6ff0417f673f602a646a891419fe66036ef6e6d1a8f54baa9fed1fc11c77cfb9cff65bae915045027046ebe0c01bf5a941f3bb0f73791d3fc0b84370f9f30af0cd5b0fc334dd61f70feb60dad785f070fef1f343ed933b49a5ca0d16a503f599a365a4296739248b28d1a20b0e2cc8975c", + "keyed_hash": "af0a7ec382aedc0cfd626e49e7628bc7a353a4cb108855541a5651bf64fbb28a7c5035ba0f48a9c73dabb2be0533d02e8fd5d0d5639a18b2803ba6bf527e1d145d5fd6406c437b79bcaad6c7bdf1cf4bd56a893c3eb9510335a7a798548c6753f74617bede88bef924ba4b334f8852476d90b26c5dc4c3668a2519266a562c6c8034a6", + "derive_key": "dc3b6485f9d94935329442916b0d059685ba815a1fa2a14107217453a7fc9f0e66266db2ea7c96843f9d8208e600a73f7f45b2f55b9e6d6a7ccf05daae63a3fdd10b25ac0bd2e224ce8291f88c05976d575df998477db86fb2cfbbf91725d62cb57acfeb3c2d973b89b503c2b60dde85a7802b69dc1ac2007d5623cbea8cbfb6b181f5" + }, + { + "input_len": 8, + "hash": "2351207d04fc16ade43ccab08600939c7c1fa70a5c0aaca76063d04c3228eaeb725d6d46ceed8f785ab9f2f9b06acfe398c6699c6129da084cb531177445a682894f9685eaf836999221d17c9a64a3a057000524cd2823986db378b074290a1a9b93a22e135ed2c14c7e20c6d045cd00b903400374126676ea78874d79f2dd7883cf5c", + "keyed_hash": "be2f5495c61cba1bb348a34948c004045e3bd4dae8f0fe82bf44d0da245a060048eb5e68ce6dea1eb0229e144f578b3aa7e9f4f85febd135df8525e6fe40c6f0340d13dd09b255ccd5112a94238f2be3c0b5b7ecde06580426a93e0708555a265305abf86d874e34b4995b788e37a823491f25127a502fe0704baa6bfdf04e76c13276", + "derive_key": "2b166978cef14d9d438046c720519d8b1cad707e199746f1562d0c87fbd32940f0e2545a96693a66654225ebbaac76d093bfa9cd8f525a53acb92a861a98c42e7d1c4ae82e68ab691d510012edd2a728f98cd4794ef757e94d6546961b4f280a51aac339cc95b64a92b83cc3f26d8af8dfb4c091c240acdb4d47728d23e7148720ef04" + }, + { + "input_len": 63, + "hash": "e9bc37a594daad83be9470df7f7b3798297c3d834ce80ba85d6e207627b7db7b1197012b1e7d9af4d7cb7bdd1f3bb49a90a9b5dec3ea2bbc6eaebce77f4e470cbf4687093b5352f04e4a4570fba233164e6acc36900e35d185886a827f7ea9bdc1e5c3ce88b095a200e62c10c043b3e9bc6cb9b6ac4dfa51794b02ace9f98779040755", + "keyed_hash": "bb1eb5d4afa793c1ebdd9fb08def6c36d10096986ae0cfe148cd101170ce37aea05a63d74a840aecd514f654f080e51ac50fd617d22610d91780fe6b07a26b0847abb38291058c97474ef6ddd190d30fc318185c09ca1589d2024f0a6f16d45f11678377483fa5c005b2a107cb9943e5da634e7046855eaa888663de55d6471371d55d", + "derive_key": "b6451e30b953c206e34644c6803724e9d2725e0893039cfc49584f991f451af3b89e8ff572d3da4f4022199b9563b9d70ebb616efff0763e9abec71b550f1371e233319c4c4e74da936ba8e5bbb29a598e007a0bbfa929c99738ca2cc098d59134d11ff300c39f82e2fce9f7f0fa266459503f64ab9913befc65fddc474f6dc1c67669" + }, + { + "input_len": 64, + "hash": "4eed7141ea4a5cd4b788606bd23f46e212af9cacebacdc7d1f4c6dc7f2511b98fc9cc56cb831ffe33ea8e7e1d1df09b26efd2767670066aa82d023b1dfe8ab1b2b7fbb5b97592d46ffe3e05a6a9b592e2949c74160e4674301bc3f97e04903f8c6cf95b863174c33228924cdef7ae47559b10b294acd660666c4538833582b43f82d74", + "keyed_hash": "ba8ced36f327700d213f120b1a207a3b8c04330528586f414d09f2f7d9ccb7e68244c26010afc3f762615bbac552a1ca909e67c83e2fd5478cf46b9e811efccc93f77a21b17a152ebaca1695733fdb086e23cd0eb48c41c034d52523fc21236e5d8c9255306e48d52ba40b4dac24256460d56573d1312319afcf3ed39d72d0bfc69acb", + "derive_key": "a5c4a7053fa86b64746d4bb688d06ad1f02a18fce9afd3e818fefaa7126bf73e9b9493a9befebe0bf0c9509fb3105cfa0e262cde141aa8e3f2c2f77890bb64a4cca96922a21ead111f6338ad5244f2c15c44cb595443ac2ac294231e31be4a4307d0a91e874d36fc9852aeb1265c09b6e0cda7c37ef686fbbcab97e8ff66718be048bb" + }, + { + "input_len": 65, + "hash": "de1e5fa0be70df6d2be8fffd0e99ceaa8eb6e8c93a63f2d8d1c30ecb6b263dee0e16e0a4749d6811dd1d6d1265c29729b1b75a9ac346cf93f0e1d7296dfcfd4313b3a227faaaaf7757cc95b4e87a49be3b8a270a12020233509b1c3632b3485eef309d0abc4a4a696c9decc6e90454b53b000f456a3f10079072baaf7a981653221f2c", + "keyed_hash": "c0a4edefa2d2accb9277c371ac12fcdbb52988a86edc54f0716e1591b4326e72d5e795f46a596b02d3d4bfb43abad1e5d19211152722ec1f20fef2cd413e3c22f2fc5da3d73041275be6ede3517b3b9f0fc67ade5956a672b8b75d96cb43294b9041497de92637ed3f2439225e683910cb3ae923374449ca788fb0f9bea92731bc26ad", + "derive_key": "51fd05c3c1cfbc8ed67d139ad76f5cf8236cd2acd26627a30c104dfd9d3ff8a82b02e8bd36d8498a75ad8c8e9b15eb386970283d6dd42c8ae7911cc592887fdbe26a0a5f0bf821cd92986c60b2502c9be3f98a9c133a7e8045ea867e0828c7252e739321f7c2d65daee4468eb4429efae469a42763f1f94977435d10dccae3e3dce88d" + }, + { + "input_len": 127, + "hash": "d81293fda863f008c09e92fc382a81f5a0b4a1251cba1634016a0f86a6bd640de3137d477156d1fde56b0cf36f8ef18b44b2d79897bece12227539ac9ae0a5119da47644d934d26e74dc316145dcb8bb69ac3f2e05c242dd6ee06484fcb0e956dc44355b452c5e2bbb5e2b66e99f5dd443d0cbcaaafd4beebaed24ae2f8bb672bcef78", + "keyed_hash": "c64200ae7dfaf35577ac5a9521c47863fb71514a3bcad18819218b818de85818ee7a317aaccc1458f78d6f65f3427ec97d9c0adb0d6dacd4471374b621b7b5f35cd54663c64dbe0b9e2d95632f84c611313ea5bd90b71ce97b3cf645776f3adc11e27d135cbadb9875c2bf8d3ae6b02f8a0206aba0c35bfe42574011931c9a255ce6dc", + "derive_key": "c91c090ceee3a3ac81902da31838012625bbcd73fcb92e7d7e56f78deba4f0c3feeb3974306966ccb3e3c69c337ef8a45660ad02526306fd685c88542ad00f759af6dd1adc2e50c2b8aac9f0c5221ff481565cf6455b772515a69463223202e5c371743e35210bbbbabd89651684107fd9fe493c937be16e39cfa7084a36207c99bea3" + }, + { + "input_len": 128, + "hash": "f17e570564b26578c33bb7f44643f539624b05df1a76c81f30acd548c44b45efa69faba091427f9c5c4caa873aa07828651f19c55bad85c47d1368b11c6fd99e47ecba5820a0325984d74fe3e4058494ca12e3f1d3293d0010a9722f7dee64f71246f75e9361f44cc8e214a100650db1313ff76a9f93ec6e84edb7add1cb4a95019b0c", + "keyed_hash": "b04fe15577457267ff3b6f3c947d93be581e7e3a4b018679125eaf86f6a628ecd86bbe0001f10bda47e6077b735016fca8119da11348d93ca302bbd125bde0db2b50edbe728a620bb9d3e6f706286aedea973425c0b9eedf8a38873544cf91badf49ad92a635a93f71ddfcee1eae536c25d1b270956be16588ef1cfef2f1d15f650bd5", + "derive_key": "81720f34452f58a0120a58b6b4608384b5c51d11f39ce97161a0c0e442ca022550e7cd651e312f0b4c6afb3c348ae5dd17d2b29fab3b894d9a0034c7b04fd9190cbd90043ff65d1657bbc05bfdecf2897dd894c7a1b54656d59a50b51190a9da44db426266ad6ce7c173a8c0bbe091b75e734b4dadb59b2861cd2518b4e7591e4b83c9" + }, + { + "input_len": 129, + "hash": "683aaae9f3c5ba37eaaf072aed0f9e30bac0865137bae68b1fde4ca2aebdcb12f96ffa7b36dd78ba321be7e842d364a62a42e3746681c8bace18a4a8a79649285c7127bf8febf125be9de39586d251f0d41da20980b70d35e3dac0eee59e468a894fa7e6a07129aaad09855f6ad4801512a116ba2b7841e6cfc99ad77594a8f2d181a7", + "keyed_hash": "d4a64dae6cdccbac1e5287f54f17c5f985105457c1a2ec1878ebd4b57e20d38f1c9db018541eec241b748f87725665b7b1ace3e0065b29c3bcb232c90e37897fa5aaee7e1e8a2ecfcd9b51463e42238cfdd7fee1aecb3267fa7f2128079176132a412cd8aaf0791276f6b98ff67359bd8652ef3a203976d5ff1cd41885573487bcd683", + "derive_key": "938d2d4435be30eafdbb2b7031f7857c98b04881227391dc40db3c7b21f41fc18d72d0f9c1de5760e1941aebf3100b51d64644cb459eb5d20258e233892805eb98b07570ef2a1787cd48e117c8d6a63a68fd8fc8e59e79dbe63129e88352865721c8d5f0cf183f85e0609860472b0d6087cefdd186d984b21542c1c780684ed6832d8d" + }, + { + "input_len": 1023, + "hash": "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11a182d27a591b05592b15607500e1e8dd56bc6c7fc063715b7a1d737df5bad3339c56778957d870eb9717b57ea3d9fb68d1b55127bba6a906a4a24bbd5acb2d123a37b28f9e9a81bbaae360d58f85e5fc9d75f7c370a0cc09b6522d9c8d822f2f28f485", + "keyed_hash": "c951ecdf03288d0fcc96ee3413563d8a6d3589547f2c2fb36d9786470f1b9d6e890316d2e6d8b8c25b0a5b2180f94fb1a158ef508c3cde45e2966bd796a696d3e13efd86259d756387d9becf5c8bf1ce2192b87025152907b6d8cc33d17826d8b7b9bc97e38c3c85108ef09f013e01c229c20a83d9e8efac5b37470da28575fd755a10", + "derive_key": "74a16c1c3d44368a86e1ca6df64be6a2f64cce8f09220787450722d85725dea59c413264404661e9e4d955409dfe4ad3aa487871bcd454ed12abfe2c2b1eb7757588cf6cb18d2eccad49e018c0d0fec323bec82bf1644c6325717d13ea712e6840d3e6e730d35553f59eff5377a9c350bcc1556694b924b858f329c44ee64b884ef00d" + }, + { + "input_len": 1024, + "hash": "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af71cf8107265ecdaf8505b95d8fcec83a98a6a96ea5109d2c179c47a387ffbb404756f6eeae7883b446b70ebb144527c2075ab8ab204c0086bb22b7c93d465efc57f8d917f0b385c6df265e77003b85102967486ed57db5c5ca170ba441427ed9afa684e", + "keyed_hash": "75c46f6f3d9eb4f55ecaaee480db732e6c2105546f1e675003687c31719c7ba4a78bc838c72852d4f49c864acb7adafe2478e824afe51c8919d06168414c265f298a8094b1ad813a9b8614acabac321f24ce61c5a5346eb519520d38ecc43e89b5000236df0597243e4d2493fd626730e2ba17ac4d8824d09d1a4a8f57b8227778e2de", + "derive_key": "7356cd7720d5b66b6d0697eb3177d9f8d73a4a5c5e968896eb6a6896843027066c23b601d3ddfb391e90d5c8eccdef4ae2a264bce9e612ba15e2bc9d654af1481b2e75dbabe615974f1070bba84d56853265a34330b4766f8e75edd1f4a1650476c10802f22b64bd3919d246ba20a17558bc51c199efdec67e80a227251808d8ce5bad" + }, + { + "input_len": 1025, + "hash": "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444f4c4a22b4b399155358a994e52bf255de60035742ec71bd08ac275a1b51cc6bfe332b0ef84b409108cda080e6269ed4b3e2c3f7d722aa4cdc98d16deb554e5627be8f955c98e1d5f9565a9194cad0c4285f93700062d9595adb992ae68ff12800ab67a", + "keyed_hash": "357dc55de0c7e382c900fd6e320acc04146be01db6a8ce7210b7189bd664ea69362396b77fdc0d2634a552970843722066c3c15902ae5097e00ff53f1e116f1cd5352720113a837ab2452cafbde4d54085d9cf5d21ca613071551b25d52e69d6c81123872b6f19cd3bc1333edf0c52b94de23ba772cf82636cff4542540a7738d5b930", + "derive_key": "effaa245f065fbf82ac186839a249707c3bddf6d3fdda22d1b95a3c970379bcb5d31013a167509e9066273ab6e2123bc835b408b067d88f96addb550d96b6852dad38e320b9d940f86db74d398c770f462118b35d2724efa13da97194491d96dd37c3c09cbef665953f2ee85ec83d88b88d11547a6f911c8217cca46defa2751e7f3ad" + }, + { + "input_len": 2048, + "hash": "e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a9a60bf80001410ec9eea6698cd537939fad4749edd484cb541aced55cd9bf54764d063f23f6f1e32e12958ba5cfeb1bf618ad094266d4fc3c968c2088f677454c288c67ba0dba337b9d91c7e1ba586dc9a5bc2d5e90c14f53a8863ac75655461cea8f9", + "keyed_hash": "879cf1fa2ea0e79126cb1063617a05b6ad9d0b696d0d757cf053439f60a99dd10173b961cd574288194b23ece278c330fbb8585485e74967f31352a8183aa782b2b22f26cdcadb61eed1a5bc144b8198fbb0c13abbf8e3192c145d0a5c21633b0ef86054f42809df823389ee40811a5910dcbd1018af31c3b43aa55201ed4edaac74fe", + "derive_key": "7b2945cb4fef70885cc5d78a87bf6f6207dd901ff239201351ffac04e1088a23e2c11a1ebffcea4d80447867b61badb1383d842d4e79645d48dd82ccba290769caa7af8eaa1bd78a2a5e6e94fbdab78d9c7b74e894879f6a515257ccf6f95056f4e25390f24f6b35ffbb74b766202569b1d797f2d4bd9d17524c720107f985f4ddc583" + }, + { + "input_len": 2049, + "hash": "5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b687952256303096de31d71d74103403822a2e0bc1eb193e7aecc9643a76b7bbc0c9f9c52e8783aae98764ca468962b5c2ec92f0c74eb5448d519713e09413719431c802f948dd5d90425a4ecdadece9eb178d80f26efccae630734dff63340285adec2aed3b51073ad3", + "keyed_hash": "9f29700902f7c86e514ddc4df1e3049f258b2472b6dd5267f61bf13983b78dd5f9a88abfefdfa1e00b418971f2b39c64ca621e8eb37fceac57fd0c8fc8e117d43b81447be22d5d8186f8f5919ba6bcc6846bd7d50726c06d245672c2ad4f61702c646499ee1173daa061ffe15bf45a631e2946d616a4c345822f1151284712f76b2b0e", + "derive_key": "2ea477c5515cc3dd606512ee72bb3e0e758cfae7232826f35fb98ca1bcbdf27316d8e9e79081a80b046b60f6a263616f33ca464bd78d79fa18200d06c7fc9bffd808cc4755277a7d5e09da0f29ed150f6537ea9bed946227ff184cc66a72a5f8c1e4bd8b04e81cf40fe6dc4427ad5678311a61f4ffc39d195589bdbc670f63ae70f4b6" + }, + { + "input_len": 3072, + "hash": "b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd29a3f6b0b978d6608335c09dc94ccf682f9951cdfc501bfe47b9c9189a6fc7b404d120258506341a6d802857322fbd20d3e5dae05b95c88793fa83db1cb08e7d8008d1599b6209d78336e24839724c191b2a52a80448306e0daa84a3fdb566661a37e11", + "keyed_hash": "044a0e7b172a312dc02a4c9a818c036ffa2776368d7f528268d2e6b5df19177022f302d0529e4174cc507c463671217975e81dab02b8fdeb0d7ccc7568dd22574c783a76be215441b32e91b9a904be8ea81f7a0afd14bad8ee7c8efc305ace5d3dd61b996febe8da4f56ca0919359a7533216e2999fc87ff7d8f176fbecb3d6f34278b", + "derive_key": "050df97f8c2ead654d9bb3ab8c9178edcd902a32f8495949feadcc1e0480c46b3604131bbd6e3ba573b6dd682fa0a63e5b165d39fc43a625d00207607a2bfeb65ff1d29292152e26b298868e3b87be95d6458f6f2ce6118437b632415abe6ad522874bcd79e4030a5e7bad2efa90a7a7c67e93f0a18fb28369d0a9329ab5c24134ccb0" + }, + { + "input_len": 3073, + "hash": "7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd39a27ae3b79d68d89da9bf25bc27139ae65a324918a5f9b7828181e52cf373c84f35b639b7fccbb985b6f2fa56aea0c18f531203497b8bbd3a07ceb5926f1cab74d14bd66486d9a91eba99059a98bd1cd25876b2af5a76c3e9eed554ed72ea952b603bf", + "keyed_hash": "68dede9bef00ba89e43f31a6825f4cf433389fedae75c04ee9f0cf16a427c95a96d6da3fe985054d3478865be9a092250839a697bbda74e279e8a9e69f0025e4cfddd6cfb434b1cd9543aaf97c635d1b451a4386041e4bb100f5e45407cbbc24fa53ea2de3536ccb329e4eb9466ec37093a42cf62b82903c696a93a50b702c80f3c3c5", + "derive_key": "72613c9ec9ff7e40f8f5c173784c532ad852e827dba2bf85b2ab4b76f7079081576288e552647a9d86481c2cae75c2dd4e7c5195fb9ada1ef50e9c5098c249d743929191441301c69e1f48505a4305ec1778450ee48b8e69dc23a25960fe33070ea549119599760a8a2d28aeca06b8c5e9ba58bc19e11fe57b6ee98aa44b2a8e6b14a5" + }, + { + "input_len": 4096, + "hash": "015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e9690289e9409ddb1b99768eafe1623da896faf7e1114bebeadc1be30829b6f8af707d85c298f4f0ff4d9438aef948335612ae921e76d411c3a9111df62d27eaf871959ae0062b5492a0feb98ef3ed4af277f5395172dbe5c311918ea0074ce0036454f620", + "keyed_hash": "befc660aea2f1718884cd8deb9902811d332f4fc4a38cf7c7300d597a081bfc0bbb64a36edb564e01e4b4aaf3b060092a6b838bea44afebd2deb8298fa562b7b597c757b9df4c911c3ca462e2ac89e9a787357aaf74c3b56d5c07bc93ce899568a3eb17d9250c20f6c5f6c1e792ec9a2dcb715398d5a6ec6d5c54f586a00403a1af1de", + "derive_key": "1e0d7f3db8c414c97c6307cbda6cd27ac3b030949da8e23be1a1a924ad2f25b9d78038f7b198596c6cc4a9ccf93223c08722d684f240ff6569075ed81591fd93f9fff1110b3a75bc67e426012e5588959cc5a4c192173a03c00731cf84544f65a2fb9378989f72e9694a6a394a8a30997c2e67f95a504e631cd2c5f55246024761b245" + }, + { + "input_len": 4097, + "hash": "9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb99505f91b0b5600a11251652eacfa9497b31cd3c409ce2e45cfe6c0a016967316c426bd26f619eab5d70af9a418b845c608840390f361630bd497b1ab44019316357c61dbe091ce72fc16dc340ac3d6e009e050b3adac4b5b2c92e722cffdc46501531956", + "keyed_hash": "00df940cd36bb9fa7cbbc3556744e0dbc8191401afe70520ba292ee3ca80abbc606db4976cfdd266ae0abf667d9481831ff12e0caa268e7d3e57260c0824115a54ce595ccc897786d9dcbf495599cfd90157186a46ec800a6763f1c59e36197e9939e900809f7077c102f888caaf864b253bc41eea812656d46742e4ea42769f89b83f", + "derive_key": "aca51029626b55fda7117b42a7c211f8c6e9ba4fe5b7a8ca922f34299500ead8a897f66a400fed9198fd61dd2d58d382458e64e100128075fc54b860934e8de2e84170734b06e1d212a117100820dbc48292d148afa50567b8b84b1ec336ae10d40c8c975a624996e12de31abbe135d9d159375739c333798a80c64ae895e51e22f3ad" + }, + { + "input_len": 5120, + "hash": "9cadc15fed8b5d854562b26a9536d9707cadeda9b143978f319ab34230535833acc61c8fdc114a2010ce8038c853e121e1544985133fccdd0a2d507e8e615e611e9a0ba4f47915f49e53d721816a9198e8b30f12d20ec3689989175f1bf7a300eee0d9321fad8da232ece6efb8e9fd81b42ad161f6b9550a069e66b11b40487a5f5059", + "keyed_hash": "2c493e48e9b9bf31e0553a22b23503c0a3388f035cece68eb438d22fa1943e209b4dc9209cd80ce7c1f7c9a744658e7e288465717ae6e56d5463d4f80cdb2ef56495f6a4f5487f69749af0c34c2cdfa857f3056bf8d807336a14d7b89bf62bef2fb54f9af6a546f818dc1e98b9e07f8a5834da50fa28fb5874af91bf06020d1bf0120e", + "derive_key": "7a7acac8a02adcf3038d74cdd1d34527de8a0fcc0ee3399d1262397ce5817f6055d0cefd84d9d57fe792d65a278fd20384ac6c30fdb340092f1a74a92ace99c482b28f0fc0ef3b923e56ade20c6dba47e49227166251337d80a037e987ad3a7f728b5ab6dfafd6e2ab1bd583a95d9c895ba9c2422c24ea0f62961f0dca45cad47bfa0d" + }, + { + "input_len": 5121, + "hash": "628bd2cb2004694adaab7bbd778a25df25c47b9d4155a55f8fbd79f2fe154cff96adaab0613a6146cdaabe498c3a94e529d3fc1da2bd08edf54ed64d40dcd6777647eac51d8277d70219a9694334a68bc8f0f23e20b0ff70ada6f844542dfa32cd4204ca1846ef76d811cdb296f65e260227f477aa7aa008bac878f72257484f2b6c95", + "keyed_hash": "6ccf1c34753e7a044db80798ecd0782a8f76f33563accaddbfbb2e0ea4b2d0240d07e63f13667a8d1490e5e04f13eb617aea16a8c8a5aaed1ef6fbde1b0515e3c81050b361af6ead126032998290b563e3caddeaebfab592e155f2e161fb7cba939092133f23f9e65245e58ec23457b78a2e8a125588aad6e07d7f11a85b88d375b72d", + "derive_key": "b07f01e518e702f7ccb44a267e9e112d403a7b3f4883a47ffbed4b48339b3c341a0add0ac032ab5aaea1e4e5b004707ec5681ae0fcbe3796974c0b1cf31a194740c14519273eedaabec832e8a784b6e7cfc2c5952677e6c3f2c3914454082d7eb1ce1766ac7d75a4d3001fc89544dd46b5147382240d689bbbaefc359fb6ae30263165" + }, + { + "input_len": 6144, + "hash": "3e2e5b74e048f3add6d21faab3f83aa44d3b2278afb83b80b3c35164ebeca2054d742022da6fdda444ebc384b04a54c3ac5839b49da7d39f6d8a9db03deab32aade156c1c0311e9b3435cde0ddba0dce7b26a376cad121294b689193508dd63151603c6ddb866ad16c2ee41585d1633a2cea093bea714f4c5d6b903522045b20395c83", + "keyed_hash": "3d6b6d21281d0ade5b2b016ae4034c5dec10ca7e475f90f76eac7138e9bc8f1dc35754060091dc5caf3efabe0603c60f45e415bb3407db67e6beb3d11cf8e4f7907561f05dace0c15807f4b5f389c841eb114d81a82c02a00b57206b1d11fa6e803486b048a5ce87105a686dee041207e095323dfe172df73deb8c9532066d88f9da7e", + "derive_key": "2a95beae63ddce523762355cf4b9c1d8f131465780a391286a5d01abb5683a1597099e3c6488aab6c48f3c15dbe1942d21dbcdc12115d19a8b8465fb54e9053323a9178e4275647f1a9927f6439e52b7031a0b465c861a3fc531527f7758b2b888cf2f20582e9e2c593709c0a44f9c6e0f8b963994882ea4168827823eef1f64169fef" + }, + { + "input_len": 6145, + "hash": "f1323a8631446cc50536a9f705ee5cb619424d46887f3c376c695b70e0f0507f18a2cfdd73c6e39dd75ce7c1c6e3ef238fd54465f053b25d21044ccb2093beb015015532b108313b5829c3621ce324b8e14229091b7c93f32db2e4e63126a377d2a63a3597997d4f1cba59309cb4af240ba70cebff9a23d5e3ff0cdae2cfd54e070022", + "keyed_hash": "9ac301e9e39e45e3250a7e3b3df701aa0fb6889fbd80eeecf28dbc6300fbc539f3c184ca2f59780e27a576c1d1fb9772e99fd17881d02ac7dfd39675aca918453283ed8c3169085ef4a466b91c1649cc341dfdee60e32231fc34c9c4e0b9a2ba87ca8f372589c744c15fd6f985eec15e98136f25beeb4b13c4e43dc84abcc79cd4646c", + "derive_key": "379bcc61d0051dd489f686c13de00d5b14c505245103dc040d9e4dd1facab8e5114493d029bdbd295aaa744a59e31f35c7f52dba9c3642f773dd0b4262a9980a2aef811697e1305d37ba9d8b6d850ef07fe41108993180cf779aeece363704c76483458603bbeeb693cffbbe5588d1f3535dcad888893e53d977424bb707201569a8d2" + }, + { + "input_len": 7168, + "hash": "61da957ec2499a95d6b8023e2b0e604ec7f6b50e80a9678b89d2628e99ada77a5707c321c83361793b9af62a40f43b523df1c8633cecb4cd14d00bdc79c78fca5165b863893f6d38b02ff7236c5a9a8ad2dba87d24c547cab046c29fc5bc1ed142e1de4763613bb162a5a538e6ef05ed05199d751f9eb58d332791b8d73fb74e4fce95", + "keyed_hash": "b42835e40e9d4a7f42ad8cc04f85a963a76e18198377ed84adddeaecacc6f3fca2f01d5277d69bb681c70fa8d36094f73ec06e452c80d2ff2257ed82e7ba348400989a65ee8daa7094ae0933e3d2210ac6395c4af24f91c2b590ef87d7788d7066ea3eaebca4c08a4f14b9a27644f99084c3543711b64a070b94f2c9d1d8a90d035d52", + "derive_key": "11c37a112765370c94a51415d0d651190c288566e295d505defdad895dae223730d5a5175a38841693020669c7638f40b9bc1f9f39cf98bda7a5b54ae24218a800a2116b34665aa95d846d97ea988bfcb53dd9c055d588fa21ba78996776ea6c40bc428b53c62b5f3ccf200f647a5aae8067f0ea1976391fcc72af1945100e2a6dcb88" + }, + { + "input_len": 7169, + "hash": "a003fc7a51754a9b3c7fae0367ab3d782dccf28855a03d435f8cfe74605e781798a8b20534be1ca9eb2ae2df3fae2ea60e48c6fb0b850b1385b5de0fe460dbe9d9f9b0d8db4435da75c601156df9d047f4ede008732eb17adc05d96180f8a73548522840779e6062d643b79478a6e8dbce68927f36ebf676ffa7d72d5f68f050b119c8", + "keyed_hash": "ed9b1a922c046fdb3d423ae34e143b05ca1bf28b710432857bf738bcedbfa5113c9e28d72fcbfc020814ce3f5d4fc867f01c8f5b6caf305b3ea8a8ba2da3ab69fabcb438f19ff11f5378ad4484d75c478de425fb8e6ee809b54eec9bdb184315dc856617c09f5340451bf42fd3270a7b0b6566169f242e533777604c118a6358250f54", + "derive_key": "554b0a5efea9ef183f2f9b931b7497995d9eb26f5c5c6dad2b97d62fc5ac31d99b20652c016d88ba2a611bbd761668d5eda3e568e940faae24b0d9991c3bd25a65f770b89fdcadabcb3d1a9c1cb63e69721cacf1ae69fefdcef1e3ef41bc5312ccc17222199e47a26552c6adc460cf47a72319cb5039369d0060eaea59d6c65130f1dd" + }, + { + "input_len": 8192, + "hash": "aae792484c8efe4f19e2ca7d371d8c467ffb10748d8a5a1ae579948f718a2a635fe51a27db045a567c1ad51be5aa34c01c6651c4d9b5b5ac5d0fd58cf18dd61a47778566b797a8c67df7b1d60b97b19288d2d877bb2df417ace009dcb0241ca1257d62712b6a4043b4ff33f690d849da91ea3bf711ed583cb7b7a7da2839ba71309bbf", + "keyed_hash": "dc9637c8845a770b4cbf76b8daec0eebf7dc2eac11498517f08d44c8fc00d58a4834464159dcbc12a0ba0c6d6eb41bac0ed6585cabfe0aca36a375e6c5480c22afdc40785c170f5a6b8a1107dbee282318d00d915ac9ed1143ad40765ec120042ee121cd2baa36250c618adaf9e27260fda2f94dea8fb6f08c04f8f10c78292aa46102", + "derive_key": "ad01d7ae4ad059b0d33baa3c01319dcf8088094d0359e5fd45d6aeaa8b2d0c3d4c9e58958553513b67f84f8eac653aeeb02ae1d5672dcecf91cd9985a0e67f4501910ecba25555395427ccc7241d70dc21c190e2aadee875e5aae6bf1912837e53411dabf7a56cbf8e4fb780432b0d7fe6cec45024a0788cf5874616407757e9e6bef7" + }, + { + "input_len": 8193, + "hash": "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3bb2282aa69be089359ea1154b9a9286c4a56af4de975a9aa4a5c497654914d279bea60bb6d2cf7225a2fa0ff5ef56bbe4b149f3ed15860f78b4e2ad04e158e375c1e0c0b551cd7dfc82f1b155c11b6b3ed51ec9edb30d133653bb5709d1dbd55f4e1ff6", + "keyed_hash": "954a2a75420c8d6547e3ba5b98d963e6fa6491addc8c023189cc519821b4a1f5f03228648fd983aef045c2fa8290934b0866b615f585149587dda2299039965328835a2b18f1d63b7e300fc76ff260b571839fe44876a4eae66cbac8c67694411ed7e09df51068a22c6e67d6d3dd2cca8ff12e3275384006c80f4db68023f24eebba57", + "derive_key": "af1e0346e389b17c23200270a64aa4e1ead98c61695d917de7d5b00491c9b0f12f20a01d6d622edf3de026a4db4e4526225debb93c1237934d71c7340bb5916158cbdafe9ac3225476b6ab57a12357db3abbad7a26c6e66290e44034fb08a20a8d0ec264f309994d2810c49cfba6989d7abb095897459f5425adb48aba07c5fb3c83c0" + }, + { + "input_len": 16384, + "hash": "f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde49d764c270176e53e97bdffa58d549073f2c660be0e81293767ed4e4929f9ad34bbb39a529334c57c4a381ffd2a6d4bfdbf1482651b172aa883cc13408fa67758a3e47503f93f87720a3177325f7823251b85275f64636a8f1d599c2e49722f42e93893", + "keyed_hash": "9e9fc4eb7cf081ea7c47d1807790ed211bfec56aa25bb7037784c13c4b707b0df9e601b101e4cf63a404dfe50f2e1865bb12edc8fca166579ce0c70dba5a5c0fc960ad6f3772183416a00bd29d4c6e651ea7620bb100c9449858bf14e1ddc9ecd35725581ca5b9160de04060045993d972571c3e8f71e9d0496bfa744656861b169d65", + "derive_key": "160e18b5878cd0df1c3af85eb25a0db5344d43a6fbd7a8ef4ed98d0714c3f7e160dc0b1f09caa35f2f417b9ef309dfe5ebd67f4c9507995a531374d099cf8ae317542e885ec6f589378864d3ea98716b3bbb65ef4ab5e0ab5bb298a501f19a41ec19af84a5e6b428ecd813b1a47ed91c9657c3fba11c406bc316768b58f6802c9e9b57" + }, + { + "input_len": 31744, + "hash": "62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47860cc51f2b0c28a7b77304bd55fe73af663c02d3f52ea053ba43431ca5bab7bfea2f5e9d7121770d88f70ae9649ea713087d1914f7f312147e247f87eb2d4ffef0ac978bf7b6579d57d533355aa20b8b77b13fd09748728a5cc327a8ec470f4013226f", + "keyed_hash": "efa53b389ab67c593dba624d898d0f7353ab99e4ac9d42302ee64cbf9939a4193a7258db2d9cd32a7a3ecfce46144114b15c2fcb68a618a976bd74515d47be08b628be420b5e830fade7c080e351a076fbc38641ad80c736c8a18fe3c66ce12f95c61c2462a9770d60d0f77115bbcd3782b593016a4e728d4c06cee4505cb0c08a42ec", + "derive_key": "39772aef80e0ebe60596361e45b061e8f417429d529171b6764468c22928e28e9759adeb797a3fbf771b1bcea30150a020e317982bf0d6e7d14dd9f064bc11025c25f31e81bd78a921db0174f03dd481d30e93fd8e90f8b2fee209f849f2d2a52f31719a490fb0ba7aea1e09814ee912eba111a9fde9d5c274185f7bae8ba85d300a2b" + }, + { + "input_len": 102400, + "hash": "bc3e3d41a1146b069abffad3c0d44860cf664390afce4d9661f7902e7943e085e01c59dab908c04c3342b816941a26d69c2605ebee5ec5291cc55e15b76146e6745f0601156c3596cb75065a9c57f35585a52e1ac70f69131c23d611ce11ee4ab1ec2c009012d236648e77be9295dd0426f29b764d65de58eb7d01dd42248204f45f8e", + "keyed_hash": "1c35d1a5811083fd7119f5d5d1ba027b4d01c0c6c49fb6ff2cf75393ea5db4a7f9dbdd3e1d81dcbca3ba241bb18760f207710b751846faaeb9dff8262710999a59b2aa1aca298a032d94eacfadf1aa192418eb54808db23b56e34213266aa08499a16b354f018fc4967d05f8b9d2ad87a7278337be9693fc638a3bfdbe314574ee6fc4", + "derive_key": "4652cff7a3f385a6103b5c260fc1593e13c778dbe608efb092fe7ee69df6e9c6d83a3e041bc3a48df2879f4a0a3ed40e7c961c73eff740f3117a0504c2dff4786d44fb17f1549eb0ba585e40ec29bf7732f0b7e286ff8acddc4cb1e23b87ff5d824a986458dcc6a04ac83969b80637562953df51ed1a7e90a7926924d2763778be8560" + } + ] + } \ No newline at end of file diff --git a/g16ckt/src/gadgets/hash/mod.rs b/g16ckt/src/gadgets/hash/mod.rs new file mode 100644 index 0000000..6bc7d82 --- /dev/null +++ b/g16ckt/src/gadgets/hash/mod.rs @@ -0,0 +1 @@ +pub mod blake3; diff --git a/g16ckt/src/gadgets/mod.rs b/g16ckt/src/gadgets/mod.rs index 6eceb96..ab99882 100644 --- a/g16ckt/src/gadgets/mod.rs +++ b/g16ckt/src/gadgets/mod.rs @@ -2,6 +2,7 @@ pub mod basic; pub mod bigint; pub mod bn254; pub mod groth16; +pub mod hash; pub use groth16::{groth16_verify, groth16_verify_compressed}; 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!(