Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix missing subgroup check #62

Merged
merged 4 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading