From f2eacb1f7fdbb760213cf8037a1bd1a10672133f Mon Sep 17 00:00:00 2001 From: Flying Nobita <46126470+flyingnobita@users.noreply.github.com> Date: Fri, 11 Aug 2023 05:42:35 +0800 Subject: [PATCH] feat: Add BLS signature verification for BN254 (#89) * feat: Add BLS signature verification for BN254 * Use load_private() and sum() as suggested * Corrected as per request * Update bls_signature_verify() to return AssignedValue * Eliminate unnecessary fp_chip. Shortened result comparison * Use forloop instead of iterator for affine to avoid dependence on halo2_proofs * Add test to Github CI --- .github/workflows/ci.yml | 1 + .../configs/bn254/bench_bls_signature.config | 15 ++ .../bn254/bls_signature_circuit.config | 1 + halo2-ecc/src/bn254/bls_signature.rs | 79 ++++++ halo2-ecc/src/bn254/mod.rs | 1 + halo2-ecc/src/bn254/pairing.rs | 16 +- halo2-ecc/src/bn254/tests/bls_signature.rs | 245 ++++++++++++++++++ halo2-ecc/src/bn254/tests/mod.rs | 2 + halo2-ecc/src/bn254/tests/pairing.rs | 12 +- 9 files changed, 354 insertions(+), 18 deletions(-) create mode 100644 halo2-ecc/configs/bn254/bench_bls_signature.config create mode 100644 halo2-ecc/configs/bn254/bls_signature_circuit.config create mode 100644 halo2-ecc/src/bn254/bls_signature.rs create mode 100644 halo2-ecc/src/bn254/tests/bls_signature.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a8c2db3..8a3eb1f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: cargo test -- test_sm cargo test -- test_fb cargo test -- test_pairing + cargo test -- test_bls_signature - name: Run halo2-ecc tests real prover working-directory: 'halo2-ecc' run: | diff --git a/halo2-ecc/configs/bn254/bench_bls_signature.config b/halo2-ecc/configs/bn254/bench_bls_signature.config new file mode 100644 index 00000000..694ee974 --- /dev/null +++ b/halo2-ecc/configs/bn254/bench_bls_signature.config @@ -0,0 +1,15 @@ +{"strategy":"Simple","degree":14,"num_advice":211,"num_lookup_advice":27,"num_fixed":1,"lookup_bits":13,"limb_bits":91,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":15,"num_advice":105,"num_lookup_advice":14,"num_fixed":1,"lookup_bits":14,"limb_bits":90,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":16,"num_advice":50,"num_lookup_advice":6,"num_fixed":1,"lookup_bits":15,"limb_bits":90,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":17,"num_advice":25,"num_lookup_advice":3,"num_fixed":1,"lookup_bits":16,"limb_bits":88,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":18,"num_advice":13,"num_lookup_advice":2,"num_fixed":1,"lookup_bits":17,"limb_bits":88,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":19,"num_advice":6,"num_lookup_advice":1,"num_fixed":1,"lookup_bits":18,"limb_bits":90,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":20,"num_advice":3,"num_lookup_advice":1,"num_fixed":1,"lookup_bits":19,"limb_bits":88,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":21,"num_advice":2,"num_lookup_advice":1,"num_fixed":1,"lookup_bits":20,"limb_bits":88,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":22,"num_advice":1,"num_lookup_advice":1,"num_fixed":1,"lookup_bits":21,"limb_bits":88,"num_limbs":3,"num_aggregation":2} +{"strategy":"Simple","degree":17,"num_advice":25,"num_lookup_advice":3,"num_fixed":1,"lookup_bits":16,"limb_bits":88,"num_limbs":3,"num_aggregation":20} +{"strategy":"Simple","degree":17,"num_advice":25,"num_lookup_advice":3,"num_fixed":1,"lookup_bits":16,"limb_bits":88,"num_limbs":3,"num_aggregation":200} +{"strategy":"Simple","degree":17,"num_advice":25,"num_lookup_advice":3,"num_fixed":1,"lookup_bits":16,"limb_bits":88,"num_limbs":3,"num_aggregation":2000} +{"strategy":"Simple","degree":17,"num_advice":25,"num_lookup_advice":3,"num_fixed":1,"lookup_bits":16,"limb_bits":88,"num_limbs":3,"num_aggregation":20000} +{"strategy":"Simple","degree":17,"num_advice":25,"num_lookup_advice":3,"num_fixed":1,"lookup_bits":16,"limb_bits":88,"num_limbs":3,"num_aggregation":40000} +{"strategy":"Simple","degree":17,"num_advice":25,"num_lookup_advice":3,"num_fixed":1,"lookup_bits":16,"limb_bits":88,"num_limbs":3,"num_aggregation":80000} \ No newline at end of file diff --git a/halo2-ecc/configs/bn254/bls_signature_circuit.config b/halo2-ecc/configs/bn254/bls_signature_circuit.config new file mode 100644 index 00000000..1cfb3d21 --- /dev/null +++ b/halo2-ecc/configs/bn254/bls_signature_circuit.config @@ -0,0 +1 @@ +{"strategy":"Simple","degree":19,"num_advice":6,"num_lookup_advice":1,"num_fixed":1,"lookup_bits":18,"limb_bits":90,"num_limbs":3,"num_aggregation":30} diff --git a/halo2-ecc/src/bn254/bls_signature.rs b/halo2-ecc/src/bn254/bls_signature.rs new file mode 100644 index 00000000..0e10a090 --- /dev/null +++ b/halo2-ecc/src/bn254/bls_signature.rs @@ -0,0 +1,79 @@ +#![allow(non_snake_case)] + +use super::pairing::PairingChip; +use super::{Fp12Chip, Fp2Chip, FpChip}; +use crate::ecc::EccChip; +use crate::fields::FieldChip; +use crate::fields::PrimeField; +use crate::halo2_proofs::halo2curves::bn256::Fq12; +use crate::halo2_proofs::halo2curves::bn256::{G1Affine, G2Affine}; +use halo2_base::{AssignedValue, Context}; + +// To avoid issues with mutably borrowing twice (not allowed in Rust), we only store fp_chip and construct g2_chip and fp12_chip in scope when needed for temporary mutable borrows +pub struct BlsSignatureChip<'chip, F: PrimeField> { + pub fp_chip: &'chip FpChip<'chip, F>, + pub pairing_chip: &'chip PairingChip<'chip, F>, +} + +impl<'chip, F: PrimeField> BlsSignatureChip<'chip, F> { + pub fn new(fp_chip: &'chip FpChip, pairing_chip: &'chip PairingChip) -> Self { + Self { fp_chip, pairing_chip } + } + + // Verifies that e(g1, signature) = e(pubkey, H(m)) by checking e(g1, signature)*e(pubkey, -H(m)) === 1 + // where e(,) is optimal Ate pairing + // G1: {g1, pubkey}, G2: {signature, message} + // TODO add support for aggregating signatures over different messages + pub fn bls_signature_verify( + &self, + ctx: &mut Context, + g1: G1Affine, + signatures: &[G2Affine], + pubkeys: &[G1Affine], + msghash: G2Affine, + ) -> AssignedValue { + assert!( + signatures.len() == pubkeys.len(), + "signatures and pubkeys must be the same length" + ); + assert!(!signatures.is_empty(), "signatures must not be empty"); + assert!(!pubkeys.is_empty(), "pubkeys must not be empty"); + + let g1_chip = EccChip::new(self.fp_chip); + let fp2_chip = Fp2Chip::::new(self.fp_chip); + let g2_chip = EccChip::new(&fp2_chip); + + let g1_assigned = self.pairing_chip.load_private_g1(ctx, g1); + + let hash_m_assigned = self.pairing_chip.load_private_g2(ctx, msghash); + + let signature_points = signatures + .iter() + .map(|pt| g2_chip.load_private::(ctx, (pt.x, pt.y))) + .collect::>(); + let signature_agg_assigned = g2_chip.sum::(ctx, signature_points); + + let pubkey_points = pubkeys + .iter() + .map(|pt| g1_chip.load_private::(ctx, (pt.x, pt.y))) + .collect::>(); + let pubkey_agg_assigned = g1_chip.sum::(ctx, pubkey_points); + + let fp12_chip = Fp12Chip::::new(self.fp_chip); + let g12_chip = EccChip::new(&fp12_chip); + let neg_signature_assigned_g12 = g12_chip.negate(ctx, &signature_agg_assigned); + + let multi_paired = self.pairing_chip.multi_miller_loop( + ctx, + vec![ + (&g1_assigned, &neg_signature_assigned_g12), + (&pubkey_agg_assigned, &hash_m_assigned), + ], + ); + let result = fp12_chip.final_exp(ctx, multi_paired); + + // Check signatures are verified + let fp12_one = fp12_chip.load_constant(ctx, Fq12::one()); + fp12_chip.is_equal(ctx, result, fp12_one) + } +} diff --git a/halo2-ecc/src/bn254/mod.rs b/halo2-ecc/src/bn254/mod.rs index deed3c4d..ab0a4ee3 100644 --- a/halo2-ecc/src/bn254/mod.rs +++ b/halo2-ecc/src/bn254/mod.rs @@ -3,6 +3,7 @@ use crate::fields::vector::FieldVector; use crate::fields::{fp, fp12, fp2}; use crate::halo2_proofs::halo2curves::bn256::{Fq, Fq12, Fq2}; +pub mod bls_signature; pub mod final_exp; pub mod pairing; diff --git a/halo2-ecc/src/bn254/pairing.rs b/halo2-ecc/src/bn254/pairing.rs index 5a1cc19f..886985d4 100644 --- a/halo2-ecc/src/bn254/pairing.rs +++ b/halo2-ecc/src/bn254/pairing.rs @@ -453,23 +453,15 @@ impl<'chip, F: PrimeField> PairingChip<'chip, F> { Self { fp_chip } } - pub fn load_private_g1_unchecked( - &self, - ctx: &mut Context, - point: G1Affine, - ) -> EcPoint> { + pub fn load_private_g1(&self, ctx: &mut Context, point: G1Affine) -> EcPoint> { let g1_chip = EccChip::new(self.fp_chip); - g1_chip.load_private_unchecked(ctx, (point.x, point.y)) + g1_chip.load_private::(ctx, (point.x, point.y)) } - pub fn load_private_g2_unchecked( - &self, - ctx: &mut Context, - point: G2Affine, - ) -> EcPoint> { + pub fn load_private_g2(&self, ctx: &mut Context, point: G2Affine) -> EcPoint> { let fp2_chip = Fp2Chip::new(self.fp_chip); let g2_chip = EccChip::new(&fp2_chip); - g2_chip.load_private_unchecked(ctx, (point.x, point.y)) + g2_chip.load_private::(ctx, (point.x, point.y)) } pub fn miller_loop( diff --git a/halo2-ecc/src/bn254/tests/bls_signature.rs b/halo2-ecc/src/bn254/tests/bls_signature.rs new file mode 100644 index 00000000..115ab8ed --- /dev/null +++ b/halo2-ecc/src/bn254/tests/bls_signature.rs @@ -0,0 +1,245 @@ +use std::{ + fs::{self, File}, + io::{BufRead, BufReader}, +}; + +use super::*; +use crate::{fields::FpStrategy, halo2_proofs::halo2curves::bn256::G2Affine}; +use halo2_base::{ + gates::{ + builder::{ + CircuitBuilderStage, GateThreadBuilder, MultiPhaseThreadBreakPoints, + RangeCircuitBuilder, + }, + RangeChip, + }, + halo2_proofs::{ + halo2curves::{ + bn256::{multi_miller_loop, G2Prepared, Gt}, + pairing::MillerLoopResult, + }, + poly::kzg::multiopen::{ProverGWC, VerifierGWC}, + }, + utils::fs::gen_srs, + Context, +}; +use rand_core::OsRng; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +struct BlsSignatureCircuitParams { + strategy: FpStrategy, + degree: u32, + num_advice: usize, + num_lookup_advice: usize, + num_fixed: usize, + lookup_bits: usize, + limb_bits: usize, + num_limbs: usize, + num_aggregation: u32, +} + +/// Verify e(g1, signature_agg) = e(pubkey_agg, H(m)) +fn bls_signature_test( + ctx: &mut Context, + params: BlsSignatureCircuitParams, + g1: G1Affine, + signatures: &[G2Affine], + pubkeys: &[G1Affine], + msghash: G2Affine, +) { + // Calculate halo2 pairing by multipairing + std::env::set_var("LOOKUP_BITS", params.lookup_bits.to_string()); + let range = RangeChip::::default(params.lookup_bits); + let fp_chip = FpChip::::new(&range, params.limb_bits, params.num_limbs); + let pairing_chip = PairingChip::new(&fp_chip); + let bls_signature_chip = BlsSignatureChip::new(&fp_chip, &pairing_chip); + let result = bls_signature_chip.bls_signature_verify(ctx, g1, signatures, pubkeys, msghash); + + // Calculate non-halo2 pairing by multipairing + let mut signatures_g2: G2Affine = signatures[0]; + for i in 1..signatures.len() { + signatures_g2 = (signatures_g2 + signatures[i]).into(); + } + let signature_g2_prepared = G2Prepared::from(signatures_g2); + + let mut pubkeys_g1: G1Affine = pubkeys[0]; + for i in 1..signatures.len() { + pubkeys_g1 = (pubkeys_g1 + pubkeys[i]).into(); + } + let pubkey_aggregated = pubkeys_g1; + + let hash_m_prepared = G2Prepared::from(-msghash); + let actual_result = + multi_miller_loop(&[(&g1, &signature_g2_prepared), (&pubkey_aggregated, &hash_m_prepared)]) + .final_exponentiation(); + + // Compare the 2 results + assert_eq!(*result.value(), F::from(actual_result == Gt::identity())) +} + +fn random_bls_signature_circuit( + params: BlsSignatureCircuitParams, + stage: CircuitBuilderStage, + break_points: Option, +) -> RangeCircuitBuilder { + let k = params.degree as usize; + let mut builder = match stage { + CircuitBuilderStage::Mock => GateThreadBuilder::mock(), + CircuitBuilderStage::Prover => GateThreadBuilder::prover(), + CircuitBuilderStage::Keygen => GateThreadBuilder::keygen(), + }; + + assert!(params.num_aggregation > 0, "Cannot aggregate 0 signatures!"); + + // TODO: Implement hash_to_curve(msg) for arbitrary message + let msg_hash = G2Affine::random(OsRng); + let g1 = G1Affine::generator(); + + let mut sks: Vec = Vec::new(); + let mut signatures: Vec = Vec::new(); + let mut pubkeys: Vec = Vec::new(); + + for _ in 0..params.num_aggregation { + let sk = Fr::random(OsRng); + let signature = G2Affine::from(msg_hash * sk); + let pubkey = G1Affine::from(G1Affine::generator() * sk); + + sks.push(sk); + signatures.push(signature); + pubkeys.push(pubkey); + } + + let start0 = start_timer!(|| format!("Witness generation for circuit in {stage:?} stage")); + bls_signature_test::(builder.main(0), params, g1, &signatures, &pubkeys, msg_hash); + + let circuit = match stage { + CircuitBuilderStage::Mock => { + builder.config(k, Some(20)); + RangeCircuitBuilder::mock(builder) + } + CircuitBuilderStage::Keygen => { + builder.config(k, Some(20)); + RangeCircuitBuilder::keygen(builder) + } + CircuitBuilderStage::Prover => RangeCircuitBuilder::prover(builder, break_points.unwrap()), + }; + end_timer!(start0); + circuit +} + +#[test] +fn test_bls_signature() { + let run_path = "configs/bn254/bls_signature_circuit.config"; + let path = run_path; + let params: BlsSignatureCircuitParams = serde_json::from_reader( + File::open(path).unwrap_or_else(|e| panic!("{path} does not exist: {e:?}")), + ) + .unwrap(); + println!("num_advice: {num_advice}", num_advice = params.num_advice); + let circuit = random_bls_signature_circuit(params, CircuitBuilderStage::Mock, None); + MockProver::run(params.degree, &circuit, vec![]).unwrap().assert_satisfied(); +} + +#[test] +fn bench_bls_signature() -> Result<(), Box> { + let rng = OsRng; + let config_path = "configs/bn254/bench_bls_signature.config"; + let bench_params_file = + File::open(config_path).unwrap_or_else(|e| panic!("{config_path} does not exist: {e:?}")); + fs::create_dir_all("results/bn254").unwrap(); + fs::create_dir_all("data").unwrap(); + + let results_path = "results/bn254/bls_signature_bench.csv"; + let mut fs_results = File::create(results_path).unwrap(); + writeln!(fs_results, "degree,num_advice,num_lookup,num_fixed,lookup_bits,limb_bits,num_limbs,num_aggregation,proof_time,proof_size,verify_time")?; + + let bench_params_reader = BufReader::new(bench_params_file); + for line in bench_params_reader.lines() { + let bench_params: BlsSignatureCircuitParams = + serde_json::from_str(line.unwrap().as_str()).unwrap(); + let k = bench_params.degree; + println!("---------------------- degree = {k} ------------------------------",); + + let params = gen_srs(k); + let circuit = random_bls_signature_circuit(bench_params, CircuitBuilderStage::Keygen, None); + + let vk_time = start_timer!(|| "Generating vkey"); + let vk = keygen_vk(¶ms, &circuit)?; + end_timer!(vk_time); + + let pk_time = start_timer!(|| "Generating pkey"); + let pk = keygen_pk(¶ms, vk, &circuit)?; + end_timer!(pk_time); + + let break_points = circuit.0.break_points.take(); + drop(circuit); + // create a proof + let proof_time = start_timer!(|| "Proving time"); + let circuit = random_bls_signature_circuit( + bench_params, + CircuitBuilderStage::Prover, + Some(break_points), + ); + let mut transcript = Blake2bWrite::<_, _, Challenge255<_>>::init(vec![]); + create_proof::< + KZGCommitmentScheme, + ProverGWC<'_, Bn256>, + Challenge255, + _, + Blake2bWrite, G1Affine, Challenge255>, + _, + >(¶ms, &pk, &[circuit], &[&[]], rng, &mut transcript)?; + let proof = transcript.finalize(); + end_timer!(proof_time); + + let proof_size = { + let path = format!( + "data/bls_signature_bn254_circuit_proof_{}_{}_{}_{}_{}_{}_{}_{}.data", + bench_params.degree, + bench_params.num_advice, + bench_params.num_lookup_advice, + bench_params.num_fixed, + bench_params.lookup_bits, + bench_params.limb_bits, + bench_params.num_limbs, + bench_params.num_aggregation + ); + let mut fd = File::create(&path)?; + fd.write_all(&proof)?; + let size = fd.metadata().unwrap().len(); + fs::remove_file(path)?; + size + }; + + let verify_time = start_timer!(|| "Verify time"); + let verifier_params = params.verifier_params(); + let strategy = SingleStrategy::new(¶ms); + let mut transcript = Blake2bRead::<_, _, Challenge255<_>>::init(&proof[..]); + verify_proof::< + KZGCommitmentScheme, + VerifierGWC<'_, Bn256>, + Challenge255, + Blake2bRead<&[u8], G1Affine, Challenge255>, + SingleStrategy<'_, Bn256>, + >(verifier_params, pk.get_vk(), strategy, &[&[]], &mut transcript) + .unwrap(); + end_timer!(verify_time); + + writeln!( + fs_results, + "{},{},{},{},{},{},{},{},{:?},{},{:?}", + bench_params.degree, + bench_params.num_advice, + bench_params.num_lookup_advice, + bench_params.num_fixed, + bench_params.lookup_bits, + bench_params.limb_bits, + bench_params.num_limbs, + bench_params.num_aggregation, + proof_time.time.elapsed(), + proof_size, + verify_time.time.elapsed() + )?; + } + Ok(()) +} diff --git a/halo2-ecc/src/bn254/tests/mod.rs b/halo2-ecc/src/bn254/tests/mod.rs index 172300a1..89aea571 100644 --- a/halo2-ecc/src/bn254/tests/mod.rs +++ b/halo2-ecc/src/bn254/tests/mod.rs @@ -1,4 +1,5 @@ #![allow(non_snake_case)] +use super::bls_signature::BlsSignatureChip; use super::pairing::PairingChip; use super::*; use crate::{ecc::EccChip, fields::PrimeField}; @@ -24,6 +25,7 @@ use halo2_base::utils::fe_to_biguint; use serde::{Deserialize, Serialize}; use std::io::Write; +pub mod bls_signature; pub mod ec_add; pub mod fixed_base_msm; pub mod msm; diff --git a/halo2-ecc/src/bn254/tests/pairing.rs b/halo2-ecc/src/bn254/tests/pairing.rs index 2b93c89f..d00330ee 100644 --- a/halo2-ecc/src/bn254/tests/pairing.rs +++ b/halo2-ecc/src/bn254/tests/pairing.rs @@ -43,10 +43,10 @@ fn pairing_check_test( let range = RangeChip::::default(params.lookup_bits); let fp_chip = FpChip::::new(&range, params.limb_bits, params.num_limbs); let chip = PairingChip::new(&fp_chip); - let P_assigned = chip.load_private_g1_unchecked(ctx, P); - let Q_assigned = chip.load_private_g2_unchecked(ctx, Q); - let S_assigned = chip.load_private_g1_unchecked(ctx, S); - let T_assigned = chip.load_private_g2_unchecked(ctx, G2Affine::generator()); + let P_assigned = chip.load_private_g1(ctx, P); + let Q_assigned = chip.load_private_g2(ctx, Q); + let S_assigned = chip.load_private_g1(ctx, S); + let T_assigned = chip.load_private_g2(ctx, G2Affine::generator()); chip.pairing_check(ctx, &Q_assigned, &P_assigned, &T_assigned, &S_assigned); } @@ -60,8 +60,8 @@ fn pairing_test( let range = RangeChip::::default(params.lookup_bits); let fp_chip = FpChip::::new(&range, params.limb_bits, params.num_limbs); let chip = PairingChip::new(&fp_chip); - let P_assigned = chip.load_private_g1_unchecked(ctx, P); - let Q_assigned = chip.load_private_g2_unchecked(ctx, Q); + let P_assigned = chip.load_private_g1(ctx, P); + let Q_assigned = chip.load_private_g2(ctx, Q); // test optimal ate pairing let f = chip.pairing(ctx, &Q_assigned, &P_assigned); let actual_f = pairing(&P, &Q);