nautilus_lighter/signing/hash/
poseidon2.rs1use super::params::{
39 EXTERNAL_CONSTANTS, INTERNAL_CONSTANTS, MATRIX_DIAG_12, RATE, ROUNDS_F_HALF, WIDTH,
40};
41use crate::signing::field::{Fp, Fp5};
42
43pub const HASH_OUT: usize = 4;
45
46pub fn permute(state: &mut [Fp; WIDTH]) {
48 external_linear_layer(state);
49 full_rounds(state, 0);
50 partial_rounds(state);
51 full_rounds(state, ROUNDS_F_HALF);
52}
53
54#[inline]
61fn absorb(input: &[Fp]) -> [Fp; WIDTH] {
62 let mut state = [Fp::ZERO; WIDTH];
63
64 let mut i = 0;
65 while i < input.len() {
66 let chunk_end = core::cmp::min(i + RATE, input.len());
67 for (j, &val) in input[i..chunk_end].iter().enumerate() {
68 state[j] = val;
69 }
70 permute(&mut state);
71 i += RATE;
72 }
73
74 state
75}
76
77#[must_use]
87pub fn hash_n_to_m_no_pad(input: &[Fp], num_outputs: usize) -> Vec<Fp> {
88 let mut state = absorb(input);
89
90 let mut out = Vec::with_capacity(num_outputs);
91 while out.len() < num_outputs {
92 for slot in &state[..RATE] {
93 out.push(*slot);
94 if out.len() == num_outputs {
95 return out;
96 }
97 }
98 permute(&mut state);
99 }
100 out
101}
102
103#[must_use]
108pub fn hash_n_to_hash_no_pad(input: &[Fp]) -> [Fp; HASH_OUT] {
109 let state = absorb(input);
110 [state[0], state[1], state[2], state[3]]
111}
112
113#[must_use]
115pub fn hash_no_pad(input: &[Fp]) -> [Fp; HASH_OUT] {
116 hash_n_to_hash_no_pad(input)
117}
118
119#[must_use]
121pub fn hash_two_to_one(a: [Fp; HASH_OUT], b: [Fp; HASH_OUT]) -> [Fp; HASH_OUT] {
122 let buf = [a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3]];
123 hash_n_to_hash_no_pad(&buf)
124}
125
126#[must_use]
135pub fn hash_n_to_one(inputs: &[[Fp; HASH_OUT]]) -> [Fp; HASH_OUT] {
136 assert!(
137 !inputs.is_empty(),
138 "hash_n_to_one requires at least one input"
139 );
140
141 if inputs.len() == 1 {
142 return inputs[0];
143 }
144
145 let mut acc = hash_two_to_one(inputs[0], inputs[1]);
146 for next in &inputs[2..] {
147 acc = hash_two_to_one(acc, *next);
148 }
149 acc
150}
151
152#[must_use]
160pub fn hash_to_quintic_extension(input: &[Fp]) -> Fp5 {
161 let state = absorb(input);
162 Fp5([state[0], state[1], state[2], state[3], state[4]])
163}
164
165#[must_use]
172pub fn hash_two_to_quintic(a: Fp5, b: Fp5) -> Fp5 {
173 let mut preimage = [Fp::ZERO; 10];
174 preimage[..5].copy_from_slice(&a.0);
175 preimage[5..].copy_from_slice(&b.0);
176 hash_to_quintic_extension(&preimage)
177}
178
179fn sbox_full(state: &mut [Fp; WIDTH]) {
181 for slot in state.iter_mut() {
182 *slot = sbox(*slot);
183 }
184}
185
186#[inline]
188fn sbox(x: Fp) -> Fp {
189 let x2 = x.square();
190 let x6 = (x2 * x).square();
191 x6 * x
192}
193
194fn external_linear_layer(state: &mut [Fp; WIDTH]) {
198 for block in 0..3 {
199 let base = block * 4;
200 let s0 = state[base];
201 let s1 = state[base + 1];
202 let s2 = state[base + 2];
203 let s3 = state[base + 3];
204 let t0 = s0 + s1;
205 let t1 = s2 + s3;
206 let t2 = t0 + t1;
207 let t3 = t2 + s1;
208 let t4 = t2 + s3;
209 let t5 = s0 + s0;
210 let t6 = s2 + s2;
211 state[base] = t3 + t0;
212 state[base + 1] = t6 + t3;
213 state[base + 2] = t1 + t4;
214 state[base + 3] = t5 + t4;
215 }
216
217 let mut sums = [Fp::ZERO; 4];
218
219 for k in 0..4 {
220 let mut j = 0;
221
222 while j < WIDTH {
223 sums[k] += state[j + k];
224 j += 4;
225 }
226 }
227
228 for i in 0..WIDTH {
229 state[i] += sums[i % 4];
230 }
231}
232
233fn internal_linear_layer(state: &mut [Fp; WIDTH]) {
236 let mut sum = state[0];
237
238 for slot in &state[1..] {
239 sum += *slot;
240 }
241
242 for i in 0..WIDTH {
243 state[i] = state[i] * MATRIX_DIAG_12[i] + sum;
244 }
245}
246
247fn full_rounds(state: &mut [Fp; WIDTH], start: usize) {
248 for round_consts in &EXTERNAL_CONSTANTS[start..start + ROUNDS_F_HALF] {
249 for (slot, rc) in state.iter_mut().zip(round_consts.iter()) {
250 *slot += *rc;
251 }
252 sbox_full(state);
253 external_linear_layer(state);
254 }
255}
256
257fn partial_rounds(state: &mut [Fp; WIDTH]) {
258 for rc in &INTERNAL_CONSTANTS {
259 state[0] += *rc;
260 state[0] = sbox(state[0]);
261 internal_linear_layer(state);
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use proptest::prelude::*;
268 use rstest::rstest;
269 use serde::Deserialize;
270
271 use super::*;
272 use crate::signing::fixtures::{arb_fp, bytes_to_hex, hex_to_bytes};
273
274 const VECTORS_JSON: &str = include_str!(concat!(
275 env!("CARGO_MANIFEST_DIR"),
276 "/test_data/signing_hash_poseidon2_vectors.json",
277 ));
278
279 #[derive(Debug, Deserialize)]
280 struct VectorsFile {
281 vectors: Vectors,
282 }
283
284 #[derive(Debug, Deserialize)]
285 struct Vectors {
286 permute: Vec<PermuteVector>,
287 sponge: Vec<SpongeVector>,
288 hash_to_quintic: Vec<QuinticVector>,
289 hash_n_to_one: Vec<HashNToOneVector>,
290 }
291
292 #[derive(Debug, Deserialize)]
293 struct PermuteVector {
294 input: String,
295 output: String,
296 }
297
298 #[derive(Debug, Deserialize)]
299 struct SpongeVector {
300 input: String,
301 num_outputs: usize,
302 output: String,
303 }
304
305 #[derive(Debug, Deserialize)]
306 struct QuinticVector {
307 input: String,
308 output: String,
309 }
310
311 #[derive(Debug, Deserialize)]
312 struct HashNToOneVector {
313 inputs: Vec<String>,
314 output: String,
315 }
316
317 fn decode_fps(hex: &str) -> Vec<Fp> {
318 let bytes = hex_to_bytes(hex);
319 assert!(
320 bytes.len().is_multiple_of(8),
321 "fp encoding must be 8-byte multiples, was {} bytes",
322 bytes.len(),
323 );
324 bytes
325 .as_chunks::<8>()
326 .0
327 .iter()
328 .map(|chunk| {
329 let mut buf = [0u8; 8];
330 buf.copy_from_slice(chunk);
331 Fp::try_from_le_bytes(buf).expect("non-canonical Fp limb")
332 })
333 .collect()
334 }
335
336 fn encode_fps(fs: &[Fp]) -> String {
337 let mut bytes = Vec::with_capacity(fs.len() * 8);
338 for f in fs {
339 bytes.extend_from_slice(&f.to_le_bytes());
340 }
341 bytes_to_hex(&bytes)
342 }
343
344 #[rstest]
345 fn permute_matches_go_reference_vectors() {
346 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
347 assert!(!suite.vectors.permute.is_empty(), "permute vectors empty");
348
349 for (i, v) in suite.vectors.permute.iter().enumerate() {
350 let input = decode_fps(&v.input);
351 assert_eq!(input.len(), WIDTH, "vector {i}: input width");
352
353 let mut state = [Fp::ZERO; WIDTH];
354 state.copy_from_slice(&input);
355 permute(&mut state);
356
357 assert_eq!(encode_fps(&state), v.output, "vector {i}: permute output");
358 }
359 }
360
361 #[rstest]
362 fn sponge_matches_go_reference_vectors() {
363 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
364 assert!(!suite.vectors.sponge.is_empty(), "sponge vectors empty");
365
366 for (i, v) in suite.vectors.sponge.iter().enumerate() {
367 let input = decode_fps(&v.input);
368 let out = hash_n_to_m_no_pad(&input, v.num_outputs);
369
370 assert_eq!(out.len(), v.num_outputs, "vector {i}: sponge output length");
371 assert_eq!(encode_fps(&out), v.output, "vector {i}: sponge output");
372 }
373 }
374
375 #[rstest]
376 fn hash_to_quintic_matches_go_reference_vectors() {
377 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
378 assert!(
379 !suite.vectors.hash_to_quintic.is_empty(),
380 "hash_to_quintic vectors empty",
381 );
382
383 for (i, v) in suite.vectors.hash_to_quintic.iter().enumerate() {
384 let input = decode_fps(&v.input);
385 let out = hash_to_quintic_extension(&input);
386
387 assert_eq!(
388 bytes_to_hex(&out.to_le_bytes()),
389 v.output,
390 "vector {i}: hash_to_quintic output",
391 );
392 }
393 }
394
395 #[rstest]
396 fn hash_two_to_one_matches_concatenation() {
397 let a = [
398 Fp::from_u64_reduce(1),
399 Fp::from_u64_reduce(2),
400 Fp::from_u64_reduce(3),
401 Fp::from_u64_reduce(4),
402 ];
403 let b = [
404 Fp::from_u64_reduce(5),
405 Fp::from_u64_reduce(6),
406 Fp::from_u64_reduce(7),
407 Fp::from_u64_reduce(8),
408 ];
409 let buf = [a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3]];
410 assert_eq!(hash_two_to_one(a, b), hash_n_to_hash_no_pad(&buf));
411 }
412
413 #[rstest]
414 fn hash_n_to_one_single_input_is_identity() {
415 let a = [
416 Fp::from_u64_reduce(11),
417 Fp::from_u64_reduce(22),
418 Fp::from_u64_reduce(33),
419 Fp::from_u64_reduce(44),
420 ];
421 assert_eq!(hash_n_to_one(&[a]), a);
422 }
423
424 #[rstest]
425 fn hash_n_to_one_matches_go_reference_vectors() {
426 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
427 assert!(
428 !suite.vectors.hash_n_to_one.is_empty(),
429 "hash_n_to_one vectors empty",
430 );
431
432 for (i, v) in suite.vectors.hash_n_to_one.iter().enumerate() {
433 let inputs: Vec<[Fp; HASH_OUT]> = v
434 .inputs
435 .iter()
436 .map(|hex| {
437 let limbs = decode_fps(hex);
438 assert_eq!(
439 limbs.len(),
440 HASH_OUT,
441 "vector {i}: each input must encode {HASH_OUT} limbs, was {}",
442 limbs.len(),
443 );
444 [limbs[0], limbs[1], limbs[2], limbs[3]]
445 })
446 .collect();
447
448 let out = hash_n_to_one(&inputs);
449
450 assert_eq!(
451 encode_fps(&out),
452 v.output,
453 "vector {i}: hash_n_to_one output (n={})",
454 inputs.len(),
455 );
456 }
457 }
458
459 #[rstest]
460 #[should_panic(expected = "hash_n_to_one requires at least one input")]
461 fn hash_n_to_one_empty_panics() {
462 let _ = hash_n_to_one(&[]);
463 }
464
465 #[rstest]
470 #[case(0)]
471 #[case(1)]
472 #[case(RATE - 1)]
473 #[case(RATE)]
474 fn empty_input_squeezes_zeros_up_to_rate(#[case] num_outputs: usize) {
475 let out = hash_n_to_m_no_pad(&[], num_outputs);
476 assert_eq!(out.len(), num_outputs, "output length mismatch");
477 for (i, fp) in out.iter().enumerate() {
478 assert!(fp.is_zero(), "slot {i} must be zero, was {fp:?}");
479 }
480 }
481
482 #[rstest]
486 #[case(1)]
487 #[case(RATE - 1)]
488 #[case(RATE)]
489 #[case(RATE + 1)]
490 #[case(2 * RATE - 1)]
491 #[case(2 * RATE)]
492 #[case(2 * RATE + 1)]
493 #[case(3 * RATE)]
494 fn sponge_handles_input_length_at_rate_boundaries(#[case] input_len: usize) {
495 let input: Vec<Fp> = (0..input_len)
496 .map(|i| Fp::from_u64_reduce(i as u64 + 1))
497 .collect();
498 let out_a = hash_n_to_m_no_pad(&input, HASH_OUT);
499 let out_b = hash_n_to_m_no_pad(&input, HASH_OUT);
500 assert_eq!(out_a.len(), HASH_OUT, "input_len {input_len}: length");
501 assert_eq!(out_a, out_b, "input_len {input_len}: not deterministic");
502 }
503
504 proptest! {
505 #[rstest]
507 fn prop_permute_deterministic(s in any::<[u64; WIDTH]>()) {
508 let state: [Fp; WIDTH] = core::array::from_fn(|i| Fp::from_u64_reduce(s[i]));
509 let mut s1 = state;
510 let mut s2 = state;
511 permute(&mut s1);
512 permute(&mut s2);
513 prop_assert_eq!(s1, s2);
514 }
515
516 #[rstest]
519 fn prop_permute_injective_on_pairs(
520 s1 in any::<[u64; WIDTH]>(),
521 s2 in any::<[u64; WIDTH]>(),
522 ) {
523 let mut state1: [Fp; WIDTH] = core::array::from_fn(|i| Fp::from_u64_reduce(s1[i]));
524 let mut state2: [Fp; WIDTH] = core::array::from_fn(|i| Fp::from_u64_reduce(s2[i]));
525 prop_assume!(state1 != state2);
526 permute(&mut state1);
527 permute(&mut state2);
528 prop_assert_ne!(state1, state2);
529 }
530
531 #[rstest]
533 fn prop_hash_no_pad_deterministic(
534 input in proptest::collection::vec(arb_fp(), 0..32),
535 ) {
536 prop_assert_eq!(hash_no_pad(&input), hash_no_pad(&input));
537 }
538
539 #[rstest]
541 fn prop_hash_two_to_one_equals_concat(
542 a in any::<[u64; HASH_OUT]>(),
543 b in any::<[u64; HASH_OUT]>(),
544 ) {
545 let a_fp: [Fp; HASH_OUT] = core::array::from_fn(|i| Fp::from_u64_reduce(a[i]));
546 let b_fp: [Fp; HASH_OUT] = core::array::from_fn(|i| Fp::from_u64_reduce(b[i]));
547 let concat = [
548 a_fp[0], a_fp[1], a_fp[2], a_fp[3],
549 b_fp[0], b_fp[1], b_fp[2], b_fp[3],
550 ];
551 prop_assert_eq!(hash_two_to_one(a_fp, b_fp), hash_no_pad(&concat));
552 }
553
554 #[rstest]
557 fn prop_hash_n_to_one_left_fold(
558 inputs in proptest::collection::vec(any::<[u64; HASH_OUT]>(), 1..6),
559 ) {
560 let inputs_fp: Vec<[Fp; HASH_OUT]> = inputs
561 .iter()
562 .map(|raw| core::array::from_fn(|j| Fp::from_u64_reduce(raw[j])))
563 .collect();
564 let mut expected = inputs_fp[0];
565 for next in &inputs_fp[1..] {
566 expected = hash_two_to_one(expected, *next);
567 }
568 prop_assert_eq!(hash_n_to_one(&inputs_fp), expected);
569 }
570
571 #[rstest]
573 fn prop_hash_to_quintic_extension_deterministic(
574 input in proptest::collection::vec(arb_fp(), 0..32),
575 ) {
576 prop_assert_eq!(
577 hash_to_quintic_extension(&input),
578 hash_to_quintic_extension(&input),
579 );
580 }
581
582 #[rstest]
585 fn prop_hash_two_to_quintic_matches_concat(
586 a in any::<[u64; 5]>(),
587 b in any::<[u64; 5]>(),
588 ) {
589 let a_fp5 = Fp5::from_u64s_reduce(a);
590 let b_fp5 = Fp5::from_u64s_reduce(b);
591 let mut concat = [Fp::ZERO; 10];
592 concat[..5].copy_from_slice(&a_fp5.0);
593 concat[5..].copy_from_slice(&b_fp5.0);
594 prop_assert_eq!(
595 hash_two_to_quintic(a_fp5, b_fp5),
596 hash_to_quintic_extension(&concat),
597 );
598 }
599 }
600}