From 7f858c3ac7f184d7b5b6a877c8ab4d0b4d6ca5d1 Mon Sep 17 00:00:00 2001 From: lighter-zz Date: Mon, 23 Feb 2026 10:29:31 -0500 Subject: [PATCH 1/5] implement poseidon1/2 with state width = 8 --- .gitignore | 2 + plonky2/benches/hashing.rs | 57 ++++++++- plonky2/src/hash/poseidon.rs | 49 ++++++++ plonky2/src/hash/poseidon2/config.rs | 128 +++++++++++++++++++ plonky2/src/hash/poseidon2/hash.rs | 177 ++++++++++++++++++++++++++- plonky2/src/hash/poseidon2/p3.rs | 29 ++++- 6 files changed, 435 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 293a17bb6..00b00f0c6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ pgo-data.profdata # MacOS nuisances .DS_Store + +/plonky2/target/ \ No newline at end of file diff --git a/plonky2/benches/hashing.rs b/plonky2/benches/hashing.rs index 05e8731cb..e4ec71207 100644 --- a/plonky2/benches/hashing.rs +++ b/plonky2/benches/hashing.rs @@ -8,9 +8,12 @@ use p3_symmetric::Permutation; use plonky2::field::goldilocks_field::GoldilocksField; use plonky2::field::types::Sample; use plonky2::hash::hash_types::{BytesHash, RichField}; +use plonky2::hash::hashing::hash_n_to_m_no_pad; use plonky2::hash::keccak::KeccakHash; -use plonky2::hash::poseidon::{Poseidon, SPONGE_WIDTH}; -use plonky2::hash::poseidon2::hash::Poseidon2; +use plonky2::hash::poseidon::{ + Poseidon, PoseidonHash, PoseidonPermutation, SPONGE_RATE, SPONGE_WIDTH, +}; +use plonky2::hash::poseidon2::hash::{Poseidon2, Poseidon2Hash, Poseidon2Permutation}; use plonky2::plonk::config::Hasher; use tynm::type_name; @@ -50,6 +53,54 @@ pub(crate) fn bench_poseidon2(c: &mut Criterion) { ); } +pub(crate) fn bench_poseidon_n_to_4(c: &mut Criterion) { + c.bench_function("poseidon 8-to-4", |b| { + // This calls PoseidonHash::hash_8_to_4, whose underlying permutation state width is 12. + b.iter_batched( + || GoldilocksField::rand_array::(), + |input| PoseidonHash::hash_8_to_4(input), + BatchSize::SmallInput, + ) + }); + + c.bench_function("poseidon 12-to-4", |b| { + // Width-12 sponge path. + b.iter_batched( + || GoldilocksField::rand_array::(), + |input| { + hash_n_to_m_no_pad::>( + &input, 4, + ) + }, + BatchSize::SmallInput, + ) + }); +} + +pub(crate) fn bench_poseidon2_n_to_4(c: &mut Criterion) { + c.bench_function("poseidon2 8-to-4", |b| { + // True width-8 Poseidon2 permutation path (single permutation, truncate to 4). + b.iter_batched( + || GoldilocksField::rand_array::(), + |input| Poseidon2Hash::hash_8_to_4(input), + BatchSize::SmallInput, + ) + }); + + c.bench_function("poseidon2 12-to-4", |b| { + // Width-12 sponge path. + b.iter_batched( + || GoldilocksField::rand_array::(), + |input| { + hash_n_to_m_no_pad::>( + &input, 4, + ) + }, + BatchSize::SmallInput, + ) + }); +} + pub(crate) fn bench_p3_poseidon2(c: &mut Criterion) { const WIDTH: usize = 12; const D: u64 = 7; @@ -111,7 +162,9 @@ pub(crate) fn bench_p3_poseidon2(c: &mut Criterion) { fn criterion_benchmark(c: &mut Criterion) { bench_poseidon::(c); + bench_poseidon_n_to_4(c); bench_poseidon2::(c); + bench_poseidon2_n_to_4(c); bench_p3_poseidon2(c); bench_keccak::(c); } diff --git a/plonky2/src/hash/poseidon.rs b/plonky2/src/hash/poseidon.rs index a7c763252..839593dba 100644 --- a/plonky2/src/hash/poseidon.rs +++ b/plonky2/src/hash/poseidon.rs @@ -9,6 +9,7 @@ use plonky2_field::packed::PackedField; use unroll::unroll_for_loops; use crate::field::extension::{Extendable, FieldExtension}; +use crate::field::goldilocks_field::GoldilocksField as F; use crate::field::types::{Field, PrimeField64}; use crate::gates::gate::Gate; use crate::gates::poseidon::PoseidonGate; @@ -886,6 +887,20 @@ impl Hasher for PoseidonHash { } } +impl PoseidonHash { + #[inline] + pub fn hash_8_to_4(input: [F; SPONGE_RATE]) -> HashOut { + // This is an 8-input / 4-output API over the existing Poseidon permutation in this crate. + // The underlying permutation state width is SPONGE_WIDTH (= 12), not 8. + let mut state = [F::ZERO; SPONGE_WIDTH]; + state[..SPONGE_RATE].copy_from_slice(&input); + let state = F::poseidon(state); + HashOut { + elements: state[..4].try_into().unwrap(), + } + } +} + impl AlgebraicHasher for PoseidonHash { type AlgebraicPermutation = PoseidonPermutation; @@ -956,3 +971,37 @@ pub(crate) mod test_helpers { } } } + +#[cfg(test)] +mod tests { + use core::array; + + use num::{BigUint, One}; + use rand::{thread_rng, RngCore}; + + use super::*; + use crate::hash::hashing::hash_n_to_m_no_pad; + + #[test] + fn test_poseidon_hash_8_to_4_matches_sponge() { + let mut rng = thread_rng(); + for _ in 0..128 { + let input = array::from_fn(|_| F::from_noncanonical_u64(rng.next_u64())); + let got = PoseidonHash::hash_8_to_4(input); + let expected = hash_n_to_m_no_pad::>(&input, 4); + assert_eq!(got.elements, expected.as_slice()); + } + } + + #[test] + fn test_poseidon_hash_8_to_4_edge_cases() { + let max = F::from_noncanonical_biguint(F::order() - BigUint::one()); + let cases: [[F; 8]; 3] = [[F::ZERO; 8], [F::ONE; 8], [max; 8]]; + + for input in cases { + let got = PoseidonHash::hash_8_to_4(input); + let expected = hash_n_to_m_no_pad::>(&input, 4); + assert_eq!(got.elements, expected.as_slice()); + } + } +} diff --git a/plonky2/src/hash/poseidon2/config.rs b/plonky2/src/hash/poseidon2/config.rs index 96f25099b..b6c8671dd 100644 --- a/plonky2/src/hash/poseidon2/config.rs +++ b/plonky2/src/hash/poseidon2/config.rs @@ -165,3 +165,131 @@ pub const MATRIX_DIAG_12_U64: [u64; WIDTH] = [ 0x0c6388b51545e883, 0xd27dbb6944917b60, ]; + +pub const WIDTH_8: usize = 8; +pub const OUT_8: usize = 4; +pub const ROUNDS_F_8: usize = 8; +pub const ROUNDS_F_HALF_8: usize = 4; +pub const ROUNDS_P_8: usize = 22; + +/// Taken from Plonky3 Poseidon2 Goldilocks implementation. +pub const EXTERNAL_CONSTANTS_8: [[u64; WIDTH_8]; ROUNDS_F_8] = [ + [ + 0xdd5743e7f2a5a5d9, + 0xcb3a864e58ada44b, + 0xffa2449ed32f8cdc, + 0x42025f65d6bd13ee, + 0x7889175e25506323, + 0x34b98bb03d24b737, + 0xbdcc535ecc4faa2a, + 0x5b20ad869fc0d033, + ], + [ + 0xf1dda5b9259dfcb4, + 0x27515210be112d59, + 0x4227d1718c766c3f, + 0x26d333161a5bd794, + 0x49b938957bf4b026, + 0x4a56b5938b213669, + 0x1120426b48c8353d, + 0x6b323c3f10a56cad, + ], + [ + 0xce57d6245ddca6b2, + 0xb1fc8d402bba1eb1, + 0xb5c5096ca959bd04, + 0x6db55cd306d31f7f, + 0xc49d293a81cb9641, + 0x1ce55a4fe979719f, + 0xa92e60a9d178a4d1, + 0x002cc64973bcfd8c, + ], + [ + 0xcea721cce82fb11b, + 0xe5b55eb8098ece81, + 0x4e30525c6f1ddd66, + 0x43c6702827070987, + 0xaca68430a7b5762a, + 0x3674238634df9c93, + 0x88cee1c825e33433, + 0xde99ae8d74b57176, + ], + [ + 0x014ef1197d341346, + 0x9725e20825d07394, + 0xfdb25aef2c5bae3b, + 0xbe5402dc598c971e, + 0x93a5711f04cdca3d, + 0xc45a9a5b2f8fb97b, + 0xfe8946a924933545, + 0x2af997a27369091c, + ], + [ + 0xaa62c88e0b294011, + 0x058eb9d810ce9f74, + 0xb3cb23eced349ae4, + 0xa3648177a77b4a84, + 0x43153d905992d95d, + 0xf4e2a97cda44aa4b, + 0x5baa2702b908682f, + 0x082923bdf4f750d1, + ], + [ + 0x98ae09a325893803, + 0xf8a6475077968838, + 0xceb0735bf00b2c5f, + 0x0a1a5d953888e072, + 0x2fcb190489f94475, + 0xb5be06270dec69fc, + 0x739cb934b09acf8b, + 0x537750b75ec7f25b, + ], + [ + 0xe9dd318bae1f3961, + 0xf7462137299efe1a, + 0xb1f6b8eee9adb940, + 0xbdebcc8a809dfe6b, + 0x40fc1f791b178113, + 0x3ac1c3362d014864, + 0x9a016184bdb8aeba, + 0x95f2394459fbc25e, + ], +]; + +/// Taken from Plonky3 Poseidon2 Goldilocks implementation. +pub const INTERNAL_CONSTANTS_8: [u64; ROUNDS_P_8] = [ + 0x488897d85ff51f56, + 0x1140737ccb162218, + 0xa7eeb9215866ed35, + 0x9bd2976fee49fcc9, + 0xc0c8f0de580a3fcc, + 0x4fb2dae6ee8fc793, + 0x343a89f35f37395b, + 0x223b525a77ca72c8, + 0x56ccb62574aaa918, + 0xc4d507d8027af9ed, + 0xa080673cf0b7e95c, + 0xf0184884eb70dcf8, + 0x044f10b0cb3d5c69, + 0xe9e3f7993938f186, + 0x1b761c80e772f459, + 0x606cec607a1b5fac, + 0x14a0c2e1d45f03cd, + 0x4eace8855398574f, + 0xf905ca7103eff3e6, + 0xf8c8f8d20862c059, + 0xb524fe8bdd678e5a, + 0xfbb7865901a1ec41, +]; + +/// Taken from Plonky3 Poseidon2 implementation. +pub const MATRIX_DIAG_8_U64: [u64; WIDTH_8] = [ + 0xa98811a1fed4e3a5, + 0x1cc48b54f377e2a0, + 0xe40cd4f6c5609a26, + 0x11de79ebca97a4a3, + 0x9177c73d8b7e929c, + 0x2a6fe8085797e791, + 0x3de6e93329f8d5ad, + 0x3f7af9125da962fe, +]; diff --git a/plonky2/src/hash/poseidon2/hash.rs b/plonky2/src/hash/poseidon2/hash.rs index d6fb72bfd..4156c9770 100644 --- a/plonky2/src/hash/poseidon2/hash.rs +++ b/plonky2/src/hash/poseidon2/hash.rs @@ -8,7 +8,7 @@ use crate::field::goldilocks_field::GoldilocksField as F; use crate::field::types::{Field, PrimeField64}; use crate::gates::poseidon2::Poseidon2Gate; use crate::hash::hash_types::{HashOut, RichField}; -use crate::hash::hashing::{compress, hash_n_to_hash_no_pad, PlonkyPermutation}; +use crate::hash::hashing::{hash_n_to_hash_no_pad, PlonkyPermutation}; use crate::iop::ext_target::ExtensionTarget; use crate::iop::target::{BoolTarget, Target}; use crate::plonk::circuit_builder::CircuitBuilder; @@ -342,6 +342,105 @@ fn external_linear_layer_u128(state: &mut [u128; WIDTH]) { } } +#[inline] +#[unroll::unroll_for_loops] +fn poseidon2_width_8(input: [P; WIDTH_8]) -> [P; WIDTH_8] { + let mut state = input; + + external_linear_layer_hl_8(&mut state); + + for r in 0..ROUNDS_F_HALF_8 { + add_rc_8(&mut state, r); + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + for &rc in INTERNAL_CONSTANTS_8.iter() { + state[0] += P::from_canonical_u64(rc); + state[0] = P::sbox_p(&state[0]); + internal_linear_layer_8(&mut state); + } + + for r in ROUNDS_F_HALF_8..ROUNDS_F_8 { + add_rc_8(&mut state, r); + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + state +} + +#[inline] +#[unroll::unroll_for_loops] +fn add_rc_8(state: &mut [P; WIDTH_8], round: usize) { + for i in 0..WIDTH_8 { + state[i] = unsafe { state[i].add_canonical_u64(EXTERNAL_CONSTANTS_8[round][i]) }; + } +} + +#[inline] +#[unroll::unroll_for_loops] +fn sbox_8(state: &mut [P; WIDTH_8]) { + for item in state.iter_mut() { + *item = P::sbox_p(item); + } +} + +#[inline] +#[unroll::unroll_for_loops] +fn internal_linear_layer_8(state: &mut [P; WIDTH_8]) { + let sum = sum_8(state); + for i in 0..WIDTH_8 { + state[i] = sum.multiply_accumulate(state[i], P::from_canonical_u64(MATRIX_DIAG_8_U64[i])); + } +} + +#[inline] +#[unroll::unroll_for_loops] +fn external_linear_layer_hl_8(state: &mut [P; WIDTH_8]) { + let mut state_u128 = state.map(|x| x.to_noncanonical_u64() as u128); + external_linear_layer_hl_u128_8(&mut state_u128); + for i in 0..WIDTH_8 { + state[i] = P::from_noncanonical_u128_with_96_bits(state_u128[i]); + } +} + +#[inline] +#[unroll::unroll_for_loops] +fn external_linear_layer_hl_u128_8(state: &mut [u128; WIDTH_8]) { + // Horizon Labs 4x4 matrix: + // [ 5 7 1 3 ] + // [ 4 6 1 1 ] + // [ 1 3 5 7 ] + // [ 1 1 4 6 ] + for i in (0..WIDTH_8).step_by(4) { + let t0 = state[i] + state[i + 1]; + let t1 = state[i + 2] + state[i + 3]; + let t2 = state[i + 1] + state[i + 1] + t1; + let t3 = state[i + 3] + state[i + 3] + t0; + let t4 = t1 + t1 + t1 + t1 + t3; + let t5 = t0 + t0 + t0 + t0 + t2; + let t6 = t3 + t5; + let t7 = t2 + t4; + + state[i] = t6; + state[i + 1] = t5; + state[i + 2] = t7; + state[i + 3] = t4; + } + + let sums = [ + state[0] + state[4], + state[1] + state[5], + state[2] + state[6], + state[3] + state[7], + ]; + + for i in 0..WIDTH_8 { + state[i] += sums[i % 4]; + } +} + impl Poseidon2 for F { #[inline] fn sbox_p(a: &Self) -> Self { @@ -493,6 +592,22 @@ fn sum_12(inputs: &[F]) -> F { F::from_noncanonical_u128_with_96_bits(tmp) } +#[inline] +/// Sum of 8 elements to u128; unrolled for performance. +fn sum_8(inputs: &[F]) -> F { + debug_assert!(inputs.len() == 8); + let tmp = inputs[0].to_noncanonical_u64() as u128 + + inputs[1].to_noncanonical_u64() as u128 + + inputs[2].to_noncanonical_u64() as u128 + + inputs[3].to_noncanonical_u64() as u128 + + inputs[4].to_noncanonical_u64() as u128 + + inputs[5].to_noncanonical_u64() as u128 + + inputs[6].to_noncanonical_u64() as u128 + + inputs[7].to_noncanonical_u64() as u128; + + F::from_noncanonical_u128_with_96_bits(tmp) +} + /// Poseidon2 hash function. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Poseidon2Hash; @@ -506,11 +621,23 @@ impl Hasher for Poseidon2Hash { } fn two_to_one(left: Self::Hash, right: Self::Hash) -> Self::Hash { - compress::(left, right) + let mut input = [F::ZERO; RATE]; + input[..OUT_8].copy_from_slice(&left.elements); + input[OUT_8..].copy_from_slice(&right.elements); + Self::hash_8_to_4(input) } } impl Poseidon2Hash { + #[inline] + pub fn hash_8_to_4(input: [P; RATE]) -> HashOut

