Skip to content

Commit

Permalink
Fix missing subgroup check (#62)
Browse files Browse the repository at this point in the history
* add subgroup check to Step

* add y sign cross-circuit check

* fix bigint sign calc

* add comment
  • Loading branch information
nulltea authored Mar 4, 2024
1 parent 53daf7f commit e2a495e
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 111 deletions.
49 changes: 28 additions & 21 deletions lightclient-circuits/src/committee_update_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -73,8 +73,9 @@ impl<S: Spec, F: Field> CommitteeUpdateCircuit<S, F> {
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
Expand Down Expand Up @@ -129,35 +130,47 @@ impl<S: Spec, F: Field> CommitteeUpdateCircuit<S, F> {
ctx: &mut Context<F>,
fp_chip: &FpChip<'_, F>,
compressed_encodings: impl IntoIterator<Item = Vec<AssignedValue<F>>>,
) -> Vec<ProperCrtUint<F>> {
let range = fp_chip.range();
) -> (Vec<ProperCrtUint<F>>, Vec<AssignedValue<F>>) {
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.
assigned_bytes.reverse();
// 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 = 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, 3-rd of those 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::<F>(
let x = decode_into_bn::<F>(
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<GateManager: CommonGateManager<F>>(
Expand Down Expand Up @@ -189,14 +202,8 @@ impl<S: Spec, F: Field> CommitteeUpdateCircuit<S, F> {
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::<bn256::Fr>(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();

Expand Down
105 changes: 76 additions & 29 deletions lightclient-circuits/src/poseidon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +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, 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};
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)
Expand All @@ -33,26 +37,29 @@ 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<F: Field>(
ctx: &mut Context<F>,
fp_chip: &FpChip<F>,
fields: impl IntoIterator<Item = &'a ProperCrtUint<F>>,
x_coords: impl IntoIterator<Item = ProperCrtUint<F>>,
y_signs_packed: impl IntoIterator<Item = AssignedValue<F>>,
) -> Result<AssignedValue<F>, Error> {
let gate = fp_chip.gate();
let limbs_bases = fp_chip.limb_bases[..2]
.iter()
.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
Expand All @@ -66,7 +73,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()]);
Expand All @@ -80,20 +93,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<F: Field>(elems: impl Iterator<Item = Fq>, limb_bits: usize) -> F {
let limbs = elems
fn poseidon_hash_g1_array<F: Field>(
x_coords: impl IntoIterator<Item = Fq>,
y_signs: impl IntoIterator<Item = bool>,
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::<F, T, POSEIDON_SIZE>::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()]);
Expand All @@ -106,27 +142,38 @@ pub fn poseidon_hash_fq_array<F: Field>(elems: impl Iterator<Item = Fq>, limb_bi
/// Wrapper on `poseidon_hash_fq_array` taking pubkeys encoded as uncompressed bytes.
pub fn poseidon_committee_commitment_from_uncompressed(
pubkeys_uncompressed: &[Vec<u8>],
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()
.map(|bytes| G1Affine::from_uncompressed_be(&bytes.as_slice().try_into().unwrap()).unwrap())
.map(|p| {
let y = BigUint::from_bytes_le(p.y.to_repr().as_ref()) * BigUint::from(2u64);
let sign = y > modulus::<halo2curves::bls12_381::Fq>();
(p.x, sign)
})
.collect_vec();
.unzip();

poseidon_hash_fq_array::<bn256::Fr>(pubkey_affines.iter().map(|p| p.x), LIMB_BITS)
poseidon_hash_g1_array::<bn256::Fr>(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<u8>]) -> 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::<bn256::Fr>(pubkeys_x, LIMB_BITS)
pub fn poseidon_committee_commitment_from_compressed(
pubkeys_compressed: &[Vec<u8>],
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::<bn256::Fr>(x_coords, y_signs, limb_bits)
}
Loading

0 comments on commit e2a495e

Please sign in to comment.