1use core::array::from_fn;
15use std::iter::once;
16
17use ff::Field;
18use midnight_proofs::{
19 circuit::{Chip, Layouter, Region, Value},
20 plonk::{Advice, Column, ConstraintSystem, Constraints, Error, Expression, Fixed, Selector},
21 poly::Rotation,
22};
23#[cfg(any(test, feature = "testing"))]
24use {crate::testing_utils::FromScratch, midnight_proofs::plonk::Instance};
25
26use super::{
27 constants::{PoseidonField, NB_FULL_ROUNDS, NB_PARTIAL_ROUNDS, RATE, WIDTH},
28 full_round_cpu, partial_round_cpu_for_circuits,
29 round_skips::PreComputedRoundCircuit,
30 NB_POSEIDON_ADVICE_COLS, NB_POSEIDON_FIXED_COLS,
31};
32#[cfg(any(test, feature = "testing"))]
33use crate::field::{
34 native::{NB_ARITH_COLS, NB_ARITH_FIXED_COLS},
35 NativeConfig,
36};
37use crate::{
38 field::NativeChip,
39 instructions::{ArithInstructions, AssignmentInstructions, SpongeInstructions},
40 types::AssignedNative,
41 utils::ComposableChip,
42 CircuitField,
43};
44
45pub(crate) const NB_SKIPS_CIRCUIT: usize = 5;
51
52pub(super) type AssignedRegister<F> = [AssignedNative<F>; WIDTH];
56
57#[derive(Clone, Debug)]
59pub struct AssignedPoseidonState<F: CircuitField> {
60 pub(super) register: AssignedRegister<F>,
61 pub(super) queue: Vec<AssignedNative<F>>,
62 pub(super) squeeze_position: usize,
63 input_len: Option<usize>,
64}
65
66#[derive(Clone, Debug)]
67pub struct PoseidonConfig<F: PoseidonField> {
69 q_full_round: Selector,
71
72 q_partial_round: Selector,
75
76 register_cols: [Column<Advice>; NB_POSEIDON_ADVICE_COLS],
83
84 constant_cols: [Column<Fixed>; NB_POSEIDON_FIXED_COLS],
88
89 pre_computed: PreComputedRoundCircuit<F>,
91}
92
93#[derive(Clone, Debug)]
95pub struct PoseidonChip<F: PoseidonField> {
96 config: PoseidonConfig<F>,
97 native_chip: NativeChip<F>,
98}
99
100impl<F: PoseidonField> Chip<F> for PoseidonChip<F> {
101 type Config = PoseidonConfig<F>;
102 type Loaded = ();
103
104 fn config(&self) -> &Self::Config {
105 &self.config
106 }
107
108 fn loaded(&self) -> &Self::Loaded {
109 &()
110 }
111}
112
113impl<F: PoseidonField> PoseidonChip<F> {
114 pub const fn register_size() -> usize {
116 WIDTH
117 }
118
119 pub const fn rate() -> usize {
121 RATE
122 }
123
124 pub const fn nb_full_rounds() -> usize {
126 NB_FULL_ROUNDS
127 }
128
129 pub const fn nb_partial_rounds() -> usize {
131 NB_PARTIAL_ROUNDS
132 }
133}
134
135fn linear_layer<F: PoseidonField>(
140 inputs: [Expression<F>; WIDTH],
141 outputs: [Expression<F>; WIDTH],
142 constants: [Expression<F>; WIDTH],
143) -> [Expression<F>; WIDTH] {
144 let mut ids = constants;
145 for (i, output) in outputs.into_iter().enumerate() {
146 ids[i] = &ids[i] - output;
147 #[allow(clippy::needless_range_loop)]
148 for j in 0..WIDTH {
149 ids[i] = &ids[i] + Expression::Constant(F::MDS[i][j]) * &inputs[j];
150 }
151 }
152 ids
153}
154
155pub(crate) fn sbox<F: Field>(x: Expression<F>) -> Expression<F> {
157 x.clone() * x.square().square()
158}
159
160impl<F: PoseidonField> ComposableChip<F> for PoseidonChip<F> {
161 type SharedResources = (
162 [Column<Advice>; NB_POSEIDON_ADVICE_COLS],
163 [Column<Fixed>; NB_POSEIDON_FIXED_COLS],
164 );
165
166 type InstructionDeps = NativeChip<F>;
167
168 fn new(config: &PoseidonConfig<F>, native_chip: &Self::InstructionDeps) -> Self {
169 Self {
170 config: config.clone(),
171 native_chip: native_chip.clone(),
172 }
173 }
174
175 fn configure(
176 meta: &mut ConstraintSystem<F>,
177 shared_res: &Self::SharedResources,
178 ) -> PoseidonConfig<F> {
179 let register_cols = shared_res.0;
180 let constant_cols = shared_res.1;
181
182 let q_full_round = meta.selector();
183 let q_partial_round = meta.complex_selector();
184
185 register_cols[..WIDTH].iter().for_each(|col| meta.enable_equality(*col));
186
187 meta.create_gate("full_round_gate", |meta| {
198 let inputs_and_hints: [(Expression<F>, Expression<F>); WIDTH] = from_fn(|i| {
201 (
202 meta.query_advice(register_cols[i], Rotation::cur()),
203 meta.query_advice(register_cols[i + WIDTH], Rotation::cur()),
204 )
205 });
206 let outputs = from_fn(|i| meta.query_advice(register_cols[i], Rotation::next()));
207 let constants = from_fn(|i| meta.query_fixed(constant_cols[i], Rotation::cur()));
208
209 let sboxed_inputs = inputs_and_hints.clone().map(|(x, x3)| x.square() * x3);
210
211 Constraints::with_selector(
212 q_full_round,
213 [
214 inputs_and_hints.map(|(x, x3)| x.clone() * x.square() - x3),
215 linear_layer(sboxed_inputs, outputs, constants),
216 ]
217 .concat(),
218 )
219 });
220
221 let pre_computed = PreComputedRoundCircuit::<F>::init();
224 let ids = pre_computed.partial_round_id;
225
226 meta.create_gate("partial_round_gate", |meta| {
230 let inputs = register_cols
231 .iter()
232 .map(|col| meta.query_advice(*col, Rotation::cur()))
233 .collect::<Vec<_>>();
234 let round_constants = constant_cols
235 .iter()
236 .map(|col| meta.query_fixed(*col, Rotation::cur()))
237 .collect::<Vec<_>>();
238 let outputs = register_cols[0..WIDTH]
239 .iter()
240 .map(|col| meta.query_advice(*col, Rotation::next()))
241 .collect::<Vec<_>>();
242
243 let constraints = ids.to_expression(&inputs);
244
245 let output_lin_constraints =
246 (0..WIDTH - 1).map(|i| &round_constants[i] - &outputs[i] + &constraints[i]);
247 let input_pow_constraints = (WIDTH - 1..WIDTH + NB_SKIPS_CIRCUIT - 1)
248 .map(|i| &round_constants[i] - &inputs[i + 1] + &constraints[i]);
249 let output_pow_constraint: Expression<F> =
250 &round_constants[WIDTH + NB_SKIPS_CIRCUIT - 1] - &outputs[WIDTH - 1]
251 + &constraints[WIDTH + NB_SKIPS_CIRCUIT - 1];
252
253 let constraints = output_lin_constraints
254 .chain(input_pow_constraints)
255 .chain(once(output_pow_constraint))
256 .collect::<Vec<_>>();
257 Constraints::with_additive_selector(q_partial_round, constraints)
258 });
259
260 PoseidonConfig {
261 q_full_round,
262 q_partial_round,
263 register_cols,
264 constant_cols,
265 pre_computed,
266 }
267 }
268
269 fn load(&self, _layouter: &mut impl Layouter<F>) -> Result<(), Error> {
270 Ok(())
271 }
272}
273
274impl<F: PoseidonField> PoseidonChip<F> {
275 fn assign_constants_full(
277 &self,
278 region: &mut Region<'_, F>,
279 round_index: usize,
280 offset: usize,
281 ) -> Result<(), Error> {
282 let round_constants = if round_index == NB_FULL_ROUNDS + NB_PARTIAL_ROUNDS - 1 {
283 [F::ZERO; WIDTH]
284 } else {
285 F::ROUND_CONSTANTS[round_index + 1]
286 };
287 for (col, constant) in self.config.constant_cols[0..WIDTH].iter().zip(round_constants) {
288 region.assign_fixed(
289 || "load constant for a full round",
290 *col,
291 offset,
292 || Value::known(constant),
293 )?;
294 }
295 Ok(())
296 }
297
298 fn assign_constants_partial(
300 &self,
301 region: &mut Region<'_, F>,
302 round_batch_index: usize,
303 offset: usize,
304 ) -> Result<(), Error> {
305 for (col, constant) in self
306 .config
307 .constant_cols
308 .iter()
309 .zip(&self.config.pre_computed.round_constants[round_batch_index])
310 {
311 region.assign_fixed(
312 || format!("load constant for partial a round with {NB_SKIPS_CIRCUIT} skips"),
313 *col,
314 offset,
315 || Value::known(*constant),
316 )?;
317 }
318 Ok(())
319 }
320
321 fn full_round(
327 &self,
328 region: &mut Region<'_, F>,
329 inputs: &mut [AssignedNative<F>; WIDTH],
330 round_index: usize,
331 offset: &mut usize,
332 ) -> Result<(), Error> {
333 self.config.q_full_round.enable(region, *offset)?;
334 self.assign_constants_full(region, round_index, *offset)?;
335
336 for (x, col) in inputs.iter().zip(self.config.register_cols[WIDTH..(2 * WIDTH)].iter()) {
338 region.assign_advice(
339 || "full round hint",
340 *col,
341 *offset,
342 || x.value().map(|x| *x * x.square()),
343 )?;
344 }
345
346 *offset += 1;
347
348 let outputs = Value::from_iter(inputs.iter().map(|x| x.value().copied()))
349 .map(|inputs: Vec<F>| {
350 let mut inputs = inputs;
351 full_round_cpu(round_index, &mut inputs);
352 inputs
353 })
354 .transpose_vec(WIDTH);
355
356 outputs
357 .iter()
358 .zip(self.config.register_cols[0..WIDTH].iter())
359 .zip(inputs)
360 .try_for_each(|((output, column), input)| {
361 region
362 .assign_advice(|| "full round output", *column, *offset, || *output)
363 .map(|assigned| *input = assigned)
364 })
365 }
366
367 fn partial_round(
374 &self,
375 region: &mut Region<'_, F>,
376 inputs: &mut [AssignedNative<F>], round_batch_index: usize,
378 offset: &mut usize,
379 ) -> Result<(), Error> {
380 self.config.q_partial_round.enable(region, *offset)?;
381 self.assign_constants_partial(region, round_batch_index, *offset)?;
382
383 let outputs = Value::from_iter(inputs.iter().map(|x| x.value().copied()))
384 .map(|inputs: Vec<F>| {
385 let mut state = inputs;
386 let skip_advice_vals = partial_round_cpu_for_circuits(
387 &self.config.pre_computed,
388 round_batch_index,
389 &mut state,
390 );
391 for (col, skip_advice) in self.config.register_cols[WIDTH..WIDTH + NB_SKIPS_CIRCUIT]
392 .iter()
393 .zip(skip_advice_vals)
394 {
395 let _ = region.assign_advice(
396 || "partial round intermediary inputs",
397 *col,
398 *offset,
399 || Value::known(skip_advice),
400 );
401 }
402 state
403 })
404 .transpose_vec(WIDTH);
405
406 *offset += 1;
407 outputs
408 .iter()
409 .zip(self.config.register_cols[0..WIDTH].iter())
410 .zip(inputs)
411 .try_for_each(|((output, column), input)| {
412 region
413 .assign_advice(|| "partial round outputs", *column, *offset, || *output)
414 .map(|assigned| *input = assigned)
415 })
416 }
417
418 pub(super) fn permutation(
423 &self,
424 layouter: &mut impl Layouter<F>,
425 inputs: &AssignedRegister<F>,
426 ) -> Result<AssignedRegister<F>, Error> {
427 layouter.assign_region(
428 || "permutation layout",
429 |mut region| {
430 let mut offset: usize = 0;
431
432 let mut state: AssignedRegister<F> = self
433 .native_chip
434 .add_constants_in_region(
435 &mut region,
436 inputs,
437 &F::ROUND_CONSTANTS[0],
438 &mut offset,
439 )?
440 .try_into()
441 .unwrap();
442
443 assert!(
447 state.iter().zip(self.config.register_cols).all(|(acell, col)| {
448 let col1: Column<Advice> = acell.cell().column.try_into().unwrap();
449 col1 == col
450 })
451 );
452
453 for round_index in 0..NB_FULL_ROUNDS / 2 {
454 self.full_round(&mut region, &mut state, round_index, &mut offset)?;
455 }
456 for round_batch_index in 0..NB_PARTIAL_ROUNDS / (1 + NB_SKIPS_CIRCUIT) {
457 self.partial_round(&mut region, &mut state, round_batch_index, &mut offset)?;
458 }
459 for round_index in
460 (NB_FULL_ROUNDS / 2 + NB_PARTIAL_ROUNDS..).take(NB_FULL_ROUNDS / 2)
461 {
462 self.full_round(&mut region, &mut state, round_index, &mut offset)?;
463 }
464 Ok(state)
465 },
466 )
467 }
468}
469
470impl<F: PoseidonField> SpongeInstructions<F, AssignedNative<F>, AssignedNative<F>>
471 for PoseidonChip<F>
472{
473 type State = AssignedPoseidonState<F>;
474
475 fn init(
476 &self,
477 layouter: &mut impl Layouter<F>,
478 input_len: Option<usize>,
479 ) -> Result<Self::State, Error> {
480 let zero = self.native_chip.assign_fixed(layouter, F::ZERO)?;
481 let mut register: AssignedRegister<F> = vec![zero; WIDTH].try_into().unwrap();
482 register[RATE] = self.native_chip.assign_fixed(
483 layouter,
484 F::from_u128(input_len.map(|len| len as u128).unwrap_or(1 << 64)),
485 )?;
486 Ok(AssignedPoseidonState {
487 register,
488 queue: Vec::new(),
489 squeeze_position: 0,
490 input_len,
491 })
492 }
493
494 fn absorb(
495 &self,
496 _layouter: &mut impl Layouter<F>,
497 state: &mut Self::State,
498 inputs: &[AssignedNative<F>],
499 ) -> Result<(), Error> {
500 state.queue.extend(inputs.to_vec());
501 state.squeeze_position = 0;
502 Ok(())
503 }
504
505 fn squeeze(
506 &self,
507 layouter: &mut impl Layouter<F>,
508 state: &mut Self::State,
509 ) -> Result<AssignedNative<F>, Error> {
510 if state.squeeze_position > 0 {
511 if state.input_len.is_some() {
513 panic!(
514 "Attempting to squeeze multiple times a fixed-size Poseidon sponge (Circuit)."
515 )
516 };
517 debug_assert!(state.queue.is_empty());
518 let output = state.register[state.squeeze_position % RATE].clone();
519 state.squeeze_position = (state.squeeze_position + 1) % RATE;
520 return Ok(output);
521 }
522
523 match state.input_len {
524 None => {
525 let padding =
526 self.native_chip.assign_fixed(layouter, F::from(state.queue.len() as u64))?;
527 state.queue.push(padding);
528 }
529 Some(len) => {
530 if state.queue.len() != len {
531 panic!("Inconsistent lengths in fixed-size Poseidon sponge (Circuit). Expected: {}, found: {}.", len, state.queue.len())
532 };
533 }
534 }
535
536 for chunk in state.queue.chunks(RATE) {
537 for (entry, value) in state.register.iter_mut().zip(chunk.iter()) {
538 *entry = self.native_chip.add(layouter, entry, value)?;
539 }
540 state.register = self.permutation(layouter, &state.register)?;
541 }
542
543 state.queue = Vec::new();
544 state.squeeze_position = 1 % RATE;
545 Ok(state.register[0].clone())
546 }
547}
548
549#[cfg(any(test, feature = "testing"))]
550impl<F: PoseidonField> FromScratch<F> for PoseidonChip<F> {
551 type Config = (NativeConfig, PoseidonConfig<F>);
552
553 fn new_from_scratch(config: &Self::Config) -> Self {
554 let native_chip = NativeChip::new(&config.0, &());
555 PoseidonChip::new(&config.1, &native_chip)
556 }
557
558 fn configure_from_scratch(
559 meta: &mut ConstraintSystem<F>,
560 advice_columns: &mut Vec<Column<Advice>>,
561 fixed_columns: &mut Vec<Column<Fixed>>,
562 instance_columns: &[Column<Instance>; 2],
563 ) -> Self::Config {
564 let nb_advice_needed = std::cmp::max(NB_POSEIDON_ADVICE_COLS, NB_ARITH_COLS);
565 let nb_fixed_needed = std::cmp::max(NB_POSEIDON_FIXED_COLS, NB_ARITH_FIXED_COLS);
566
567 while advice_columns.len() < nb_advice_needed {
568 advice_columns.push(meta.advice_column());
569 }
570 while fixed_columns.len() < nb_fixed_needed {
571 fixed_columns.push(meta.fixed_column());
572 }
573
574 let native_config = NativeChip::configure(
575 meta,
576 &(
577 advice_columns[..NB_ARITH_COLS].try_into().unwrap(),
578 fixed_columns[..NB_ARITH_FIXED_COLS].try_into().unwrap(),
579 *instance_columns,
580 ),
581 );
582 let poseidon_config = PoseidonChip::configure(
583 meta,
584 &(
585 advice_columns[..NB_POSEIDON_ADVICE_COLS].try_into().unwrap(),
586 fixed_columns[..NB_POSEIDON_FIXED_COLS].try_into().unwrap(),
587 ),
588 );
589
590 (native_config, poseidon_config)
591 }
592
593 fn load_from_scratch(&self, layouter: &mut impl Layouter<F>) -> Result<(), Error> {
594 self.native_chip.load_from_scratch(layouter)
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use midnight_proofs::{circuit::SimpleFloorPlanner, dev::MockProver, plonk::Circuit};
601
602 use super::*;
603 use crate::{
604 field::NativeGadget,
605 hash::poseidon::{permutation_cpu, round_skips::PreComputedRoundCPU},
606 instructions::{sponge::tests::test_sponge, AssertionInstructions},
607 utils::circuit_modeling::{circuit_to_json, cost_measure_end, cost_measure_start},
608 };
609
610 #[derive(Clone, Debug, Default)]
611 struct PermCircuit<F> {
612 inputs: [Value<F>; WIDTH],
613 expected: [F; WIDTH],
614 }
615
616 impl<F: PoseidonField> Circuit<F> for PermCircuit<F> {
623 type Config = <PoseidonChip<F> as FromScratch<F>>::Config;
624
625 type FloorPlanner = SimpleFloorPlanner;
626
627 type Params = ();
628
629 fn without_witnesses(&self) -> Self {
630 unreachable!()
631 }
632
633 fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
634 let committed_instance_column = meta.instance_column();
635 let instance_column = meta.instance_column();
636 PoseidonChip::configure_from_scratch(
637 meta,
638 &mut vec![],
639 &mut vec![],
640 &[committed_instance_column, instance_column],
641 )
642 }
643
644 fn synthesize(
645 &self,
646 config: Self::Config,
647 mut layouter: impl Layouter<F>,
648 ) -> Result<(), Error> {
649 let poseidon_chip = PoseidonChip::new_from_scratch(&config);
650
651 let inputs: AssignedRegister<F> = poseidon_chip
652 .native_chip
653 .assign_many(&mut layouter, &self.inputs)?
654 .try_into()
655 .unwrap();
656 cost_measure_start(&mut layouter);
657 let outputs = poseidon_chip.permutation(&mut layouter, &inputs)?;
658 cost_measure_end(&mut layouter);
659
660 for (out, expected) in outputs.iter().zip(self.expected.iter()) {
661 poseidon_chip.native_chip.assert_equal_to_fixed(&mut layouter, out, *expected)?;
662 }
663
664 poseidon_chip.load_from_scratch(&mut layouter)
665 }
666 }
667
668 pub fn run_permutation_test<F>(inputs: [F; WIDTH], cost_model: bool)
669 where
670 F: PoseidonField + ff::FromUniformBytes<64> + Ord,
671 {
672 let pre_computed = PreComputedRoundCPU::init();
673 let mut expected = inputs;
674 permutation_cpu(&pre_computed, &mut expected);
675
676 let circuit = PermCircuit {
677 inputs: inputs.map(Value::known),
678 expected,
679 };
680
681 MockProver::run(&circuit, vec![vec![], vec![]]).unwrap().assert_satisfied();
682
683 if cost_model {
684 circuit_to_json("Poseidon", "one_permutation", circuit);
685 }
686 }
687
688 fn run_sponge_test<F>(field: &str, cost_model: bool)
689 where
690 F: PoseidonField + ff::FromUniformBytes<64> + Ord,
691 {
692 println!(
693 ">> Testing Poseidon Sponge (field {field}, {} partial-round skips)",
694 NB_SKIPS_CIRCUIT
695 );
696 test_sponge::<
697 F,
698 AssignedNative<F>,
699 AssignedNative<F>,
700 PoseidonChip<F>,
701 NativeGadget<F, _, _>,
702 >(cost_model, "Poseidon");
703 println!("=> Done.\n")
704 }
705
706 #[test]
707 fn permutation_test() {
708 let inputs = [midnight_curves::Fq::from(0); WIDTH];
709 run_permutation_test(inputs, true);
711 }
712
713 #[test]
714 fn sponge_test() {
715 run_sponge_test::<midnight_curves::Fq>("blstrs", true);
718 }
719}