Skip to content

Commit

Permalink
Merge branch 'main' into nulltea/pk_aggr_avoid_infinity
Browse files Browse the repository at this point in the history
  • Loading branch information
nulltea authored Mar 5, 2024
2 parents 386c56c + 1a1f9c9 commit f6c0755
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 123 deletions.
6 changes: 3 additions & 3 deletions lightclient-circuits/config/sync_step_testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
},
"break_points": [
[
2097142,
2097142,
2097141,
2097141,
2097142,
2097140,
2097142,
2097140
2097141
]
]
}
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
11 changes: 9 additions & 2 deletions lightclient-circuits/src/gadget/crypto/sha256_wide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl<'a, F: Field> HashInstructions<F> for Sha256ChipWide<'a, F> {
builder: &mut Self::CircuitBuilder,
input: impl IntoIterator<Item = QuantumCell<F>>,
) -> Result<Vec<AssignedValue<F>>, Error> {
let assigned_bytes = input
let mut assigned_bytes = input
.into_iter()
.map(|cell| match cell {
QuantumCell::Existing(v) => v,
Expand Down Expand Up @@ -75,8 +75,14 @@ impl<'a, F: Field> HashInstructions<F> for Sha256ChipWide<'a, F> {
.map(|i| QuantumCell::Constant(gate.pow_of_two()[i * 8]))
.collect_vec();

// Following code will check the consitency of halo2-lib input bytes with their word representation in halo2 vanilla
// Since words are 4 bytes each, we extend the input bytes to be a multiple of 4 with zero bytes in a same way as it's done in vanilla during witness assignment.
assigned_bytes.resize_with(num_input_words * 4, || builder.main().load_zero());

for r in 0..num_input_rounds {
for w in 0..(num_input_words - r * NUM_WORDS_TO_ABSORB) {
let remaining_words = num_input_words - r * NUM_WORDS_TO_ABSORB;

for w in 0..std::cmp::min(remaining_words, NUM_WORDS_TO_ABSORB){
let i = (r * NUM_WORDS_TO_ABSORB + w) * 4;
let checksum = gate.inner_product(
builder.main(),
Expand Down Expand Up @@ -121,3 +127,4 @@ pub fn word_to_bytes_le<F: Field>(
.chain(to_bytes_le::<_, 16>(&word.hi(), gate, ctx))
.collect()
}

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)
}
33 changes: 26 additions & 7 deletions lightclient-circuits/src/ssz_merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ use halo2_base::{
QuantumCell,
};
use itertools::Itertools;
use lazy_static::lazy_static;
use sha2::Digest;

// Maximum number of input leafs that are not a power of two because the zero hashes are only precomputed for the first two levels.
// In practice, the maximum number of input leafs that is not a power of two used in this project is 5.
const MAX_INPUT_LEAFS_NOT_POW2: usize = 7;

/// Computes Merkle root of a list of SSZ chunks.
///
Expand All @@ -24,6 +30,13 @@ pub fn ssz_merkleize_chunks<F: Field, CircuitBuilder: CommonCircuitBuilder<F>>(
chunks: impl IntoIterator<Item = HashInputChunk<QuantumCell<F>>>,
) -> Result<Vec<AssignedValue<F>>, Error> {
let mut chunks = chunks.into_iter().collect_vec();

assert!(
chunks.len() < MAX_INPUT_LEAFS_NOT_POW2 || chunks.len().is_power_of_two(),
"merkleize_chunks: expected number of chunks to be a power of two or less than {}",
MAX_INPUT_LEAFS_NOT_POW2
);

let len_even = chunks.len() + chunks.len() % 2;
let height = (len_even as f64).log2().ceil() as usize;
for depth in 0..height {
Expand Down Expand Up @@ -94,10 +107,16 @@ pub fn verify_merkle_proof<F: Field, CircuitBuilder: CommonCircuitBuilder<F>>(
Ok(())
}

pub const ZERO_HASHES: [[u8; 32]; 2] = [
[0; 32],
[
245, 165, 253, 66, 209, 106, 32, 48, 39, 152, 239, 110, 211, 9, 151, 155, 67, 0, 61, 35,
32, 217, 240, 232, 234, 152, 49, 169, 39, 89, 251, 75,
],
];
lazy_static! {
// Calculates padding Merkle notes for the 2 first levels of the Merkle tree.
// Used to pad the input to a power of two. Only 2 levels are precomputed because the number of not even inputs is limited.
static ref ZERO_HASHES: [[u8; 32]; 2] = {
std::iter::successors(Some([0; 32]), |&prev| {
Some(sha2::Sha256::digest([prev, prev].concat()).to_vec().try_into().unwrap())
})
.take(2)
.collect_vec()
.try_into()
.unwrap()
};
}
Loading

0 comments on commit f6c0755

Please sign in to comment.