From cc1a8b6cdfa3f089063c78cc5d3c661a67b1ff5a Mon Sep 17 00:00:00 2001 From: Timofey Luin Date: Thu, 8 Feb 2024 13:41:51 +0100 Subject: [PATCH 1/4] add subgroup check to Step --- lightclient-circuits/src/sync_step_circuit.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lightclient-circuits/src/sync_step_circuit.rs b/lightclient-circuits/src/sync_step_circuit.rs index 0045719..411edf2 100644 --- a/lightclient-circuits/src/sync_step_circuit.rs +++ b/lightclient-circuits/src/sync_step_circuit.rs @@ -41,6 +41,7 @@ use halo2_ecc::{ use halo2curves::bls12_381::{G1Affine, G2Affine}; use itertools::Itertools; use num_bigint::BigUint; +use snark_verifier::loader::halo2::EccInstructions; use ssz_rs::Merkleized; use std::{env::var, marker::PhantomData, vec}; @@ -84,7 +85,7 @@ impl StepCircuit { .as_slice() .iter() .map(|bytes| { - G1Affine::from_uncompressed_unchecked_be(&bytes.as_slice().try_into().unwrap()) + G1Affine::from_uncompressed_be(&bytes.as_slice().try_into().unwrap()) .unwrap() }) .collect_vec(); @@ -331,7 +332,7 @@ impl StepCircuit { let sig_affine = G2Affine::from_compressed_be(&bytes_compressed.try_into().unwrap()) .expect("correct signature"); - g2_chip.load_private_unchecked(ctx, sig_affine.into_coordinates()) + g2_chip.assign_point(ctx, sig_affine) } /// Takes a list of pubkeys and aggregates them. From d3fefe746dc7a13af25996bfc23adb6466600133 Mon Sep 17 00:00:00 2001 From: Timofey Luin Date: Thu, 22 Feb 2024 17:19:28 +0100 Subject: [PATCH 2/4] add y sign cross-circuit check --- .../src/committee_update_circuit.rs | 47 ++++---- lightclient-circuits/src/poseidon.rs | 104 +++++++++++++----- lightclient-circuits/src/sync_step_circuit.rs | 92 ++++++---------- prover/src/utils.rs | 4 +- test-utils/src/lib.rs | 4 +- 5 files changed, 138 insertions(+), 113 deletions(-) diff --git a/lightclient-circuits/src/committee_update_circuit.rs b/lightclient-circuits/src/committee_update_circuit.rs index e200ce4..c41265e 100644 --- a/lightclient-circuits/src/committee_update_circuit.rs +++ b/lightclient-circuits/src/committee_update_circuit.rs @@ -4,16 +4,17 @@ use crate::{ gadget::crypto::{HashInstructions, Sha256ChipWide, ShaBitGateManager, ShaCircuitBuilder}, - poseidon::{fq_array_poseidon, poseidon_hash_fq_array}, + poseidon::{g1_array_poseidon, poseidon_committee_commitment_from_compressed}, ssz_merkle::{ssz_merkleize_chunks, verify_merkle_proof}, - sync_step_circuit::clear_3_bits, util::{bytes_be_to_u128, AppCircuit, CommonGateManager, Eth2ConfigPinning, IntoWitness}, witness::{self, HashInput, HashInputChunk}, Eth2CircuitBuilder, }; use eth_types::{Field, Spec, LIMB_BITS, NUM_LIMBS}; use halo2_base::{ - gates::{circuit::CircuitBuilderStage, flex_gate::threads::CommonCircuitBuilder}, + gates::{ + circuit::CircuitBuilderStage, flex_gate::threads::CommonCircuitBuilder, GateInstructions, + }, halo2_proofs::{ halo2curves::bn256::{self, Bn256}, plonk::Error, @@ -27,7 +28,6 @@ use halo2_ecc::{ bls12_381::FpChip, fields::FieldChip, }; -use halo2curves::bls12_381; use itertools::Itertools; use ssz_rs::Merkleized; use std::{env::var, iter, marker::PhantomData, vec}; @@ -73,8 +73,9 @@ impl CommitteeUpdateCircuit { Self::sync_committee_root_ssz(builder, &sha256_chip, compressed_encodings.clone())?; let poseidon_commit = { - let pubkeys_x = Self::decode_pubkeys_x(builder.main(), fp_chip, compressed_encodings); - fq_array_poseidon(builder.main(), fp_chip, &pubkeys_x)? + let (pubkeys_x, y_signs_packed) = + Self::decode_pubkeys_x(builder.main(), fp_chip, compressed_encodings); + g1_array_poseidon(builder.main(), fp_chip, pubkeys_x, y_signs_packed)? }; // Finalized header @@ -129,11 +130,10 @@ impl CommitteeUpdateCircuit { ctx: &mut Context, fp_chip: &FpChip<'_, F>, compressed_encodings: impl IntoIterator>>, - ) -> Vec> { - let range = fp_chip.range(); + ) -> (Vec>, Vec>) { let gate = fp_chip.gate(); - compressed_encodings + let (x_bigints, y_signs): (Vec<_>, Vec<_>) = compressed_encodings .into_iter() .map(|mut assigned_bytes| { // following logic is for little endian decoding but input bytes are in BE, therefore we reverse them. @@ -144,20 +144,33 @@ impl CommitteeUpdateCircuit { let masked_byte = &assigned_bytes[48 - 1]; // clear the flag bits from a last byte of compressed pubkey. // we are using [`clear_3_bits`] function which appears to be just as useful here as for public input commitment. - let cleared_byte = clear_3_bits(ctx, range, masked_byte); + let (cleared_byte, y_sign) = { + let bits = gate.num_to_bits(ctx, *masked_byte, 8); + let cleared = gate.bits_to_num(ctx, &bits[..5]); + (cleared, bits[5]) // 3 MSB bits are cleared, 3th bit is a sign bit + }; // Use the cleared byte to construct the x coordinate let assigned_x_bytes_cleared = [&assigned_bytes.as_slice()[..48 - 1], &[cleared_byte]].concat(); - decode_into_bn::( + let x = decode_into_bn::( ctx, gate, assigned_x_bytes_cleared, &fp_chip.limb_bases, fp_chip.limb_bits(), - ) + ); + + (x, y_sign) }) - .collect() + .unzip(); + + let signs_packed = y_signs + .chunks(F::CAPACITY as usize - 1) + .map(|chunk| gate.bits_to_num(ctx, chunk)) + .collect_vec(); + + (x_bigints, signs_packed) } fn sync_committee_root_ssz>( @@ -189,14 +202,8 @@ impl CommitteeUpdateCircuit { where [(); S::SYNC_COMMITTEE_SIZE]:, { - let pubkeys_x = args.pubkeys_compressed.iter().cloned().map(|mut bytes| { - bytes[0] &= 0b00011111; - bls12_381::Fq::from_bytes_be(&bytes.try_into().unwrap()) - .expect("bad bls12_381::Fq encoding") - }); - let poseidon_commitment = - poseidon_hash_fq_array::(pubkeys_x, limb_bits - (limb_bits % 2)); + poseidon_committee_commitment_from_compressed(&args.pubkeys_compressed, limb_bits); let finalized_header_root = args.finalized_header.clone().hash_tree_root().unwrap(); diff --git a/lightclient-circuits/src/poseidon.rs b/lightclient-circuits/src/poseidon.rs index 16082d2..1bc1e8b 100644 --- a/lightclient-circuits/src/poseidon.rs +++ b/lightclient-circuits/src/poseidon.rs @@ -2,13 +2,18 @@ // Code: https://github.com/ChainSafe/Spectre // SPDX-License-Identifier: LGPL-3.0-only -use eth_types::{Field, LIMB_BITS, NUM_LIMBS}; +use eth_types::{Field, NUM_LIMBS}; use halo2_base::{ - gates::GateInstructions, halo2_proofs::halo2curves::bn256, halo2_proofs::plonk::Error, - poseidon::hasher::PoseidonSponge, AssignedValue, Context, QuantumCell, + gates::GateInstructions, + halo2_proofs::{ + halo2curves::bn256::{self}, + plonk::Error, + }, + poseidon::hasher::PoseidonSponge, + AssignedValue, Context, QuantumCell, }; use halo2_ecc::{bigint::ProperCrtUint, bls12_381::FpChip, fields::FieldChip}; -use halo2curves::bls12_381::{self, Fq}; +use halo2curves::bls12_381::{self, Fq, G1Affine}; use itertools::Itertools; use pse_poseidon::Poseidon as PoseidonNative; @@ -33,10 +38,11 @@ const R_F: usize = 8; /// Each Poseidon sponge absorbs `POSEIDON_SIZE`-2 elements and previos sponge output if it's not the first batch, ie. onion commitment. /// /// Assumes that LIMB_BITS * 2 < 254 (BN254). -pub fn fq_array_poseidon<'a, F: Field>( +pub fn g1_array_poseidon( ctx: &mut Context, fp_chip: &FpChip, - fields: impl IntoIterator>, + x_coords: impl IntoIterator>, + y_signs_packed: impl IntoIterator>, ) -> Result, Error> { let gate = fp_chip.gate(); let limbs_bases = fp_chip.limb_bases[..2] @@ -44,15 +50,17 @@ pub fn fq_array_poseidon<'a, F: Field>( .map(|c| QuantumCell::Constant(*c)) .collect_vec(); - let limbs = fields + let limbs = x_coords .into_iter() .flat_map(|f| { // Fold 4 limbs into 2 to reduce number of posedidon inputs in half. + // Extra limb is a result of halo2lib bigint strategy that while only need 4 limbs to represent BLS12-381 modulus, + // requires an extra limb for correct carry mod operations. let (limbs, extra) = f.limbs().split_at(NUM_LIMBS - (NUM_LIMBS % 2)); assert!(extra.len() <= 1); if let Some(extra) = extra.first() { let zero = ctx.load_zero(); - ctx.constrain_equal(extra, &zero); + ctx.constrain_equal(extra, &zero); // At this point extra limb should always be zero. } limbs @@ -66,7 +74,13 @@ pub fn fq_array_poseidon<'a, F: Field>( let mut current_poseidon_hash = None; - for (i, chunk) in limbs.chunks(POSEIDON_SIZE - 2).enumerate() { + for (i, chunk) in limbs + .into_iter() + .chain(y_signs_packed) + .collect_vec() + .chunks(POSEIDON_SIZE - 2) + .enumerate() + { poseidon.update(chunk); if i != 0 { poseidon.update(&[current_poseidon_hash.unwrap()]); @@ -80,20 +94,43 @@ pub fn fq_array_poseidon<'a, F: Field>( /// Generates Poseidon hash commitment to a list of BLS12-381 Fq elements. /// /// This is the off-circuit analog of `fq_array_poseidon`. -pub fn poseidon_hash_fq_array(elems: impl Iterator, limb_bits: usize) -> F { - let limbs = elems +fn poseidon_hash_g1_array( + x_coords: impl IntoIterator, + y_signs: impl IntoIterator, + limb_bits: usize, +) -> F { + let limbs = x_coords + .into_iter() // Converts Fq elements to Fr limbs. .flat_map(|x| { x.to_bytes_le() .chunks((limb_bits / 8) * 2) .map(F::from_bytes_le) .collect_vec() - }) - .collect_vec(); + }); let mut poseidon = PoseidonNative::::new(R_F, R_P); let mut current_poseidon_hash = None; - for (i, chunk) in limbs.chunks(POSEIDON_SIZE - 2).enumerate() { + let y_signs = y_signs + .into_iter() + .map(|sign| F::from(sign as u64)) + .collect_vec() + .chunks(bn256::Fr::CAPACITY as usize - 1) + .map(|chunk| { + let mut packed = F::ZERO; + for (i, bit) in chunk.iter().enumerate() { + packed += *bit * (F::from(2u64).pow([i as u64])); + } + packed + }) + .collect_vec(); + + for (i, chunk) in limbs + .chain(y_signs) + .collect_vec() + .chunks(POSEIDON_SIZE - 2) + .enumerate() + { poseidon.update(chunk); if i != 0 { poseidon.update(&[current_poseidon_hash.unwrap()]); @@ -106,27 +143,34 @@ pub fn poseidon_hash_fq_array(elems: impl Iterator, limb_bi /// Wrapper on `poseidon_hash_fq_array` taking pubkeys encoded as uncompressed bytes. pub fn poseidon_committee_commitment_from_uncompressed( pubkeys_uncompressed: &[Vec], + limb_bits: usize, ) -> bn256::Fr { - let pubkey_affines = pubkeys_uncompressed + let (x_coords, y_signs): (Vec<_>, Vec<_>) = pubkeys_uncompressed .iter() .cloned() - .map(|bytes| { - halo2curves::bls12_381::G1Affine::from_uncompressed_unchecked_be( - &bytes.as_slice().try_into().unwrap(), - ) - .unwrap() - }) - .collect_vec(); + .map(|bytes| G1Affine::from_uncompressed_be(&bytes.as_slice().try_into().unwrap()).unwrap()) + .map(|p| (p.x, (p.y.to_bytes()[0] & 1) == 1)) + .unzip(); - poseidon_hash_fq_array::(pubkey_affines.iter().map(|p| p.x), LIMB_BITS) + poseidon_hash_g1_array::(x_coords, y_signs, limb_bits) } /// Wrapper on `poseidon_hash_fq_array` taking pubkeys encoded as compressed bytes. -pub fn poseidon_committee_commitment_from_compressed(pubkeys_compressed: &[Vec]) -> bn256::Fr { - let pubkeys_x = pubkeys_compressed.iter().cloned().map(|mut bytes| { - bytes[0] &= 0b00011111; - bls12_381::Fq::from_bytes_be(&bytes.try_into().unwrap()) - .expect("bad bls12_381::Fq encoding") - }); - poseidon_hash_fq_array::(pubkeys_x, LIMB_BITS) +pub fn poseidon_committee_commitment_from_compressed( + pubkeys_compressed: &[Vec], + limb_bits: usize, +) -> bn256::Fr { + let (x_coords, y_signs): (Vec<_>, Vec<_>) = pubkeys_compressed + .iter() + .cloned() + .map(|mut bytes| { + let sign = (bytes[0] & 0b00100000) != 0; // check that 3-rd MSB bit is set + bytes[0] &= 0b00011111; // clear flag bits + let x = bls12_381::Fq::from_bytes_be(&bytes.try_into().unwrap()) + .expect("bad bls12_381::Fq encoding"); + + (x, sign) + }) + .unzip(); + poseidon_hash_g1_array::(x_coords, y_signs, limb_bits) } diff --git a/lightclient-circuits/src/sync_step_circuit.rs b/lightclient-circuits/src/sync_step_circuit.rs index 411edf2..447532d 100644 --- a/lightclient-circuits/src/sync_step_circuit.rs +++ b/lightclient-circuits/src/sync_step_circuit.rs @@ -10,7 +10,7 @@ use crate::{ }, to_bytes_le, }, - poseidon::{fq_array_poseidon, poseidon_hash_fq_array}, + poseidon::{g1_array_poseidon, poseidon_committee_commitment_from_uncompressed}, ssz_merkle::{ssz_merkleize_chunks, verify_merkle_proof}, util::{AppCircuit, Eth2ConfigPinning, IntoWitness}, witness::{self, HashInput, HashInputChunk, SyncStepArgs}, @@ -27,7 +27,6 @@ use halo2_base::{ plonk::Error, poly::{commitment::Params, kzg::commitment::ParamsKZG}, }, - utils::CurveAffineExt, AssignedValue, Context, QuantumCell, }; use halo2_ecc::{ @@ -36,12 +35,10 @@ use halo2_ecc::{ hash_to_curve::{ExpandMsgXmd, HashToCurveChip}, EcPoint, EccChip, }, - fields::FieldChip, + fields::{FieldChip, FieldChipExt}, }; use halo2curves::bls12_381::{G1Affine, G2Affine}; use itertools::Itertools; -use num_bigint::BigUint; -use snark_verifier::loader::halo2::EccInstructions; use ssz_rs::Merkleized; use std::{env::var, marker::PhantomData, vec}; @@ -85,26 +82,28 @@ impl StepCircuit { .as_slice() .iter() .map(|bytes| { - G1Affine::from_uncompressed_be(&bytes.as_slice().try_into().unwrap()) - .unwrap() + G1Affine::from_uncompressed_be(&bytes.as_slice().try_into().unwrap()).unwrap() }) .collect_vec(); let mut assigned_affines = vec![]; + let mut y_signs_packed = vec![]; let (agg_pubkey, participation_sum) = Self::aggregate_pubkeys( builder.main(), fp_chip, &pubkey_affines, &args.pariticipation_bits, &mut assigned_affines, + &mut y_signs_packed, ); // Commit to the pubkeys using Poseidon hash. This constraints prover to use the pubkeys of the current sync committee, // because the same commitment is computed in `CommitteeUpdateCircuit` and stored in the contract at the begining of the period. - let poseidon_commit = fq_array_poseidon( + let poseidon_commit = g1_array_poseidon( builder.main(), fp_chip, - assigned_affines.iter().map(|p| &p.x), + assigned_affines.into_iter().map(|p| p.x), + y_signs_packed, )?; // Compute attested header root @@ -215,7 +214,7 @@ impl StepCircuit { .try_into() .unwrap(); - truncate_sha256_into_single_elem(builder.main(), range, pub_inputs_bytes) + truncate_sha256_into_single_elem(builder.main(), gate, pub_inputs_bytes) }; Ok(vec![pub_inputs_commit, poseidon_commit]) @@ -260,17 +259,8 @@ impl StepCircuit { let execution_payload_root = &args.execution_payload_root; input[56..88].copy_from_slice(execution_payload_root); - let pubkey_affines = args - .pubkeys_uncompressed - .as_slice() - .iter() - .map(|bytes| { - G1Affine::from_uncompressed_unchecked_be(&bytes.as_slice().try_into().unwrap()) - .unwrap() - }) - .collect_vec(); let poseidon_commitment = - poseidon_hash_fq_array::(pubkey_affines.iter().map(|p| p.x), limb_bits); + poseidon_committee_commitment_from_uncompressed(&args.pubkeys_uncompressed, limb_bits); let mut public_input_commitment = sha2::Sha256::digest(input).to_vec(); // Truncate to 253 bits @@ -286,40 +276,24 @@ impl StepCircuit { // Truncate the SHA256 digest to 253 bits and convert to one field element. pub fn truncate_sha256_into_single_elem( ctx: &mut Context, - range: &impl RangeInstructions, + gate: &impl GateInstructions, hash_bytes: [AssignedValue; 32], ) -> AssignedValue { let public_input_commitment_bytes = { let mut truncated_hash = hash_bytes; - let cleared_byte = clear_3_bits(ctx, range, &truncated_hash[31]); + let cleared_byte = { + let bits = gate.num_to_bits(ctx, truncated_hash[31], 8); + gate.bits_to_num(ctx, &bits[..5]) + }; truncated_hash[31] = cleared_byte; truncated_hash }; let byte_bases = (0..32) - .map(|i| QuantumCell::Constant(range.gate().pow_of_two()[i * 8])) + .map(|i| QuantumCell::Constant(gate.pow_of_two()[i * 8])) .collect_vec(); - range - .gate() - .inner_product(ctx, public_input_commitment_bytes, byte_bases) -} - -/// Clears the 3 first least significat bits. -/// This function emulates bitwise and on 00011111 (0x1F): `b & 0b00011111` = c -pub fn clear_3_bits( - ctx: &mut Context, - range: &impl RangeInstructions, - b: &AssignedValue, -) -> AssignedValue { - let gate = range.gate(); - // Shift `a` three bits to the left (equivalent to a << 3 mod 256) - let b_shifted = gate.mul(ctx, *b, QuantumCell::Constant(F::from(8))); - // since b_shifted can at max be 255*8=2^4 we use 16 bits for modulo division. - let b_shifted = range.div_mod(ctx, b_shifted, BigUint::from(256u64), 16).1; - - // Shift `s` three bits to the right (equivalent to s >> 3) to zeroing the first three bits (MSB) of `a`. - range.div_mod(ctx, b_shifted, BigUint::from(8u64), 8).0 + gate.inner_product(ctx, public_input_commitment_bytes, byte_bases) } impl StepCircuit { @@ -342,13 +316,15 @@ impl StepCircuit { fp_chip: &FpChip<'_, F>, pubkey_affines: &[G1Affine], pariticipation_bits: &[bool], - assigned_affines: &mut Vec>, + assigned_pubkeys: &mut Vec>, + y_signs_packed: &mut Vec>, ) -> (G1Point, AssignedValue) { let gate = fp_chip.gate(); let g1_chip = G1Chip::::new(fp_chip); let mut participation_bits = vec![]; + let mut y_signs = vec![]; assert_eq!(pubkey_affines.len(), S::SYNC_COMMITTEE_SIZE); @@ -358,22 +334,13 @@ impl StepCircuit { let participation_bit = ctx.load_witness(F::from(is_attested as u64)); gate.assert_bit(ctx, participation_bit); - let assigned_pk = g1_chip.assign_point_unchecked(ctx, pk); - - // *Note:* normally, we would need to take into account the sign of the y coordinate, but - // because we are concerned only with signature forgery, if this is the wrong - // sign, the signature will be invalid anyway and thus verification fails. - /* - // Square y coordinate - let ysq = fp_chip.mul(ctx, assigned_pk.y.clone(), assigned_pk.y.clone()); - // Calculate y^2 using the elliptic curve equation - let ysq_calc = calculate_ysquared::(ctx, fp_chip, assigned_pk.x.clone()); - // Constrain witness y^2 to be equal to calculated y^2 - fp_chip.assert_equal(ctx, ysq, ysq_calc); - */ - - assigned_affines.push(assigned_pk); + let assigned_affine = g1_chip.assign_point(ctx, pk); + + let y_sign = fp_chip.sgn0(ctx, assigned_affine.y()); + + assigned_pubkeys.push(assigned_affine); participation_bits.push(participation_bit); + y_signs.push(y_sign); } let rand_point = g1_chip.load_random_point::(ctx); @@ -381,7 +348,7 @@ impl StepCircuit { for (bit, point) in participation_bits .iter() .copied() - .zip(assigned_affines.iter_mut()) + .zip(assigned_pubkeys.iter_mut()) { let sum = g1_chip.add_unequal(ctx, acc.clone(), point.clone(), true); acc = g1_chip.select(ctx, sum, acc, bit); @@ -389,6 +356,11 @@ impl StepCircuit { let agg_pubkey = g1_chip.sub_unequal(ctx, acc, rand_point, false); let participation_sum = gate.sum(ctx, participation_bits); + *y_signs_packed = y_signs + .chunks(F::CAPACITY as usize - 1) + .map(|chunk| gate.bits_to_num(ctx, chunk)) + .collect_vec(); + (agg_pubkey, participation_sum) } } diff --git a/prover/src/utils.rs b/prover/src/utils.rs index 9c53db8..688d32f 100644 --- a/prover/src/utils.rs +++ b/prover/src/utils.rs @@ -5,6 +5,7 @@ use std::{ops::Deref, sync::Arc}; use beacon_api_client::{BlockId, VersionedValue}; +use eth_types::NUM_LIMBS; use ethereum_consensus_types::LightClientBootstrap; use itertools::Itertools; use lightclient_circuits::poseidon::poseidon_committee_commitment_from_uncompressed; @@ -55,7 +56,8 @@ pub(crate) async fn utils_cli(method: UtilsCmd) -> eyre::Result<()> { println!("SSZ root: {:?}", hex::encode(ssz_root.deref())); let mut committee_poseidon = - poseidon_committee_commitment_from_uncompressed(&pubkeys_uncompressed).to_bytes(); + poseidon_committee_commitment_from_uncompressed(&pubkeys_uncompressed, NUM_LIMBS) + .to_bytes(); committee_poseidon.reverse(); println!("Poseidon commitment: {}", hex::encode(committee_poseidon)); diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index c677ff7..08dc587 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -10,7 +10,7 @@ mod test_types; use crate::execution_payload_header::ExecutionPayloadHeader; use crate::test_types::{ByteVector, TestMeta, TestStep}; -use eth_types::Minimal; +use eth_types::{Minimal, LIMB_BITS}; use ethereum_consensus_types::presets::minimal::{ LightClientBootstrap, LightClientUpdateCapella, BYTES_PER_LOGS_BLOOM, MAX_EXTRA_DATA_BYTES, }; @@ -40,7 +40,7 @@ pub fn get_initial_sync_committee_poseidon Date: Fri, 23 Feb 2024 01:44:17 +0100 Subject: [PATCH 3/4] fix bigint sign calc --- .../src/committee_update_circuit.rs | 4 ++-- lightclient-circuits/src/poseidon.rs | 13 +++++++----- lightclient-circuits/src/sync_step_circuit.rs | 20 +++++++++++++++++-- lightclient-circuits/tests/step.rs | 3 +++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/lightclient-circuits/src/committee_update_circuit.rs b/lightclient-circuits/src/committee_update_circuit.rs index c41265e..31ed653 100644 --- a/lightclient-circuits/src/committee_update_circuit.rs +++ b/lightclient-circuits/src/committee_update_circuit.rs @@ -141,13 +141,13 @@ impl CommitteeUpdateCircuit { // assertion check for assigned_uncompressed vector to be equal to S::PubKeyCurve::BYTES_COMPRESSED from specification assert_eq!(assigned_bytes.len(), 48); // masked byte from compressed representation - let masked_byte = &assigned_bytes[48 - 1]; + let masked_byte = &assigned_bytes[47]; // clear the flag bits from a last byte of compressed pubkey. // we are using [`clear_3_bits`] function which appears to be just as useful here as for public input commitment. let (cleared_byte, y_sign) = { let bits = gate.num_to_bits(ctx, *masked_byte, 8); let cleared = gate.bits_to_num(ctx, &bits[..5]); - (cleared, bits[5]) // 3 MSB bits are cleared, 3th bit is a sign bit + (cleared, bits[5]) // 3 MSB bits are cleared, 3-rd of those is a sign bit }; // Use the cleared byte to construct the x coordinate let assigned_x_bytes_cleared = diff --git a/lightclient-circuits/src/poseidon.rs b/lightclient-circuits/src/poseidon.rs index 1bc1e8b..9a07764 100644 --- a/lightclient-circuits/src/poseidon.rs +++ b/lightclient-circuits/src/poseidon.rs @@ -5,16 +5,15 @@ use eth_types::{Field, NUM_LIMBS}; use halo2_base::{ gates::GateInstructions, - halo2_proofs::{ - halo2curves::bn256::{self}, - plonk::Error, - }, + halo2_proofs::{halo2curves::bn256, plonk::Error}, poseidon::hasher::PoseidonSponge, + utils::modulus, AssignedValue, Context, QuantumCell, }; use halo2_ecc::{bigint::ProperCrtUint, bls12_381::FpChip, fields::FieldChip}; use halo2curves::bls12_381::{self, Fq, G1Affine}; use itertools::Itertools; +use num_bigint::BigUint; use pse_poseidon::Poseidon as PoseidonNative; // Using recommended parameters from whitepaper https://eprint.iacr.org/2019/458.pdf (table 2, table 8) @@ -149,7 +148,11 @@ pub fn poseidon_committee_commitment_from_uncompressed( .iter() .cloned() .map(|bytes| G1Affine::from_uncompressed_be(&bytes.as_slice().try_into().unwrap()).unwrap()) - .map(|p| (p.x, (p.y.to_bytes()[0] & 1) == 1)) + .map(|p| { + let y = BigUint::from_bytes_le(p.y.to_repr().as_ref()) * BigUint::from(2u64); + let sign = y > modulus::(); + (p.x, sign) + }) .unzip(); poseidon_hash_g1_array::(x_coords, y_signs, limb_bits) diff --git a/lightclient-circuits/src/sync_step_circuit.rs b/lightclient-circuits/src/sync_step_circuit.rs index 447532d..d7bcf74 100644 --- a/lightclient-circuits/src/sync_step_circuit.rs +++ b/lightclient-circuits/src/sync_step_circuit.rs @@ -27,18 +27,21 @@ use halo2_base::{ plonk::Error, poly::{commitment::Params, kzg::commitment::ParamsKZG}, }, + utils::modulus, AssignedValue, Context, QuantumCell, }; use halo2_ecc::{ + bigint::big_less_than, bls12_381::{bls_signature::BlsSignatureChip, pairing::PairingChip, Fp2Chip, Fp2Point, FpChip}, ecc::{ hash_to_curve::{ExpandMsgXmd, HashToCurveChip}, EcPoint, EccChip, }, - fields::{FieldChip, FieldChipExt}, + fields::FieldChip, }; use halo2curves::bls12_381::{G1Affine, G2Affine}; use itertools::Itertools; +use num_bigint::BigUint; use ssz_rs::Merkleized; use std::{env::var, marker::PhantomData, vec}; @@ -336,7 +339,20 @@ impl StepCircuit { let assigned_affine = g1_chip.assign_point(ctx, pk); - let y_sign = fp_chip.sgn0(ctx, assigned_affine.y()); + let half_p = fp_chip.load_constant_uint( + ctx, + modulus::() / BigUint::from(2u64), + ); + // y_sign = pk.y * 2 > p + // due to the limiation of halo2lib api we perform an equivalent operation: y_sign = pk.y < p/2 + let y_sign = big_less_than::assign( + fp_chip.range(), + ctx, + half_p, + assigned_affine.y().clone(), + fp_chip.limb_bits, + fp_chip.limb_bases[1], + ); assigned_pubkeys.push(assigned_affine); participation_bits.push(participation_bit); diff --git a/lightclient-circuits/tests/step.rs b/lightclient-circuits/tests/step.rs index 1085d93..51c2e44 100644 --- a/lightclient-circuits/tests/step.rs +++ b/lightclient-circuits/tests/step.rs @@ -108,6 +108,9 @@ fn run_test_eth2_spec_mock(path: PathB let prover = MockProver::::run(K_SYNC, &sync_circuit, instance).unwrap(); prover.assert_satisfied(); end_timer!(timer); + + // check that sync committee poseidon commits match in both circuits + assert_eq!(sync_circuit.instances()[0][1], rotation_circuit.instances()[0][0]); } #[rstest] From 1cd94c7a8278e89d9022b6267efe5af9ad044b75 Mon Sep 17 00:00:00 2001 From: Timofey Luin Date: Mon, 4 Mar 2024 21:16:13 +0100 Subject: [PATCH 4/4] add comment --- lightclient-circuits/src/sync_step_circuit.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lightclient-circuits/src/sync_step_circuit.rs b/lightclient-circuits/src/sync_step_circuit.rs index d7bcf74..d29b4ce 100644 --- a/lightclient-circuits/src/sync_step_circuit.rs +++ b/lightclient-circuits/src/sync_step_circuit.rs @@ -344,7 +344,8 @@ impl StepCircuit { modulus::() / BigUint::from(2u64), ); // y_sign = pk.y * 2 > p - // due to the limiation of halo2lib api we perform an equivalent operation: y_sign = pk.y < p/2 + // becasue of halo2lib doesn't allow to double without carry mod checking 2*y > p gives inconsistent results + // so instead we perform an equivalent operation: y_sign = pk.y < p/2 let y_sign = big_less_than::assign( fp_chip.range(), ctx,