{ + // Width-8 Poseidon2 permutation (single permutation, no padding), then truncate to 4. + let state = poseidon2_width_8(input); + HashOut { + elements: state[..OUT_8].try_into().unwrap(), + } + } + #[inline] #[unroll::unroll_for_loops] pub fn hash_n_to_one( @@ -567,6 +694,8 @@ impl AlgebraicHasher for Poseidon2Hash { #[cfg(test)] mod test { + use core::array; + use anyhow::Result; use num::{BigUint, One}; use p3_field::{AbstractField, PrimeField64 as _}; @@ -576,7 +705,9 @@ mod test { use super::*; use crate::field::types::PrimeField64; use crate::hash::hashing::hash_n_to_m_no_pad; - use crate::hash::poseidon2::p3::p3_poseidon2_hash_n_to_m_no_pad; + use crate::hash::poseidon2::p3::{ + p3_poseidon2_hash_n_to_m_no_pad, p3_poseidon2_permute_8_to_4, + }; use crate::iop::witness::{PartialWitness, WitnessWrite}; use crate::plonk::circuit_data::CircuitConfig; use crate::plonk::config::PoseidonGoldilocksConfig; @@ -599,7 +730,7 @@ mod test { .collect::>(); let expected_output_f3 = p3_poseidon2_hash_n_to_m_no_pad(&input_f3, 12); - for i in 0..4 { + for i in 0..12 { assert_eq!( expected_output_f[i].to_canonical_u64(), expected_output_f3[i].as_canonical_u64() @@ -607,6 +738,44 @@ mod test { } } + #[test] + fn test_poseidon2_hash_8_to_4_with_plonky3() { + let mut rng = thread_rng(); + for _ in 0..128 { + let input = array::from_fn(|_| F::from_noncanonical_u64(rng.next_u64())); + + let expected = Poseidon2Hash::hash_8_to_4(input); + let input_p3 = input.map(|x| Goldilocks::from_canonical_u64(x.to_canonical_u64())); + let got_p3 = p3_poseidon2_permute_8_to_4(input_p3); + + for i in 0..4 { + assert_eq!( + expected.elements[i].to_canonical_u64(), + got_p3[i].as_canonical_u64() + ); + } + } + } + + #[test] + fn test_poseidon2_hash_8_to_4_edge_cases_with_plonky3() { + let max = F::from_noncanonical_biguint(F::order() - BigUint::one()); + let cases: [[F; 8]; 3] = [[F::ZERO; 8], [F::ONE; 8], [max; 8]]; + + for input in cases { + let expected = Poseidon2Hash::hash_8_to_4(input); + let input_p3 = input.map(|x| Goldilocks::from_canonical_u64(x.to_canonical_u64())); + let got_p3 = p3_poseidon2_permute_8_to_4(input_p3); + + for i in 0..4 { + assert_eq!( + expected.elements[i].to_canonical_u64(), + got_p3[i].as_canonical_u64() + ); + } + } + } + #[test] fn test_poseidon2_gate() -> Result<()> { let mut rng = thread_rng(); diff --git a/plonky2/src/hash/poseidon2/p3.rs b/plonky2/src/hash/poseidon2/p3.rs index 345bce362..71ab67c57 100644 --- a/plonky2/src/hash/poseidon2/p3.rs +++ b/plonky2/src/hash/poseidon2/p3.rs @@ -1,6 +1,6 @@ use p3_field::AbstractField; use p3_goldilocks::{DiffusionMatrixGoldilocks, Goldilocks}; -use p3_poseidon2::{Poseidon2, Poseidon2ExternalMatrixGeneral}; +use p3_poseidon2::{Poseidon2, Poseidon2ExternalMatrixGeneral, Poseidon2ExternalMatrixHL}; use p3_symmetric::Permutation; use super::config::*; @@ -65,3 +65,30 @@ pub fn p3_poseidon2_hash_n_to_m_no_pad( poseidon.permute_mut(&mut perm); } } + +pub fn p3_poseidon2_permute_8_to_4(input: [Goldilocks; WIDTH_8]) -> [Goldilocks; OUT_8] { + let poseidon = Poseidon2::< + Goldilocks, + Poseidon2ExternalMatrixHL, + DiffusionMatrixGoldilocks, + WIDTH_8, + D, + >::new( + ROUNDS_F_8, + EXTERNAL_CONSTANTS_8 + .iter() + .map(|v| v.map(Goldilocks::from_canonical_u64)) + .collect::>(), + Poseidon2ExternalMatrixHL, + ROUNDS_P_8, + INTERNAL_CONSTANTS_8 + .iter() + .map(|&x| Goldilocks::from_canonical_u64(x)) + .collect::>(), + DiffusionMatrixGoldilocks, + ); + + let mut state = input; + poseidon.permute_mut(&mut state); + state[..OUT_8].try_into().unwrap() +} From a38fabb46ffd6b805be823984a7936f8532c6668 Mon Sep 17 00:00:00 2001 From: lighter-zz Date: Mon, 23 Feb 2026 12:01:05 -0500 Subject: [PATCH 2/5] fix recursion --- plonky2/src/hash/merkle_proofs.rs | 45 +-------- plonky2/src/hash/poseidon2/hash.rs | 151 ++++++++++++++++++++++++++++- plonky2/src/plonk/config.rs | 27 +++++- 3 files changed, 179 insertions(+), 44 deletions(-) diff --git a/plonky2/src/hash/merkle_proofs.rs b/plonky2/src/hash/merkle_proofs.rs index 74963a2a4..06c05ad71 100644 --- a/plonky2/src/hash/merkle_proofs.rs +++ b/plonky2/src/hash/merkle_proofs.rs @@ -151,25 +151,12 @@ impl, const D: usize> CircuitBuilder { ) { debug_assert!(H::AlgebraicPermutation::RATE >= NUM_HASH_OUT_ELTS); - let zero = self.zero(); let mut state: HashOutTarget = self.hash_or_noop::(leaf_data); debug_assert_eq!(state.elements.len(), NUM_HASH_OUT_ELTS); for (&bit, &sibling) in leaf_index_bits.iter().zip(&proof.siblings) { debug_assert_eq!(sibling.elements.len(), NUM_HASH_OUT_ELTS); - - let mut perm_inputs = H::AlgebraicPermutation::default(); - perm_inputs.set_from_slice(&state.elements, 0); - perm_inputs.set_from_slice(&sibling.elements, NUM_HASH_OUT_ELTS); - // Ensure the rest of the state, if any, is zero: - perm_inputs.set_from_iter(core::iter::repeat(zero), 2 * NUM_HASH_OUT_ELTS); - let perm_outs = self.permute_swapped::(perm_inputs, bit); - let hash_outs = perm_outs.squeeze()[0..NUM_HASH_OUT_ELTS] - .try_into() - .unwrap(); - state = HashOutTarget { - elements: hash_outs, - }; + state = H::two_to_one_swapped(state, sibling, bit, self); } for i in 0..NUM_HASH_OUT_ELTS { @@ -196,7 +183,6 @@ impl, const D: usize> CircuitBuilder { ) { debug_assert!(H::AlgebraicPermutation::RATE >= NUM_HASH_OUT_ELTS); - let zero = self.zero(); let mut state: HashOutTarget = self.hash_or_noop::(leaf_data); debug_assert_eq!(state.elements.len(), NUM_HASH_OUT_ELTS); @@ -205,19 +191,7 @@ impl, const D: usize> CircuitBuilder { for (&bit, &sibling) in leaf_index_bits.iter().zip(&proof.siblings) { debug_assert_eq!(sibling.elements.len(), NUM_HASH_OUT_ELTS); - - let mut perm_inputs = H::AlgebraicPermutation::default(); - perm_inputs.set_from_slice(&state.elements, 0); - perm_inputs.set_from_slice(&sibling.elements, NUM_HASH_OUT_ELTS); - // Ensure the rest of the state, if any, is zero: - perm_inputs.set_from_iter(core::iter::repeat(zero), 2 * NUM_HASH_OUT_ELTS); - let perm_outs = self.permute_swapped::(perm_inputs, bit); - let hash_outs = perm_outs.squeeze()[0..NUM_HASH_OUT_ELTS] - .try_into() - .unwrap(); - state = HashOutTarget { - elements: hash_outs, - }; + state = H::two_to_one_swapped(state, sibling, bit, self); // Store state at specific indices for n in 0..num_log_n - 1 { final_states[n] = final_states[n + 1]; @@ -251,7 +225,6 @@ impl, const D: usize> CircuitBuilder { ) { debug_assert!(H::AlgebraicPermutation::RATE >= NUM_HASH_OUT_ELTS); - let zero = self.zero(); let mut state: HashOutTarget = self.hash_or_noop::(leaf_data[0].clone()); debug_assert_eq!(state.elements.len(), NUM_HASH_OUT_ELTS); @@ -259,19 +232,7 @@ impl, const D: usize> CircuitBuilder { let mut leaf_data_index = 1; for (&bit, &sibling) in leaf_index_bits.iter().zip(&proof.siblings) { debug_assert_eq!(sibling.elements.len(), NUM_HASH_OUT_ELTS); - - let mut perm_inputs = H::AlgebraicPermutation::default(); - perm_inputs.set_from_slice(&state.elements, 0); - perm_inputs.set_from_slice(&sibling.elements, NUM_HASH_OUT_ELTS); - // Ensure the rest of the state, if any, is zero: - perm_inputs.set_from_iter(core::iter::repeat(zero), 2 * NUM_HASH_OUT_ELTS); - let perm_outs = self.permute_swapped::(perm_inputs, bit); - let hash_outs = perm_outs.squeeze()[0..NUM_HASH_OUT_ELTS] - .try_into() - .unwrap(); - state = HashOutTarget { - elements: hash_outs, - }; + state = H::two_to_one_swapped(state, sibling, bit, self); current_height -= 1; if leaf_data_index < leaf_heights.len() diff --git a/plonky2/src/hash/poseidon2/hash.rs b/plonky2/src/hash/poseidon2/hash.rs index 4156c9770..9e6772aed 100644 --- a/plonky2/src/hash/poseidon2/hash.rs +++ b/plonky2/src/hash/poseidon2/hash.rs @@ -7,7 +7,7 @@ use crate::field::extension::{Extendable, FieldExtension}; use crate::field::goldilocks_field::GoldilocksField as F; use crate::field::types::{Field, PrimeField64}; use crate::gates::poseidon2::Poseidon2Gate; -use crate::hash::hash_types::{HashOut, RichField}; +use crate::hash::hash_types::{HashOut, HashOutTarget, RichField}; use crate::hash::hashing::{hash_n_to_hash_no_pad, PlonkyPermutation}; use crate::iop::ext_target::ExtensionTarget; use crate::iop::target::{BoolTarget, Target}; @@ -441,6 +441,143 @@ fn external_linear_layer_hl_u128_8(state: &mut [u128; WIDTH_8]) { } } +#[inline] +fn poseidon2_compress_8_to_4_circuit( + builder: &mut CircuitBuilder, + left: HashOutTarget, + right: HashOutTarget, + swap: BoolTarget, +) -> HashOutTarget +where + P: RichField + Extendable, +{ + let zero = builder.zero(); + let mut state = [zero; WIDTH_8]; + + for i in 0..OUT_8 { + state[i] = builder.select(swap, right.elements[i], left.elements[i]); + state[i + OUT_8] = builder.select(swap, left.elements[i], right.elements[i]); + } + + external_linear_layer_hl_8_circuit(builder, &mut state); + + for r in 0..ROUNDS_F_HALF_8 { + add_rc_8_circuit(builder, &mut state, r); + sbox_8_circuit(builder, &mut state); + external_linear_layer_hl_8_circuit(builder, &mut state); + } + + for &rc in INTERNAL_CONSTANTS_8.iter() { + state[0] = builder.add_const(state[0], P::from_canonical_u64(rc)); + state[0] = sbox_p_circuit(builder, state[0]); + internal_linear_layer_8_circuit(builder, &mut state); + } + + for r in ROUNDS_F_HALF_8..ROUNDS_F_8 { + add_rc_8_circuit(builder, &mut state, r); + sbox_8_circuit(builder, &mut state); + external_linear_layer_hl_8_circuit(builder, &mut state); + } + + HashOutTarget { + elements: state[..OUT_8].try_into().unwrap(), + } +} + +#[inline] +fn sbox_p_circuit(builder: &mut CircuitBuilder, x: Target) -> Target +where + P: RichField + Extendable, +{ + let x2 = builder.mul(x, x); + let x4 = builder.mul(x2, x2); + let x3 = builder.mul(x2, x); + builder.mul(x3, x4) +} + +#[inline] +fn sbox_8_circuit( + builder: &mut CircuitBuilder, + state: &mut [Target; WIDTH_8], +) +where + P: RichField + Extendable, +{ + for item in state.iter_mut() { + *item = sbox_p_circuit(builder, *item); + } +} + +#[inline] +fn add_rc_8_circuit( + builder: &mut CircuitBuilder, + state: &mut [Target; WIDTH_8], + round: usize, +) where + P: RichField + Extendable, +{ + for i in 0..WIDTH_8 { + state[i] = builder.add_const(state[i], P::from_canonical_u64(EXTERNAL_CONSTANTS_8[round][i])); + } +} + +#[inline] +fn internal_linear_layer_8_circuit( + builder: &mut CircuitBuilder, + state: &mut [Target; WIDTH_8], +) where + P: RichField + Extendable, +{ + let sum = builder.add_many(state.iter().copied()); + for i in 0..WIDTH_8 { + state[i] = + builder.mul_const_add(P::from_canonical_u64(MATRIX_DIAG_8_U64[i]), state[i], sum); + } +} + +#[inline] +fn external_linear_layer_hl_8_circuit( + builder: &mut CircuitBuilder, + state: &mut [Target; WIDTH_8], +) where + P: RichField + Extendable, +{ + for i in (0..WIDTH_8).step_by(4) { + let t0 = builder.add(state[i], state[i + 1]); + let t1 = builder.add(state[i + 2], state[i + 3]); + let two_x1 = builder.add(state[i + 1], state[i + 1]); + let two_x3 = builder.add(state[i + 3], state[i + 3]); + let t2 = builder.add(two_x1, t1); + let t3 = builder.add(two_x3, t0); + + let two_t1 = builder.add(t1, t1); + let four_t1 = builder.add(two_t1, two_t1); + let t4 = builder.add(four_t1, t3); + + let two_t0 = builder.add(t0, t0); + let four_t0 = builder.add(two_t0, two_t0); + let t5 = builder.add(four_t0, t2); + + let t6 = builder.add(t3, t5); + let t7 = builder.add(t2, t4); + state[i] = t6; + state[i + 1] = t5; + state[i + 2] = t7; + state[i + 3] = t4; + } + + let sums = [ + builder.add(state[0], state[4]), + builder.add(state[1], state[5]), + builder.add(state[2], state[6]), + builder.add(state[3], state[7]), + ]; + + for i in 0..WIDTH_8 { + state[i] = builder.add(state[i], sums[i % 4]); + } +} + impl Poseidon2 for F { #[inline] fn sbox_p(a: &Self) -> Self { @@ -690,6 +827,18 @@ impl AlgebraicHasher for Poseidon2Hash { (0..WIDTH).map(|i| Target::wire(gate, Poseidon2Gate::::wire_output(i))), ) } + + fn two_to_one_swapped( + left: HashOutTarget, + right: HashOutTarget, + swap: BoolTarget, + builder: &mut CircuitBuilder, + ) -> HashOutTarget + where + F: RichField + Extendable, + { + poseidon2_compress_8_to_4_circuit(builder, left, right, swap) + } } #[cfg(test)] diff --git a/plonky2/src/plonk/config.rs b/plonky2/src/plonk/config.rs index 4bbcbc4bc..1e4525ac5 100644 --- a/plonky2/src/plonk/config.rs +++ b/plonky2/src/plonk/config.rs @@ -16,7 +16,7 @@ use serde::Serialize; use crate::field::extension::quadratic::QuadraticExtension; use crate::field::extension::{Extendable, FieldExtension}; use crate::field::goldilocks_field::GoldilocksField; -use crate::hash::hash_types::{HashOut, RichField}; +use crate::hash::hash_types::{HashOut, HashOutTarget, RichField, NUM_HASH_OUT_ELTS}; use crate::hash::hashing::PlonkyPermutation; use crate::hash::keccak::KeccakHash; use crate::hash::poseidon::PoseidonHash; @@ -90,6 +90,31 @@ pub trait AlgebraicHasher: Hasher> { ) -> Self::AlgebraicPermutation where F: RichField + Extendable; + + /// Conditionally swap left/right hash inputs, then hash them into one hash output. + /// + /// By default this is implemented via `permute_swapped` over the hasher permutation. + /// Hashers with custom compression behavior (different from generic permutation squeezing) + /// can override this. + fn two_to_one_swapped( + left: HashOutTarget, + right: HashOutTarget, + swap: BoolTarget, + builder: &mut CircuitBuilder, + ) -> HashOutTarget + where + F: RichField + Extendable, + { + let zero = builder.zero(); + let mut perm_inputs = Self::AlgebraicPermutation::default(); + perm_inputs.set_from_slice(&left.elements, 0); + perm_inputs.set_from_slice(&right.elements, NUM_HASH_OUT_ELTS); + perm_inputs.set_from_iter(core::iter::repeat(zero), 2 * NUM_HASH_OUT_ELTS); + let perm_outs = Self::permute_swapped(perm_inputs, swap, builder); + HashOutTarget { + elements: perm_outs.squeeze()[0..NUM_HASH_OUT_ELTS].try_into().unwrap(), + } + } } /// Generic configuration trait. From c1a6808d08bac0256451460d6cda810fa117462b Mon Sep 17 00:00:00 2001 From: lighter-zz Date: Thu, 26 Feb 2026 10:35:22 -0500 Subject: [PATCH 3/5] dedicated gate --- plonky2/src/gates/mod.rs | 1 + plonky2/src/gates/poseidon2_8.rs | 825 +++++++++++++++++++++++++++++ plonky2/src/hash/poseidon2/hash.rs | 50 +- plonky2/src/plonk/config.rs | 4 +- 4 files changed, 875 insertions(+), 5 deletions(-) create mode 100644 plonky2/src/gates/poseidon2_8.rs diff --git a/plonky2/src/gates/mod.rs b/plonky2/src/gates/mod.rs index c1cb1e2ab..5440d97a3 100644 --- a/plonky2/src/gates/mod.rs +++ b/plonky2/src/gates/mod.rs @@ -40,6 +40,7 @@ pub mod noop; pub mod packed_util; pub mod poseidon; pub mod poseidon2; +pub mod poseidon2_8; pub mod poseidon_mds; pub mod public_input; pub mod random_access; diff --git a/plonky2/src/gates/poseidon2_8.rs b/plonky2/src/gates/poseidon2_8.rs new file mode 100644 index 000000000..b84820208 --- /dev/null +++ b/plonky2/src/gates/poseidon2_8.rs @@ -0,0 +1,825 @@ +// Copyright (c) Elliot Technologies, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +use core::marker::PhantomData; + +use anyhow::Result; + +use crate::field::extension::Extendable; +use crate::field::types::Field; +use crate::gates::gate::Gate; +use crate::gates::util::StridedConstraintConsumer; +use crate::hash::hash_types::{HashOutTarget, RichField}; +use crate::hash::poseidon2::config::{ + EXTERNAL_CONSTANTS_8, INTERNAL_CONSTANTS_8, MATRIX_DIAG_8_U64, OUT_8, ROUNDS_F_8, + ROUNDS_F_HALF_8, ROUNDS_P_8, WIDTH_8, +}; +use crate::hash::poseidon2::hash::Poseidon2; +use crate::iop::ext_target::ExtensionTarget; +use crate::iop::generator::{GeneratedValues, SimpleGenerator, WitnessGeneratorRef}; +use crate::iop::target::{BoolTarget, Target}; +use crate::iop::wire::Wire; +use crate::iop::witness::{PartitionWitness, Witness, WitnessWrite}; +use crate::plonk::circuit_builder::CircuitBuilder; +use crate::plonk::circuit_data::CommonCircuitData; +use crate::plonk::vars::{EvaluationTargets, EvaluationVars, EvaluationVarsBase}; +use crate::util::serialization::{Buffer, IoResult, Read, Write}; + +#[derive(Debug, Default)] +pub struct Poseidon2Gate8, const D: usize> { + _phantom: PhantomData, +} + +impl + Poseidon2, const D: usize> Poseidon2Gate8 { + pub fn new() -> Self { + Self { + _phantom: PhantomData, + } + } + + pub fn wire_input(i: usize) -> usize { + i + } + + pub fn wire_output(i: usize) -> usize { + WIDTH_8 + i + } + + pub const WIRE_SWAP: usize = 2 * WIDTH_8; + const START_DELTA: usize = 2 * WIDTH_8 + 1; + + fn wire_delta(i: usize) -> usize { + debug_assert!(i < 4); + Self::START_DELTA + i + } + + const START_ROUND_F_BEGIN: usize = Self::START_DELTA + 4; + + fn wire_full_sbox_0(round: usize, i: usize) -> usize { + debug_assert!(round != 0); + debug_assert!(round < ROUNDS_F_HALF_8); + debug_assert!(i < WIDTH_8); + Self::START_ROUND_F_BEGIN + WIDTH_8 * (round - 1) + i + } + + const START_PARTIAL: usize = Self::START_ROUND_F_BEGIN + WIDTH_8 * (ROUNDS_F_HALF_8 - 1); + + const fn wire_partial_sbox(round: usize) -> usize { + debug_assert!(round < ROUNDS_P_8); + Self::START_PARTIAL + round + } + + const START_ROUND_F_END: usize = Self::START_PARTIAL + ROUNDS_P_8; + + const fn wire_full_sbox_1(round: usize, i: usize) -> usize { + debug_assert!(round < ROUNDS_F_HALF_8); + debug_assert!(i < WIDTH_8); + Self::START_ROUND_F_END + WIDTH_8 * round + i + } + + const fn end() -> usize { + Self::START_ROUND_F_END + WIDTH_8 * ROUNDS_F_HALF_8 + } +} + +fn sbox_p(x: P) -> P { + let x2 = x.square(); + let x4 = x2.square(); + x * x2 * x4 +} + +fn sbox_8(state: &mut [P; WIDTH_8]) { + for v in state.iter_mut() { + *v = sbox_p(*v); + } +} + +fn add_rc_8(state: &mut [P; WIDTH_8], round: usize) { + for i in 0..WIDTH_8 { + state[i] += P::from_canonical_u64(EXTERNAL_CONSTANTS_8[round][i]); + } +} + +fn external_linear_layer_hl_8(state: &mut [P; WIDTH_8]) { + for i in (0..WIDTH_8).step_by(4) { + let t0 = state[i] + state[i + 1]; + let t1 = state[i + 2] + state[i + 3]; + let t2 = state[i + 1] + state[i + 1] + t1; + let t3 = state[i + 3] + state[i + 3] + t0; + let t4 = t1 + t1 + t1 + t1 + t3; + let t5 = t0 + t0 + t0 + t0 + t2; + let t6 = t3 + t5; + let t7 = t2 + t4; + + state[i] = t6; + state[i + 1] = t5; + state[i + 2] = t7; + state[i + 3] = t4; + } + + let sums = [ + state[0] + state[4], + state[1] + state[5], + state[2] + state[6], + state[3] + state[7], + ]; + for i in 0..WIDTH_8 { + state[i] += sums[i % 4]; + } +} + +fn external_linear_layer_hl_8_circuit, const D: usize>( + builder: &mut CircuitBuilder, + state: &mut [ExtensionTarget; WIDTH_8], +) { + for i in (0..WIDTH_8).step_by(4) { + let t0 = builder.add_extension(state[i], state[i + 1]); + let t1 = builder.add_extension(state[i + 2], state[i + 3]); + + let s1_twice = builder.add_extension(state[i + 1], state[i + 1]); + let t2 = builder.add_extension(s1_twice, t1); + + let s3_twice = builder.add_extension(state[i + 3], state[i + 3]); + let t3 = builder.add_extension(s3_twice, t0); + + let t1_2 = builder.add_extension(t1, t1); + let t1_4 = builder.add_extension(t1_2, t1_2); + let t4 = builder.add_extension(t1_4, t3); + + let t0_2 = builder.add_extension(t0, t0); + let t0_4 = builder.add_extension(t0_2, t0_2); + let t5 = builder.add_extension(t0_4, t2); + + let t6 = builder.add_extension(t3, t5); + let t7 = builder.add_extension(t2, t4); + + state[i] = t6; + state[i + 1] = t5; + state[i + 2] = t7; + state[i + 3] = t4; + } + + let sums = [ + builder.add_extension(state[0], state[4]), + builder.add_extension(state[1], state[5]), + builder.add_extension(state[2], state[6]), + builder.add_extension(state[3], state[7]), + ]; + + for i in 0..WIDTH_8 { + state[i] = builder.add_extension(state[i], sums[i % 4]); + } +} + +fn internal_linear_layer_8(state: &mut [P; WIDTH_8]) { + let sum = state.iter().copied().sum::

(); + for i in 0..WIDTH_8 { + state[i] = sum.multiply_accumulate(state[i], P::from_canonical_u64(MATRIX_DIAG_8_U64[i])); + } +} + +fn internal_linear_layer_8_circuit, const D: usize>( + builder: &mut CircuitBuilder, + state: &mut [ExtensionTarget; WIDTH_8], +) { + let sum = state + .iter() + .copied() + .reduce(|acc, t| builder.add_extension(acc, t)) + .unwrap(); + for i in 0..WIDTH_8 { + state[i] = builder.mul_const_add_extension( + F::from_canonical_u64(MATRIX_DIAG_8_U64[i]), + state[i], + sum, + ); + } +} + +fn sbox_p_circuit, const D: usize>( + builder: &mut CircuitBuilder, + x: ExtensionTarget, +) -> ExtensionTarget { + let x2 = builder.mul_extension(x, x); + let x4 = builder.mul_extension(x2, x2); + let x3 = builder.mul_extension(x2, x); + builder.mul_extension(x3, x4) +} + +fn sbox_8_circuit, const D: usize>( + builder: &mut CircuitBuilder, + state: &mut [ExtensionTarget; WIDTH_8], +) { + for v in state.iter_mut() { + *v = sbox_p_circuit(builder, *v); + } +} + +fn add_rc_8_circuit, const D: usize>( + builder: &mut CircuitBuilder, + state: &mut [ExtensionTarget; WIDTH_8], + round: usize, +) { + for i in 0..WIDTH_8 { + let rc = builder.constant_extension(F::Extension::from_canonical_u64( + EXTERNAL_CONSTANTS_8[round][i], + )); + state[i] = builder.add_extension(state[i], rc); + } +} + +pub fn poseidon2_compress_8_to_4_swapped< + F: RichField + Extendable + Poseidon2, + const D: usize, +>( + builder: &mut CircuitBuilder, + left: HashOutTarget, + right: HashOutTarget, + swap: BoolTarget, +) -> HashOutTarget { + let gate = builder.add_gate(Poseidon2Gate8::::new(), vec![]); + let swap_wire = Target::wire(gate, Poseidon2Gate8::::WIRE_SWAP); + builder.connect(swap.target, swap_wire); + + for i in 0..OUT_8 { + builder.connect( + left.elements[i], + Target::wire(gate, Poseidon2Gate8::::wire_input(i)), + ); + builder.connect( + right.elements[i], + Target::wire(gate, Poseidon2Gate8::::wire_input(i + OUT_8)), + ); + } + + HashOutTarget { + elements: core::array::from_fn(|i| { + Target::wire(gate, Poseidon2Gate8::::wire_output(i)) + }), + } +} + +impl + Poseidon2, const D: usize> Gate for Poseidon2Gate8 { + fn id(&self) -> String { + format!("{:?}", self, WIDTH_8) + } + + fn serialize( + &self, + _dst: &mut Vec, + _common_data: &CommonCircuitData, + ) -> IoResult<()> { + Ok(()) + } + + fn deserialize(_src: &mut Buffer, _common_data: &CommonCircuitData) -> IoResult { + Ok(Self::new()) + } + + fn eval_unfiltered(&self, vars: EvaluationVars) -> Vec { + let mut constraints = Vec::with_capacity(self.num_constraints()); + let swap = vars.local_wires[Self::WIRE_SWAP]; + constraints.push(swap * (swap - F::Extension::ONE)); + + for i in 0..4 { + let input_lhs = vars.local_wires[Self::wire_input(i)]; + let input_rhs = vars.local_wires[Self::wire_input(i + 4)]; + let delta_i = vars.local_wires[Self::wire_delta(i)]; + constraints.push(swap * (input_rhs - input_lhs) - delta_i); + } + + let mut state = [F::Extension::ZERO; WIDTH_8]; + for i in 0..4 { + let delta_i = vars.local_wires[Self::wire_delta(i)]; + let input_lhs = Self::wire_input(i); + let input_rhs = Self::wire_input(i + 4); + state[i] = vars.local_wires[input_lhs] + delta_i; + state[i + 4] = vars.local_wires[input_rhs] - delta_i; + } + + external_linear_layer_hl_8(&mut state); + + for r in 0..ROUNDS_F_HALF_8 { + add_rc_8(&mut state, r); + if r != 0 { + for i in 0..WIDTH_8 { + let sbox_in = vars.local_wires[Self::wire_full_sbox_0(r, i)]; + constraints.push(state[i] - sbox_in); + state[i] = sbox_in; + } + } + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + for (r, &rc) in INTERNAL_CONSTANTS_8.iter().enumerate() { + state[0] += F::Extension::from_canonical_u64(rc); + let sbox_in = vars.local_wires[Self::wire_partial_sbox(r)]; + constraints.push(state[0] - sbox_in); + state[0] = sbox_in; + state[0] = sbox_p(state[0]); + internal_linear_layer_8(&mut state); + } + + for r in ROUNDS_F_HALF_8..ROUNDS_F_8 { + add_rc_8(&mut state, r); + for i in 0..WIDTH_8 { + let sbox_in = vars.local_wires[Self::wire_full_sbox_1(r - ROUNDS_F_HALF_8, i)]; + constraints.push(state[i] - sbox_in); + state[i] = sbox_in; + } + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + for i in 0..WIDTH_8 { + constraints.push(state[i] - vars.local_wires[Self::wire_output(i)]); + } + + constraints + } + + fn eval_unfiltered_base_one( + &self, + vars: EvaluationVarsBase, + mut yield_constr: StridedConstraintConsumer, + ) { + let swap = vars.local_wires[Self::WIRE_SWAP]; + yield_constr.one(swap * swap.sub_one()); + + for i in 0..4 { + let input_lhs = vars.local_wires[Self::wire_input(i)]; + let input_rhs = vars.local_wires[Self::wire_input(i + 4)]; + let delta_i = vars.local_wires[Self::wire_delta(i)]; + yield_constr.one(swap * (input_rhs - input_lhs) - delta_i); + } + + let mut state = [F::ZERO; WIDTH_8]; + for i in 0..4 { + let delta_i = vars.local_wires[Self::wire_delta(i)]; + let input_lhs = Self::wire_input(i); + let input_rhs = Self::wire_input(i + 4); + state[i] = vars.local_wires[input_lhs] + delta_i; + state[i + 4] = vars.local_wires[input_rhs] - delta_i; + } + + external_linear_layer_hl_8(&mut state); + + for r in 0..ROUNDS_F_HALF_8 { + add_rc_8(&mut state, r); + if r != 0 { + for i in 0..WIDTH_8 { + let sbox_in = vars.local_wires[Self::wire_full_sbox_0(r, i)]; + yield_constr.one(state[i] - sbox_in); + state[i] = sbox_in; + } + } + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + for (r, &rc) in INTERNAL_CONSTANTS_8.iter().enumerate() { + state[0] += F::from_canonical_u64(rc); + let sbox_in = vars.local_wires[Self::wire_partial_sbox(r)]; + yield_constr.one(state[0] - sbox_in); + state[0] = sbox_in; + state[0] = sbox_p(state[0]); + internal_linear_layer_8(&mut state); + } + + for r in ROUNDS_F_HALF_8..ROUNDS_F_8 { + add_rc_8(&mut state, r); + for i in 0..WIDTH_8 { + let sbox_in = vars.local_wires[Self::wire_full_sbox_1(r - ROUNDS_F_HALF_8, i)]; + yield_constr.one(state[i] - sbox_in); + state[i] = sbox_in; + } + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + for i in 0..WIDTH_8 { + yield_constr.one(state[i] - vars.local_wires[Self::wire_output(i)]); + } + } + + fn eval_unfiltered_circuit( + &self, + builder: &mut CircuitBuilder, + vars: EvaluationTargets, + ) -> Vec> { + let mut constraints = Vec::with_capacity(self.num_constraints()); + let swap = vars.local_wires[Self::WIRE_SWAP]; + constraints.push(builder.mul_sub_extension(swap, swap, swap)); + + for i in 0..4 { + let input_lhs = vars.local_wires[Self::wire_input(i)]; + let input_rhs = vars.local_wires[Self::wire_input(i + 4)]; + let delta_i = vars.local_wires[Self::wire_delta(i)]; + let diff = builder.sub_extension(input_rhs, input_lhs); + constraints.push(builder.mul_sub_extension(swap, diff, delta_i)); + } + + let mut state = [builder.zero_extension(); WIDTH_8]; + for i in 0..4 { + let delta_i = vars.local_wires[Self::wire_delta(i)]; + let input_lhs = vars.local_wires[Self::wire_input(i)]; + let input_rhs = vars.local_wires[Self::wire_input(i + 4)]; + state[i] = builder.add_extension(input_lhs, delta_i); + state[i + 4] = builder.sub_extension(input_rhs, delta_i); + } + + external_linear_layer_hl_8_circuit(builder, &mut state); + + for r in 0..ROUNDS_F_HALF_8 { + add_rc_8_circuit(builder, &mut state, r); + if r != 0 { + for i in 0..WIDTH_8 { + let sbox_in = vars.local_wires[Self::wire_full_sbox_0(r, i)]; + constraints.push(builder.sub_extension(state[i], sbox_in)); + state[i] = sbox_in; + } + } + sbox_8_circuit(builder, &mut state); + external_linear_layer_hl_8_circuit(builder, &mut state); + } + + for (r, &rc) in INTERNAL_CONSTANTS_8.iter().enumerate() { + let round_constant = builder.constant_extension(F::Extension::from_canonical_u64(rc)); + state[0] = builder.add_extension(state[0], round_constant); + + let sbox_in = vars.local_wires[Self::wire_partial_sbox(r)]; + constraints.push(builder.sub_extension(state[0], sbox_in)); + state[0] = sbox_in; + state[0] = sbox_p_circuit(builder, state[0]); + internal_linear_layer_8_circuit(builder, &mut state); + } + + for r in ROUNDS_F_HALF_8..ROUNDS_F_8 { + add_rc_8_circuit(builder, &mut state, r); + for i in 0..WIDTH_8 { + let sbox_in = vars.local_wires[Self::wire_full_sbox_1(r - ROUNDS_F_HALF_8, i)]; + constraints.push(builder.sub_extension(state[i], sbox_in)); + state[i] = sbox_in; + } + sbox_8_circuit(builder, &mut state); + external_linear_layer_hl_8_circuit(builder, &mut state); + } + + for i in 0..WIDTH_8 { + constraints + .push(builder.sub_extension(state[i], vars.local_wires[Self::wire_output(i)])); + } + + constraints + } + + fn generators(&self, row: usize, _local_constants: &[F]) -> Vec> { + vec![WitnessGeneratorRef::new( + Poseidon2Generator8:: { + row, + _phantom: PhantomData, + } + .adapter(), + )] + } + + fn num_wires(&self) -> usize { + Self::end() + } + + fn num_constants(&self) -> usize { + 0 + } + + fn degree(&self) -> usize { + 7 + } + + fn num_constraints(&self) -> usize { + WIDTH_8 * (ROUNDS_F_8 - 1) + ROUNDS_P_8 + WIDTH_8 + 1 + 4 + } +} + +#[derive(Debug, Default)] +pub struct Poseidon2Generator8 + Poseidon2, const D: usize> { + row: usize, + _phantom: PhantomData, +} + +impl + Poseidon2, const D: usize> SimpleGenerator + for Poseidon2Generator8 +{ + fn id(&self) -> String { + "Poseidon2Generator8".to_string() + } + + fn dependencies(&self) -> Vec { + (0..WIDTH_8) + .map(|i| Poseidon2Gate8::::wire_input(i)) + .chain(Some(Poseidon2Gate8::::WIRE_SWAP)) + .map(|column| Target::wire(self.row, column)) + .collect() + } + + fn run_once( + &self, + witness: &PartitionWitness, + out_buffer: &mut GeneratedValues, + ) -> Result<()> { + let local_wire = |column| Wire { + row: self.row, + column, + }; + + let mut state = (0..WIDTH_8) + .map(|i| witness.get_wire(local_wire(Poseidon2Gate8::::wire_input(i)))) + .collect::>(); + + let swap_value = witness.get_wire(local_wire(Poseidon2Gate8::::WIRE_SWAP)); + debug_assert!(swap_value == F::ZERO || swap_value == F::ONE); + + for i in 0..4 { + let delta_i = swap_value * (state[i + 4] - state[i]); + out_buffer.set_wire(local_wire(Poseidon2Gate8::::wire_delta(i)), delta_i)?; + state[i] += delta_i; + state[i + 4] -= delta_i; + } + + let mut state: [F; WIDTH_8] = state.try_into().unwrap(); + external_linear_layer_hl_8(&mut state); + + for r in 0..ROUNDS_F_HALF_8 { + add_rc_8(&mut state, r); + if r != 0 { + for i in 0..WIDTH_8 { + out_buffer.set_wire( + local_wire(Poseidon2Gate8::::wire_full_sbox_0(r, i)), + state[i], + )?; + } + } + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + for (r, &rc) in INTERNAL_CONSTANTS_8.iter().enumerate() { + state[0] += F::from_canonical_u64(rc); + out_buffer.set_wire( + local_wire(Poseidon2Gate8::::wire_partial_sbox(r)), + state[0], + )?; + state[0] = sbox_p(state[0]); + internal_linear_layer_8(&mut state); + } + + for r in ROUNDS_F_HALF_8..ROUNDS_F_8 { + add_rc_8(&mut state, r); + for i in 0..WIDTH_8 { + out_buffer.set_wire( + local_wire(Poseidon2Gate8::::wire_full_sbox_1( + r - ROUNDS_F_HALF_8, + i, + )), + state[i], + )?; + } + sbox_8(&mut state); + external_linear_layer_hl_8(&mut state); + } + + for i in 0..WIDTH_8 { + out_buffer.set_wire(local_wire(Poseidon2Gate8::::wire_output(i)), state[i])?; + } + + Ok(()) + } + + fn serialize(&self, dst: &mut Vec, _common_data: &CommonCircuitData) -> IoResult<()> { + dst.write_usize(self.row) + } + + fn deserialize(src: &mut Buffer, _common_data: &CommonCircuitData) -> IoResult { + let row = src.read_usize()?; + Ok(Self { + row, + _phantom: PhantomData, + }) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + use super::*; + use crate::field::goldilocks_field::GoldilocksField; + use crate::gates::gate_testing::{test_eval_fns, test_low_degree}; + use crate::hash::poseidon2::hash::Poseidon2Hash; + use crate::iop::generator::generate_partial_witness; + use crate::iop::witness::PartialWitness; + use crate::plonk::circuit_data::CircuitConfig; + use crate::plonk::config::{GenericConfig, Poseidon2GoldilocksConfig}; + + #[test] + fn generated_output_matches_hash_8_to_4() { + const D: usize = 2; + type C = Poseidon2GoldilocksConfig; + type F = >::F; + + let config = CircuitConfig { + num_wires: 128, + ..CircuitConfig::standard_recursion_config() + }; + let mut builder = CircuitBuilder::new(config); + type Gate = Poseidon2Gate8; + let gate = Gate::new(); + let row = builder.add_gate(gate, vec![]); + let circuit = builder.build_prover::(); + + let permutation_inputs = (0..WIDTH_8) + .map(F::from_canonical_usize) + .collect::>(); + + let mut inputs = PartialWitness::new(); + inputs + .set_wire( + Wire { + row, + column: Gate::WIRE_SWAP, + }, + F::ZERO, + ) + .unwrap(); + for i in 0..WIDTH_8 { + inputs + .set_wire( + Wire { + row, + column: Gate::wire_input(i), + }, + permutation_inputs[i], + ) + .unwrap(); + } + + let witness = + generate_partial_witness(inputs, &circuit.prover_only, &circuit.common).unwrap(); + + let expected_outputs = Poseidon2Hash::hash_8_to_4(permutation_inputs.try_into().unwrap()); + for i in 0..OUT_8 { + let out = witness.get_wire(Wire { + row, + column: Gate::wire_output(i), + }); + assert_eq!(out, expected_outputs.elements[i]); + } + } + + #[test] + fn generated_output_matches_hash_8_to_4_swap_true() { + const D: usize = 2; + type C = Poseidon2GoldilocksConfig; + type F = >::F; + + let config = CircuitConfig { + num_wires: 128, + ..CircuitConfig::standard_recursion_config() + }; + let mut builder = CircuitBuilder::new(config); + type Gate = Poseidon2Gate8; + let gate = Gate::new(); + let row = builder.add_gate(gate, vec![]); + let circuit = builder.build_prover::(); + + let permutation_inputs = (0..WIDTH_8) + .map(|x| F::from_canonical_usize(100 + x)) + .collect::>(); + + let mut inputs = PartialWitness::new(); + inputs + .set_wire( + Wire { + row, + column: Gate::WIRE_SWAP, + }, + F::ONE, + ) + .unwrap(); + for i in 0..WIDTH_8 { + inputs + .set_wire( + Wire { + row, + column: Gate::wire_input(i), + }, + permutation_inputs[i], + ) + .unwrap(); + } + + let witness = + generate_partial_witness(inputs, &circuit.prover_only, &circuit.common).unwrap(); + + let swapped = [ + permutation_inputs[4], + permutation_inputs[5], + permutation_inputs[6], + permutation_inputs[7], + permutation_inputs[0], + permutation_inputs[1], + permutation_inputs[2], + permutation_inputs[3], + ]; + let expected_outputs = Poseidon2Hash::hash_8_to_4(swapped); + for i in 0..OUT_8 { + let out = witness.get_wire(Wire { + row, + column: Gate::wire_output(i), + }); + assert_eq!(out, expected_outputs.elements[i]); + } + } + + #[test] + fn randomized_matches_hash_8_to_4() { + use rand::{thread_rng, Rng}; + + const D: usize = 2; + type C = Poseidon2GoldilocksConfig; + type F = >::F; + + let config = CircuitConfig { + num_wires: 128, + ..CircuitConfig::standard_recursion_config() + }; + let mut builder = CircuitBuilder::new(config); + type Gate = Poseidon2Gate8; + let gate = Gate::new(); + let row = builder.add_gate(gate, vec![]); + let circuit = builder.build_prover::(); + + let mut rng = thread_rng(); + for _ in 0..200 { + let input = core::array::from_fn(|_| F::from_noncanonical_u64(rng.gen())); + + for swap in [F::ZERO, F::ONE] { + let mut pw = PartialWitness::new(); + pw.set_wire( + Wire { + row, + column: Gate::WIRE_SWAP, + }, + swap, + ) + .unwrap(); + for (i, v) in input.iter().enumerate() { + pw.set_wire( + Wire { + row, + column: Gate::wire_input(i), + }, + *v, + ) + .unwrap(); + } + + let witness = + generate_partial_witness(pw, &circuit.prover_only, &circuit.common).unwrap(); + + let expected_in = if swap == F::ONE { + [ + input[4], input[5], input[6], input[7], input[0], input[1], input[2], + input[3], + ] + } else { + input + }; + let expected = Poseidon2Hash::hash_8_to_4(expected_in); + for i in 0..OUT_8 { + let out = witness.get_wire(Wire { + row, + column: Gate::wire_output(i), + }); + assert_eq!(out, expected.elements[i]); + } + } + } + } + + #[test] + fn low_degree() { + type F = GoldilocksField; + let gate = Poseidon2Gate8::::new(); + test_low_degree(gate); + } + + #[test] + fn eval_fns() -> Result<()> { + const D: usize = 2; + type C = Poseidon2GoldilocksConfig; + type F = >::F; + let gate = Poseidon2Gate8::::new(); + test_eval_fns::(gate) + } +} diff --git a/plonky2/src/hash/poseidon2/hash.rs b/plonky2/src/hash/poseidon2/hash.rs index 9e6772aed..b9cae2342 100644 --- a/plonky2/src/hash/poseidon2/hash.rs +++ b/plonky2/src/hash/poseidon2/hash.rs @@ -7,6 +7,7 @@ use crate::field::extension::{Extendable, FieldExtension}; use crate::field::goldilocks_field::GoldilocksField as F; use crate::field::types::{Field, PrimeField64}; use crate::gates::poseidon2::Poseidon2Gate; +use crate::gates::poseidon2_8::poseidon2_compress_8_to_4_swapped; use crate::hash::hash_types::{HashOut, HashOutTarget, RichField}; use crate::hash::hashing::{hash_n_to_hash_no_pad, PlonkyPermutation}; use crate::iop::ext_target::ExtensionTarget; @@ -499,8 +500,7 @@ where fn sbox_8_circuit( builder: &mut CircuitBuilder, state: &mut [Target; WIDTH_8], -) -where +) where P: RichField + Extendable, { for item in state.iter_mut() { @@ -517,7 +517,10 @@ fn add_rc_8_circuit( P: RichField + Extendable, { for i in 0..WIDTH_8 { - state[i] = builder.add_const(state[i], P::from_canonical_u64(EXTERNAL_CONSTANTS_8[round][i])); + state[i] = builder.add_const( + state[i], + P::from_canonical_u64(EXTERNAL_CONSTANTS_8[round][i]), + ); } } @@ -837,7 +840,7 @@ impl AlgebraicHasher for Poseidon2Hash { where F: RichField + Extendable, { - poseidon2_compress_8_to_4_circuit(builder, left, right, swap) + poseidon2_compress_8_to_4_swapped(builder, left, right, swap) } } @@ -991,4 +994,43 @@ mod test { let proof = circuit.prove(pw).unwrap(); circuit.verify(proof.clone()) } + + #[test] + fn test_poseidon2_two_to_one_matches_swapped_false_circuit() -> Result<()> { + let mut builder = CircuitBuilder::::new(CircuitConfig::standard_recursion_config()); + let left_t = builder.add_virtual_hash(); + let right_t = builder.add_virtual_hash(); + let expected_t = builder.add_virtual_hash(); + + let got_t = >::two_to_one_swapped( + left_t, + right_t, + builder._false(), + &mut builder, + ); + builder.connect_hashes(got_t, expected_t); + + let circuit = builder.build::(); + let mut rng = thread_rng(); + + for _ in 0..32 { + let left = HashOut:: { + elements: array::from_fn(|_| F::from_noncanonical_u64(rng.next_u64())), + }; + let right = HashOut:: { + elements: array::from_fn(|_| F::from_noncanonical_u64(rng.next_u64())), + }; + let expected = >::two_to_one(left, right); + + let mut pw = PartialWitness::new(); + pw.set_hash_target(left_t, left)?; + pw.set_hash_target(right_t, right)?; + pw.set_hash_target(expected_t, expected)?; + + let proof = circuit.prove(pw)?; + circuit.verify(proof)?; + } + + Ok(()) + } } diff --git a/plonky2/src/plonk/config.rs b/plonky2/src/plonk/config.rs index 1e4525ac5..06aac86aa 100644 --- a/plonky2/src/plonk/config.rs +++ b/plonky2/src/plonk/config.rs @@ -112,7 +112,9 @@ pub trait AlgebraicHasher: Hasher> { perm_inputs.set_from_iter(core::iter::repeat(zero), 2 * NUM_HASH_OUT_ELTS); let perm_outs = Self::permute_swapped(perm_inputs, swap, builder); HashOutTarget { - elements: perm_outs.squeeze()[0..NUM_HASH_OUT_ELTS].try_into().unwrap(), + elements: perm_outs.squeeze()[0..NUM_HASH_OUT_ELTS] + .try_into() + .unwrap(), } } } From efa4b810b059f0447841d18ef5aaeed3d22af071 Mon Sep 17 00:00:00 2001 From: lighter-zz Date: Thu, 26 Feb 2026 10:48:33 -0500 Subject: [PATCH 4/5] serdes --- plonky2/src/util/serialization/gate_serialization.rs | 2 ++ plonky2/src/util/serialization/generator_serialization.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/plonky2/src/util/serialization/gate_serialization.rs b/plonky2/src/util/serialization/gate_serialization.rs index d48f5ca7a..f196144f9 100644 --- a/plonky2/src/util/serialization/gate_serialization.rs +++ b/plonky2/src/util/serialization/gate_serialization.rs @@ -111,6 +111,7 @@ pub mod default { use crate::gates::noop::NoopGate; use crate::gates::poseidon::PoseidonGate; use crate::gates::poseidon2::Poseidon2Gate; + use crate::gates::poseidon2_8::Poseidon2Gate8; use crate::gates::poseidon_mds::PoseidonMdsGate; use crate::gates::public_input::PublicInputGate; use crate::gates::random_access::RandomAccessGate; @@ -151,6 +152,7 @@ pub mod default { PoseidonMdsGate, PoseidonGate, Poseidon2Gate, + Poseidon2Gate8, PublicInputGate, RandomAccessGate, ReducingExtensionGate, diff --git a/plonky2/src/util/serialization/generator_serialization.rs b/plonky2/src/util/serialization/generator_serialization.rs index 08a53cee6..0efc25634 100644 --- a/plonky2/src/util/serialization/generator_serialization.rs +++ b/plonky2/src/util/serialization/generator_serialization.rs @@ -118,6 +118,8 @@ pub mod default { use crate::gates::multiplication_base::MultiplicationBaseGenerator; use crate::gates::multiplication_extension::MulExtensionGenerator; use crate::gates::poseidon::PoseidonGenerator; + use crate::gates::poseidon2::Poseidon2Generator; + use crate::gates::poseidon2_8::Poseidon2Generator8; use crate::gates::poseidon_mds::PoseidonMdsGenerator; use crate::gates::random_access::RandomAccessGenerator; use crate::gates::reducing::ReducingGenerator; @@ -177,6 +179,8 @@ pub mod default { MulExtensionGenerator, NonzeroTestGenerator, PoseidonGenerator, + Poseidon2Generator, + Poseidon2Generator8, PoseidonMdsGenerator, QuotientGeneratorExtension, RandomAccessGenerator, From bf18911b717917cb7245fd255e233f85b09cc23e Mon Sep 17 00:00:00 2001 From: lighter-zz Date: Mon, 2 Mar 2026 08:13:00 -0500 Subject: [PATCH 5/5] clean up --- plonky2/src/hash/poseidon2/hash.rs | 139 ----------------------------- 1 file changed, 139 deletions(-) diff --git a/plonky2/src/hash/poseidon2/hash.rs b/plonky2/src/hash/poseidon2/hash.rs index b9cae2342..85750db40 100644 --- a/plonky2/src/hash/poseidon2/hash.rs +++ b/plonky2/src/hash/poseidon2/hash.rs @@ -442,145 +442,6 @@ fn external_linear_layer_hl_u128_8(state: &mut [u128; WIDTH_8]) { } } -#[inline] -fn poseidon2_compress_8_to_4_circuit( - builder: &mut CircuitBuilder, - left: HashOutTarget, - right: HashOutTarget, - swap: BoolTarget, -) -> HashOutTarget -where - P: RichField + Extendable, -{ - let zero = builder.zero(); - let mut state = [zero; WIDTH_8]; - - for i in 0..OUT_8 { - state[i] = builder.select(swap, right.elements[i], left.elements[i]); - state[i + OUT_8] = builder.select(swap, left.elements[i], right.elements[i]); - } - - external_linear_layer_hl_8_circuit(builder, &mut state); - - for r in 0..ROUNDS_F_HALF_8 { - add_rc_8_circuit(builder, &mut state, r); - sbox_8_circuit(builder, &mut state); - external_linear_layer_hl_8_circuit(builder, &mut state); - } - - for &rc in INTERNAL_CONSTANTS_8.iter() { - state[0] = builder.add_const(state[0], P::from_canonical_u64(rc)); - state[0] = sbox_p_circuit(builder, state[0]); - internal_linear_layer_8_circuit(builder, &mut state); - } - - for r in ROUNDS_F_HALF_8..ROUNDS_F_8 { - add_rc_8_circuit(builder, &mut state, r); - sbox_8_circuit(builder, &mut state); - external_linear_layer_hl_8_circuit(builder, &mut state); - } - - HashOutTarget { - elements: state[..OUT_8].try_into().unwrap(), - } -} - -#[inline] -fn sbox_p_circuit(builder: &mut CircuitBuilder, x: Target) -> Target -where - P: RichField + Extendable, -{ - let x2 = builder.mul(x, x); - let x4 = builder.mul(x2, x2); - let x3 = builder.mul(x2, x); - builder.mul(x3, x4) -} - -#[inline] -fn sbox_8_circuit( - builder: &mut CircuitBuilder, - state: &mut [Target; WIDTH_8], -) where - P: RichField + Extendable, -{ - for item in state.iter_mut() { - *item = sbox_p_circuit(builder, *item); - } -} - -#[inline] -fn add_rc_8_circuit( - builder: &mut CircuitBuilder, - state: &mut [Target; WIDTH_8], - round: usize, -) where - P: RichField + Extendable, -{ - for i in 0..WIDTH_8 { - state[i] = builder.add_const( - state[i], - P::from_canonical_u64(EXTERNAL_CONSTANTS_8[round][i]), - ); - } -} - -#[inline] -fn internal_linear_layer_8_circuit( - builder: &mut CircuitBuilder, - state: &mut [Target; WIDTH_8], -) where - P: RichField + Extendable, -{ - let sum = builder.add_many(state.iter().copied()); - for i in 0..WIDTH_8 { - state[i] = - builder.mul_const_add(P::from_canonical_u64(MATRIX_DIAG_8_U64[i]), state[i], sum); - } -} - -#[inline] -fn external_linear_layer_hl_8_circuit( - builder: &mut CircuitBuilder, - state: &mut [Target; WIDTH_8], -) where - P: RichField + Extendable, -{ - for i in (0..WIDTH_8).step_by(4) { - let t0 = builder.add(state[i], state[i + 1]); - let t1 = builder.add(state[i + 2], state[i + 3]); - let two_x1 = builder.add(state[i + 1], state[i + 1]); - let two_x3 = builder.add(state[i + 3], state[i + 3]); - let t2 = builder.add(two_x1, t1); - let t3 = builder.add(two_x3, t0); - - let two_t1 = builder.add(t1, t1); - let four_t1 = builder.add(two_t1, two_t1); - let t4 = builder.add(four_t1, t3); - - let two_t0 = builder.add(t0, t0); - let four_t0 = builder.add(two_t0, two_t0); - let t5 = builder.add(four_t0, t2); - - let t6 = builder.add(t3, t5); - let t7 = builder.add(t2, t4); - state[i] = t6; - state[i + 1] = t5; - state[i + 2] = t7; - state[i + 3] = t4; - } - - let sums = [ - builder.add(state[0], state[4]), - builder.add(state[1], state[5]), - builder.add(state[2], state[6]), - builder.add(state[3], state[7]), - ]; - - for i in 0..WIDTH_8 { - state[i] = builder.add(state[i], sums[i % 4]); - } -} - impl Poseidon2 for F { #[inline] fn sbox_p(a: &Self) -> Self {