From 9c8078565d8123250981a4352b4a4ee489cf7446 Mon Sep 17 00:00:00 2001 From: timofey Date: Wed, 4 Oct 2023 19:16:42 +0200 Subject: [PATCH] Add missing instances (#18) * add missing instances in step & rotation circuits * return all 32 bytes as expected by sha chip * replace onion hashing with input concat * off-circuit instance generation for concat input hashing 0c6f3f * refactor poseidon commit native * replace instance comm with instance vector in rotation circuit * spel fix --- .../src/committee_update_circuit.rs | 134 +++++----- lightclient-circuits/src/poseidon.rs | 24 +- lightclient-circuits/src/sync_step_circuit.rs | 245 ++++++++---------- lightclient-circuits/src/witness/hashing.rs | 7 +- lightclient-circuits/tests/step.rs | 5 +- 5 files changed, 196 insertions(+), 219 deletions(-) diff --git a/lightclient-circuits/src/committee_update_circuit.rs b/lightclient-circuits/src/committee_update_circuit.rs index 8e7f32ee..3faec832 100644 --- a/lightclient-circuits/src/committee_update_circuit.rs +++ b/lightclient-circuits/src/committee_update_circuit.rs @@ -1,4 +1,4 @@ -use std::{env::var, marker::PhantomData, vec}; +use std::{env::var, iter, marker::PhantomData, vec}; use crate::{ builder::Eth2CircuitBuilder, @@ -6,8 +6,9 @@ use crate::{ calculate_ysquared, Fp2Point, FpPoint, G1Chip, G1Point, G2Chip, G2Point, HashInstructions, HashToCurveCache, HashToCurveChip, Sha256ChipWide, ShaBitThreadBuilder, ShaCircuitBuilder, }, - poseidon::{fq_array_poseidon, g1_array_poseidon_native, poseidon_sponge}, + poseidon::{fq_array_poseidon, fq_array_poseidon_native, poseidon_sponge}, ssz_merkle::ssz_merkleize_chunks, + sync_step_circuit::{clear_3_bits, to_bytes_le, truncate_sha256_into_single_elem}, util::{ decode_into_field, gen_pkey, AppCircuit, AssignedValueCell, Challenges, Eth2ConfigPinning, IntoWitness, ThreadBuilderBase, @@ -26,7 +27,7 @@ use halo2_base::{ range::{RangeConfig, RangeStrategy}, }, safe_types::{GateInstructions, RangeChip, RangeInstructions}, - utils::{fs::gen_srs, CurveAffineExt}, + utils::{fs::gen_srs, CurveAffineExt, ScalarField}, AssignedValue, Context, QuantumCell, }; use halo2_ecc::{ @@ -42,7 +43,7 @@ use halo2_proofs::{ poly::{commitment::Params, kzg::commitment::ParamsKZG}, }; use halo2curves::{ - bls12_381::{Fq, Fq12, G1Affine, G2Affine, G2Prepared, G1, G2}, + bls12_381::{self, Fq, Fq12, G1Affine, G2Affine, G2Prepared, G1, G2}, bn256, }; use itertools::Itertools; @@ -51,7 +52,7 @@ use num_bigint::BigUint; use pasta_curves::group::{ff, GroupEncoding}; use poseidon::PoseidonChip; use snark_verifier_sdk::CircuitExt; -use ssz_rs::Merkleized; +use ssz_rs::{Merkleized, Vector}; use sync_committee_primitives::consensus_types::BeaconBlockHeader; #[allow(type_alias_bounds)] @@ -83,89 +84,82 @@ impl CommitteeUpdateCircuit { }) .collect_vec(); - let root = + let committee_root_ssz = Self::sync_committee_root_ssz(thread_pool, &sha256_chip, compressed_encodings.clone())?; - let pubkeys_x = Self::decode_pubkeys_x(thread_pool.main(), &fp_chip, compressed_encodings); - let poseidon_commit = fq_array_poseidon(thread_pool.main(), range.gate(), &pubkeys_x)?; + let poseidon_commit = { + let pubkeys_x = + Self::decode_pubkeys_x(thread_pool.main(), &fp_chip, compressed_encodings); + fq_array_poseidon(thread_pool.main(), range.gate(), &pubkeys_x)? + }; - Ok(vec![poseidon_commit]) + let public_inputs = iter::once(poseidon_commit) + .chain(committee_root_ssz) + .collect(); + + Ok(public_inputs) } - pub fn instance(pubkeys_uncompressed: Vec>) -> Vec> { - let pubkey_affines = pubkeys_uncompressed + pub fn instance(args: &witness::CommitteeRotationArgs) -> Vec> { + let pubkeys_x = args.pubkeys_compressed.iter().cloned().map(|mut bytes| { + bytes[47] &= 0b11111000; + bls12_381::Fq::from_bytes_le(&bytes) + }); + + let poseidon_commitment = fq_array_poseidon_native::(pubkeys_x).unwrap(); + + let mut pk_vector: Vector, 512> = args + .pubkeys_compressed .iter() - .map(|bytes| { - G1Affine::from_compressed_unchecked(&bytes.as_slice().try_into().unwrap()).unwrap() - }) - .collect_vec(); - let poseidon_commitment = g1_array_poseidon_native::(&pubkey_affines).unwrap(); - vec![vec![poseidon_commitment]] + .cloned() + .map(|v| v.try_into().unwrap()) + .collect_vec() + .try_into() + .unwrap(); + + let ssz_root = pk_vector.hash_tree_root().unwrap(); + + let instance_vec = iter::once(poseidon_commitment) + .chain(ssz_root.0.map(|b| bn256::Fr::from(b as u64))) + .collect(); + + vec![instance_vec] } - fn decode_pubkeys_x>>>( + fn decode_pubkeys_x( ctx: &mut Context, fp_chip: &FpChip<'_, F>, - compressed_encodings: I, + compressed_encodings: impl IntoIterator>>, ) -> Vec> { let range = fp_chip.range(); let gate = fp_chip.gate(); - let g1_chip = G1Chip::::new(fp_chip); - - let mut pubkeys_x = vec![]; - - for assigned_bytes in compressed_encodings { - // assertion check for assigned_uncompressed vector to be equal to S::PubKeyCurve::BYTES_COMPRESSED from specification - assert_eq!(assigned_bytes.len(), G1::BYTES_COMPRESSED); - - // masked byte from compressed representation - let masked_byte = &assigned_bytes[G1::BYTES_COMPRESSED - 1]; - // clear the sign bit from masked byte - let cleared_byte = Self::clear_flag_bits(range, masked_byte, ctx); - // Use the cleared byte to construct the x coordinate - let assigned_x_bytes_cleared = [ - &assigned_bytes.as_slice()[..G1::BYTES_COMPRESSED - 1], - &[cleared_byte], - ] - .concat(); - let x_crt = decode_into_field::( - assigned_x_bytes_cleared, - &fp_chip.limb_bases, - gate, - ctx, - ); - - pubkeys_x.push(x_crt); - } - - pubkeys_x - } - - /// Clears the 3 first least significat bits used for flags from a last byte of compressed pubkey. - /// This function emulates bitwise and on 00011111 (0x1F): `b & 0b00011111` = c - fn clear_flag_bits( - range: &RangeChip, - b: &AssignedValue, - ctx: &mut Context, - ) -> 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 + compressed_encodings + .into_iter() + .map(|assigned_bytes| { + // assertion check for assigned_uncompressed vector to be equal to S::PubKeyCurve::BYTES_COMPRESSED from specification + assert_eq!(assigned_bytes.len(), G1::BYTES_COMPRESSED); + // masked byte from compressed representation + let masked_byte = &assigned_bytes[G1::BYTES_COMPRESSED - 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); + // Use the cleared byte to construct the x coordinate + let assigned_x_bytes_cleared = [ + &assigned_bytes.as_slice()[..G1::BYTES_COMPRESSED - 1], + &[cleared_byte], + ] + .concat(); + + decode_into_field::(assigned_x_bytes_cleared, &fp_chip.limb_bases, gate, ctx) + }) + .collect() } - fn sync_committee_root_ssz< - ThreadBuilder: ThreadBuilderBase, - I: IntoIterator>>, - >( + fn sync_committee_root_ssz>( thread_pool: &mut ThreadBuilder, hasher: &impl HashInstructions, - compressed_encodings: I, + compressed_encodings: impl IntoIterator>>, ) -> Result>, Error> { let mut pubkeys_hashes: Vec>> = compressed_encodings .into_iter() diff --git a/lightclient-circuits/src/poseidon.rs b/lightclient-circuits/src/poseidon.rs index 11fafb02..c2f83e04 100644 --- a/lightclient-circuits/src/poseidon.rs +++ b/lightclient-circuits/src/poseidon.rs @@ -1,11 +1,11 @@ use crate::gadget::crypto::G1Point; -use eth_types::{AppCurveExt, Field}; +use eth_types::{AppCurveExt, Field, Spec}; use halo2_base::safe_types::ScalarField; use halo2_base::{safe_types::GateInstructions, AssignedValue, Context}; use halo2_ecc::bigint::{ProperCrtUint, ProperUint}; use halo2_proofs::plonk::Error; -use halo2curves::bls12_381::G1Affine; use halo2curves::bls12_381::G1; +use halo2curves::bls12_381::{self, G1Affine}; use itertools::Itertools; use poseidon::PoseidonChip; use poseidon_native::Poseidon as PoseidonNative; @@ -42,21 +42,17 @@ pub fn fq_array_poseidon<'a, F: Field>( Ok(current_poseidon_hash.unwrap()) } -pub fn g1_array_poseidon_native(points: &[G1Affine]) -> Result { - let limbs = points - .iter() - // Converts the point (usually in Fq) to limbs. - .flat_map(|point| { - point - .x - .to_bytes_le() - .chunks(14) +pub fn fq_array_poseidon_native( + elems: impl Iterator, +) -> Result { + let limbs = elems + // Converts Fq elements to Fr limbs. + .flat_map(|x| { + x.to_bytes_le() + .chunks(bls12_381::G1::LIMB_BITS / 8) .map(F::from_bytes_le) .collect_vec() }) - // Converts the Fq point to a circuit field. It is safe because the limbs should be smaller - // even if the bits in the Field of the point are larger than the bits of the circuit field. - .map(|fq_limbs| F::from_bytes_le_unsecure(&fq_limbs.to_bytes_le())) .collect_vec(); let mut poseidon = PoseidonNative::::new(R_F, R_P); diff --git a/lightclient-circuits/src/sync_step_circuit.rs b/lightclient-circuits/src/sync_step_circuit.rs index 97f74eab..24636f1a 100644 --- a/lightclient-circuits/src/sync_step_circuit.rs +++ b/lightclient-circuits/src/sync_step_circuit.rs @@ -2,7 +2,9 @@ use std::{ cell::RefCell, collections::HashMap, env::{set_var, var}, - fs, iter, + fs, + io::Read, + iter, marker::PhantomData, ops::Neg, path::Path, @@ -16,7 +18,7 @@ use crate::{ calculate_ysquared, Fp2Point, FpPoint, G1Chip, G1Point, G2Chip, G2Point, HashInstructions, HashToCurveCache, HashToCurveChip, Sha256Chip, ShaCircuitBuilder, ShaThreadBuilder, }, - poseidon::{fq_array_poseidon, g1_array_poseidon_native, poseidon_sponge}, + poseidon::{fq_array_poseidon, fq_array_poseidon_native, poseidon_sponge}, ssz_merkle::{ssz_merkleize_chunks, verify_merkle_proof}, util::{ decode_into_field, gen_pkey, AppCircuit, AssignedValueCell, Challenges, Eth2ConfigPinning, @@ -129,7 +131,7 @@ impl SyncStepCircuit { let mut h2c_cache = HashToCurveCache::::default(); // Verify attestted header - let attested_slot: HashInputChunk<_> = args.attested_header.slot.into_witness(); + let attested_slot_bytes: HashInputChunk<_> = args.attested_header.slot.into_witness(); let attested_header_state_root = args .attested_header .state_root @@ -141,7 +143,7 @@ impl SyncStepCircuit { thread_pool, &sha256_chip, [ - attested_slot.clone(), + attested_slot_bytes.clone(), args.attested_header.proposer_index.into_witness(), args.attested_header.parent_root.as_ref().into_witness(), attested_header_state_root.clone().into(), @@ -161,13 +163,12 @@ impl SyncStepCircuit { .map(|&b| thread_pool.main().load_witness(F::from(b as u64))) .collect_vec(); - let finalized_slot: HashInputChunk<_> = args.finalized_header.slot.into_witness(); - + let finalized_slot_bytes: HashInputChunk<_> = args.finalized_header.slot.into_witness(); let finalized_header_root = ssz_merkleize_chunks( thread_pool, &sha256_chip, [ - finalized_slot.clone(), + finalized_slot_bytes.clone(), args.finalized_header.proposer_index.into_witness(), args.finalized_header.parent_root.as_ref().into_witness(), args.finalized_header.state_root.as_ref().into_witness(), @@ -209,7 +210,7 @@ impl SyncStepCircuit { args.finality_branch .iter() .map(|w| w.clone().into_witness()), - finalized_header_root.into(), + finalized_header_root.clone().into(), &attested_header_state_root, S::FINALIZED_HEADER_INDEX, )?; @@ -227,140 +228,76 @@ impl SyncStepCircuit { )?; // Public Input Commitment - let h = sha256_chip.digest::<64>( - thread_pool, - HashInput::TwoToOne(attested_slot, finalized_slot), - false, - )?; - - // TODO: Investigate if we should hash it all concatinated in one go - // TODO: Investigate if we need `finalized_header_root` in PI - // let h = sha256_chip.digest::<64>( - // thread_pool, - // HashInput::TwoToOne(h.output_bytes.into(), finalized_header_root.into()), - // false, - // )?; + let participation_sum_le = + to_bytes_le::<_, 8>(thread_pool.main(), gate, &participation_sum); - let byte_base = (0..32) - .map(|i| QuantumCell::Constant(gate.pow_of_two()[i * 8])) - .collect_vec(); + let poseidon_commit_le = to_bytes_le::<_, 32>(thread_pool.main(), gate, &poseidon_commit); - let participation_sum_bytes = { - let assigned_sum_bytes = participation_sum - .value() - .to_bytes_le() + // See "Onion hashing vs. Input concatenation" in https://github.com/ChainSafe/Spectre/issues/17#issuecomment-1740965182 + let public_inputs_concat = itertools::chain![ + attested_slot_bytes.bytes.into_iter().take(8), + finalized_slot_bytes.bytes.into_iter().take(8), + participation_sum_le .into_iter() - .map(|v| thread_pool.main().load_witness(F::from(v as u64))) - .collect_vec(); - - // Constrain the participation sum bytes to be equal to the participation_sum - let sum_field = gate.inner_product( - thread_pool.main(), - assigned_sum_bytes.clone(), - byte_base.clone(), - ); - thread_pool - .main() - .constrain_equal(&sum_field, &participation_sum); - - assigned_sum_bytes - }; - - let h = sha256_chip.digest::<64>( - thread_pool, - HashInput::TwoToOne(h.output_bytes.into(), participation_sum_bytes.into()), - false, - )?; - - let h = sha256_chip.digest::<64>( - thread_pool, - HashInput::TwoToOne(h.output_bytes.into(), execution_payload_root), - false, - )?; - - let poseidon_commit_bytes = { - let assigned_bytes = poseidon_commit - .value() - .to_bytes_le() + .map(|b| QuantumCell::Existing(b)), + finalized_header_root .into_iter() - .map(|v| thread_pool.main().load_witness(F::from(v as u64))) - .collect_vec(); - - // Constrain poseidon bytes to be equal to the poseidon_commit_value - let poseidon_commit_field = gate.inner_product( - thread_pool.main(), - assigned_bytes.clone(), - byte_base.clone(), - ); - thread_pool - .main() - .constrain_equal(&poseidon_commit_field, &poseidon_commit); - - assigned_bytes - }; - - let public_input_commitment = sha256_chip.digest::<64>( - thread_pool, - HashInput::TwoToOne(h.output_bytes.into(), poseidon_commit_bytes.into()), - false, - )?; + .map(|b| QuantumCell::Existing(b)), + execution_payload_root.bytes.into_iter(), + poseidon_commit_le + .into_iter() + .map(|b| QuantumCell::Existing(b)), + ] + .collect_vec(); - // Truncate the public input commitment to 253 bits and convert to one field element - let public_input_commitment_bytes = { - let mut truncated_hash = public_input_commitment.output_bytes; - let cleared_byte = clear_3_bits(range, &truncated_hash[31], thread_pool.main()); - truncated_hash[31] = cleared_byte; - truncated_hash - }; + let pi_hash_bytes = sha256_chip + .digest::<{ 8 * 3 + 32 * 3 }>( + thread_pool, + HashInputChunk::new(public_inputs_concat).into(), + false, + )? + .output_bytes; - let pi_field = - gate.inner_product(thread_pool.main(), public_input_commitment_bytes, byte_base); + let pi_commit = truncate_sha256_into_single_elem(thread_pool.main(), range, pi_hash_bytes); - Ok(vec![pi_field]) + Ok(vec![pi_commit]) } pub fn instance(args: SyncStepArgs) -> bn256::Fr { - let mut input: [u8; 64] = [0; 64]; - - let mut attested_slot = args.attested_header.slot.to_le_bytes().to_vec(); - let mut finalized_slot = args.finalized_header.slot.to_le_bytes().to_vec(); - attested_slot.resize(32, 0); - finalized_slot.resize(32, 0); - - input[..32].copy_from_slice(&attested_slot); - input[32..].copy_from_slice(&finalized_slot); - let h = sha2::Sha256::digest(input).to_vec(); - - // let finalized_header_root: [u8; 32] = args - // .finalized_header - // .clone() - // .hash_tree_root() - // .unwrap() - // .as_bytes() - // .try_into() - // .unwrap(); - - // input[..32].copy_from_slice(&h); - // input[32..].copy_from_slice(&finalized_header_root); - // let h = sha2::Sha256::digest(input).to_vec(); - - let mut participation = args + const INPUT_SIZE: usize = 8 * 3 + 32 * 3; + let mut input = [0; INPUT_SIZE]; + + let mut attested_slot_le = args.attested_header.slot.to_le_bytes().to_vec(); + attested_slot_le.resize(8, 0); + input[..8].copy_from_slice(&attested_slot_le); + + let mut finalized_slot_le = args.finalized_header.slot.to_le_bytes().to_vec(); + finalized_slot_le.resize(8, 0); + input[8..16].copy_from_slice(&finalized_slot_le); + + let mut participation_le = args .pariticipation_bits .iter() .map(|v| *v as u64) .sum::() .to_le_bytes() .to_vec(); - participation.resize(32, 0); + participation_le.resize(8, 0); + input[16..24].copy_from_slice(&participation_le); - input[..32].copy_from_slice(&h); - input[32..].copy_from_slice(&participation); - let h = sha2::Sha256::digest(input).to_vec(); + let finalized_header_root: [u8; 32] = args + .finalized_header + .clone() + .hash_tree_root() + .unwrap() + .as_bytes() + .try_into() + .unwrap(); + + input[24..56].copy_from_slice(&finalized_header_root); let execution_payload_root = &args.execution_payload_root; - input[..32].copy_from_slice(&h); - input[32..].copy_from_slice(execution_payload_root); - let h = sha2::Sha256::digest(input).to_vec(); + input[56..88].copy_from_slice(execution_payload_root); let pubkey_affines = args .pubkeys_uncompressed @@ -371,10 +308,10 @@ impl SyncStepCircuit { .unwrap() }) .collect_vec(); - let poseidon_commitment = g1_array_poseidon_native::(&pubkey_affines).unwrap(); - let poseidon_commitment_bytes = poseidon_commitment.to_bytes_le(); - input[..32].copy_from_slice(&h); - input[32..].copy_from_slice(&poseidon_commitment_bytes); + let poseidon_commitment = + fq_array_poseidon_native::(pubkey_affines.iter().map(|p| p.x)).unwrap(); + let poseidon_commitment_le = poseidon_commitment.to_bytes_le(); + input[88..].copy_from_slice(&poseidon_commitment_le); let mut public_input_commitment = sha2::Sha256::digest(input).to_vec(); // Truncate to 253 bits @@ -383,12 +320,34 @@ impl SyncStepCircuit { } } +// 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, + 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]); + truncated_hash[31] = cleared_byte; + truncated_hash + }; + + let byte_bases = (0..32) + .map(|i| QuantumCell::Constant(range.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 -fn clear_3_bits( - range: &RangeChip, - b: &AssignedValue, +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) @@ -400,6 +359,30 @@ fn clear_3_bits( range.div_mod(ctx, b_shifted, BigUint::from(8u64), 8).0 } +pub fn to_bytes_le( + ctx: &mut Context, + gate: &impl GateInstructions, + a: &AssignedValue, +) -> Vec> { + let byte_bases = (0..MAX_BYTES) + .map(|i| QuantumCell::Constant(gate.pow_of_two()[i * 8])) + .collect_vec(); + + let assigned_bytes = a + .value() + .to_bytes_le() + .into_iter() + .take(MAX_BYTES) + .map(|v| ctx.load_witness(F::from(v as u64))) + .collect_vec(); + + // Constrain poseidon bytes to be equal to the recovered checksum + let checksum = gate.inner_product(ctx, assigned_bytes.clone(), byte_bases); + ctx.constrain_equal(&checksum, &checksum); + + assigned_bytes +} + impl SyncStepCircuit { fn assign_signature( ctx: &mut Context, @@ -555,7 +538,7 @@ mod tests { }; fn load_circuit_args() -> SyncStepArgs { - serde_json::from_slice(&fs::read("../test_data/sync_step.json").unwrap()).unwrap() + serde_json::from_slice(&fs::read("../test_data/sync_step_512.json").unwrap()).unwrap() } #[test] diff --git a/lightclient-circuits/src/witness/hashing.rs b/lightclient-circuits/src/witness/hashing.rs index f345508c..7cc51f50 100644 --- a/lightclient-circuits/src/witness/hashing.rs +++ b/lightclient-circuits/src/witness/hashing.rs @@ -137,8 +137,11 @@ pub struct HashInputChunk { } impl HashInputChunk { - pub fn new(bytes: Vec, is_rlc: bool) -> Self { - Self { bytes, is_rlc } + pub fn new(bytes: Vec) -> Self { + Self { + is_rlc: bytes.len() >= 32, + bytes, + } } pub fn map B>(self, f: F) -> HashInputChunk { diff --git a/lightclient-circuits/tests/step.rs b/lightclient-circuits/tests/step.rs index 3ea74112..23c07c22 100644 --- a/lightclient-circuits/tests/step.rs +++ b/lightclient-circuits/tests/step.rs @@ -348,8 +348,9 @@ fn test_step_mock( #[exclude("deneb*")] path: PathBuf, ) { - const K: u32 = 21; - let params = gen_srs(K); + const K_ROTATION: u32 = 18; + const K_SYNC: u32 = 21; + let params = gen_srs(K_ROTATION); let witness = read_test_files_and_gen_witness(path); let pinning = Eth2ConfigPinning::from_path("./config/sync_step.json");