Merge branch 'main' into aes-gcm-circuits
arnaucube git@arnaucube.com
Wed, 18 Feb 2026 19:35:02 +0100
6 files changed,
285 insertions(+),
2 deletions(-)
M
Cargo.toml
→
Cargo.toml
@@ -1,6 +1,7 @@
[workspace] members = [ - "aes-gcm" + "aes-gcm", + "feistel" ] resolver = "2"
M
README.md
→
README.md
@@ -5,3 +5,4 @@
- [aes-gcm](/aes-gcm): Plonky2 circuits for AES & AES-GCM encryption system as described at - AES: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf - AES-GCM (Galois Counter Mode): https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf +- [feistel](/feistel): a Plonky2 library for a finite field analogue of the Feistel cipher as described [here](https://en.wikipedia.org/wiki/Feistel_cipher).
M
aes-gcm/src/circuit_aes.rs
→
aes-gcm/src/circuit_aes.rs
@@ -104,7 +104,7 @@ a: [[ByteTarget; N]; M],
x: [ByteTarget; N], ) -> [ByteTarget; M]; - /// returns a 0u8 in the shape of a ByteArrayTarget. + /// returns a 0u8 in the shape of a ByteTarget. fn zero_byte(&mut self) -> ByteTarget; fn empty_state(&mut self) -> StateTarget;
A
feistel/Cargo.toml
@@ -0,0 +1,9 @@
+[package] +name = "plonky2-feistel" +version = "0.1.0" +edition = "2024" + +[dependencies] +plonky2 = { workspace = true } +anyhow = { workspace = true } +rand = "0.8.5"
A
feistel/src/circuit.rs
@@ -0,0 +1,153 @@
+use std::array; + +use plonky2::{ + field::{extension::Extendable, goldilocks_field::GoldilocksField as F}, + hash::hash_types::RichField, + iop::{ + target::Target, + witness::{PartialWitness, WitnessWrite}, + }, + plonk::circuit_builder::CircuitBuilder, +}; + +pub(crate) const D: usize = 2; + +/// State on which the Feistel-like cipher operates +#[derive(Debug, Copy, Clone)] +pub struct FeistelStateTarget<const STATE_LEN: usize>([Target; STATE_LEN]); + +pub trait CircuitBuilderFeistel<F: RichField + Extendable<D>, const D: usize> { + /// Feistel state target constructor + fn add_feistel_state_target<const STATE_LEN: usize>(&mut self) + -> FeistelStateTarget<STATE_LEN>; + + /// Generates Feistel-like cipher as in `crate::feistel_cipher` + fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize>( + &mut self, + state: FeistelStateTarget<{ 2 * STATE_HALF_LEN }>, + key_schedule: &[[Target; KEY_LEN]], + f: &impl Fn(&mut Self, Vec<Target>) -> [Target; STATE_HALF_LEN], + ) -> FeistelStateTarget<{ 2 * STATE_HALF_LEN }>; +} + +impl CircuitBuilderFeistel<F, D> for CircuitBuilder<F, D> { + fn add_feistel_state_target<const STATE_LEN: usize>( + &mut self, + ) -> FeistelStateTarget<STATE_LEN> { + FeistelStateTarget(self.add_virtual_target_arr()) + } + + fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize>( + &mut self, + state: FeistelStateTarget<{ 2 * STATE_HALF_LEN }>, + key_schedule: &[[Target; KEY_LEN]], + f: &impl Fn(&mut Self, Vec<Target>) -> [Target; STATE_HALF_LEN], + ) -> FeistelStateTarget<{ 2 * STATE_HALF_LEN }> { + if key_schedule.is_empty() { + state + } else { + let k = key_schedule[0]; + let l = &state.0[..STATE_HALF_LEN]; + let r = &state.0[STATE_HALF_LEN..]; + + let new_l = r; + let round_offset = f(self, [r, &k].concat()); + let new_r: [_; STATE_HALF_LEN] = array::from_fn(|i| self.add(l[i], round_offset[i])); + self.feistel_cipher( + FeistelStateTarget(array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + })), + &key_schedule[1..], + f, + ) + } + } +} + +pub trait PartialWitnessFeistel { + /// Sets witnesses for a Feistel state target + fn set_feistel_state_target<const STATE_SIZE: usize>( + &mut self, + target: &FeistelStateTarget<STATE_SIZE>, + value: &[F; STATE_SIZE], + ) -> anyhow::Result<()>; +} + +impl PartialWitnessFeistel for PartialWitness<F> { + fn set_feistel_state_target<const STATE_SIZE: usize>( + &mut self, + target: &FeistelStateTarget<STATE_SIZE>, + value: &[F; STATE_SIZE], + ) -> anyhow::Result<()> { + self.set_target_arr(&target.0, value) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + field::goldilocks_field::GoldilocksField as F, + hash::{ + hashing::hash_n_to_hash_no_pad, + poseidon::{PoseidonHash, PoseidonPermutation}, + }, + iop::witness::{PartialWitness, WitnessWrite}, + plonk::{ + circuit_builder::CircuitBuilder, circuit_data::CircuitConfig, + config::PoseidonGoldilocksConfig, + }, + }; + + use super::D; + use crate::{ + circuit::{CircuitBuilderFeistel, FeistelStateTarget, PartialWitnessFeistel}, + feistel_cipher, + tests::random_fields, + }; + + #[test] + fn feistel_poseidon_check() -> anyhow::Result<()> { + const NR: usize = 32; + const STATE_HALF_LEN: usize = 4; + const KEY_LEN: usize = 4; + + let config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::<F, D>::new(config); + + let state_target: FeistelStateTarget<{ 2 * STATE_HALF_LEN }> = + builder.add_feistel_state_target(); + let key_schedule_target: [[_; KEY_LEN]; NR] = + std::array::from_fn(|_| builder.add_virtual_target_arr()); + + let f = |builder: &mut CircuitBuilder<F, D>, targets: Vec<_>| { + builder + .hash_n_to_hash_no_pad::<PoseidonHash>(targets) + .elements + }; + + let out_state_target = builder.feistel_cipher(state_target, &key_schedule_target, &f); + + let data = builder.build::<PoseidonGoldilocksConfig>(); + (0..10).try_for_each(|_| { + let state: [_; 2 * STATE_HALF_LEN] = random_fields(); + let key_schedule: [[_; KEY_LEN]; NR] = std::array::from_fn(|_| random_fields()); + let eff = |fields: Vec<_>| { + hash_n_to_hash_no_pad::<F, PoseidonPermutation<_>>(&fields).elements + }; + let out_state = feistel_cipher(state, &key_schedule, &eff); + + let mut pw = PartialWitness::new(); + pw.set_feistel_state_target(&state_target, &state)?; + std::iter::zip(key_schedule_target, key_schedule) + .try_for_each(|(t, v)| pw.set_target_arr(&t, &v))?; + pw.set_feistel_state_target(&out_state_target, &out_state)?; + + let proof = data.prove(pw)?; + data.verify(proof) + }) + } +}
A
feistel/src/lib.rs
@@ -0,0 +1,119 @@
+#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +pub mod circuit; + +use std::array; + +use plonky2::field::types::Field; + +/// Feistel-type cipher for finite fields, where XORing is replaced +/// with addition, cf. https://en.wikipedia.org/wiki/Feistel_cipher or +/// §3.5.1 of Stinson's *Cryptography* (3rd edition). +pub fn feistel_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize, F: Field>( + state: [F; 2 * STATE_HALF_LEN], + key_schedule: &[[F; KEY_LEN]], + f: &impl Fn(Vec<F>) -> [F; STATE_HALF_LEN], +) -> [F; 2 * STATE_HALF_LEN] { + if key_schedule.is_empty() { + state + } else { + let k = key_schedule[0]; + let l = &state[..STATE_HALF_LEN]; + let r = &state[STATE_HALF_LEN..]; + + let new_l = r; + let round_offset = f([r, &k].concat()); + let new_r: [_; STATE_HALF_LEN] = array::from_fn(|i| l[i] + round_offset[i]); + + feistel_cipher( + array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + }), + &key_schedule[1..], + f, + ) + } +} + +/// Feistel-type inverse cipher for finite fields, where XORing is +/// replaced with subtraction. +pub fn feistel_inv_cipher<const STATE_HALF_LEN: usize, const KEY_LEN: usize, F: Field>( + state: [F; 2 * STATE_HALF_LEN], + reverse_key_schedule: &[[F; KEY_LEN]], + f: &impl Fn(Vec<F>) -> [F; STATE_HALF_LEN], +) -> [F; 2 * STATE_HALF_LEN] { + if reverse_key_schedule.is_empty() { + state + } else { + let k = reverse_key_schedule[0]; + let l = &state[..STATE_HALF_LEN]; + let r = &state[STATE_HALF_LEN..]; + + let new_r = l; + let round_offset = f([l, &k].concat()); + let new_l: [_; STATE_HALF_LEN] = array::from_fn(|i| r[i] - round_offset[i]); + + feistel_inv_cipher( + array::from_fn(|i| { + if i < STATE_HALF_LEN { + new_l[i] + } else { + new_r[i - STATE_HALF_LEN] + } + }), + &reverse_key_schedule[1..], + f, + ) + } +} + +#[cfg(test)] +mod tests { + use plonky2::{ + field::{ + goldilocks_field::GoldilocksField as F, + types::{Field, Field64}, + }, + hash::{ + hashing::hash_n_to_hash_no_pad, keccak::KeccakPermutation, + poseidon::PoseidonPermutation, + }, + }; + use rand::{Rng, rngs::OsRng}; + + use crate::{feistel_cipher, feistel_inv_cipher}; + + pub(crate) fn random_fields<const N: usize>() -> [F; N] { + std::array::from_fn(|_| F::from_canonical_u64(OsRng.gen_range(0..F::ORDER))) + } + + #[test] + fn feistel_keccak_poseidon_roundtrip() { + const NR: usize = 32; + const STATE_HALF_LEN: usize = 4; + const KEY_LEN: usize = 4; + + let f_keccak = + |fields: Vec<_>| hash_n_to_hash_no_pad::<F, KeccakPermutation<_>>(&fields).elements; + let f_poseidon = + |fields: Vec<_>| hash_n_to_hash_no_pad::<F, PoseidonPermutation<_>>(&fields).elements; + + [f_keccak, f_poseidon].into_iter().for_each(|f| { + (0..10).for_each(|_| { + let state: [_; 2 * STATE_HALF_LEN] = random_fields(); + let key_schedule: [[_; KEY_LEN]; NR] = std::array::from_fn(|_| random_fields()); + + let out_state = feistel_cipher(state, &key_schedule, &f); + let reverse_key_schedule = key_schedule.into_iter().rev().collect::<Vec<_>>(); + let maybe_original_state = feistel_inv_cipher(out_state, &reverse_key_schedule, &f); + + assert_eq!(state, maybe_original_state); + }) + }) + } +}