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 .chunks_exact(8)
326 .map(|chunk| {
327 let mut buf = [0u8; 8];
328 buf.copy_from_slice(chunk);
329 Fp::try_from_le_bytes(buf).expect("non-canonical Fp limb")
330 })
331 .collect()
332 }
333
334 fn encode_fps(fs: &[Fp]) -> String {
335 let mut bytes = Vec::with_capacity(fs.len() * 8);
336 for f in fs {
337 bytes.extend_from_slice(&f.to_le_bytes());
338 }
339 bytes_to_hex(&bytes)
340 }
341
342 #[rstest]
343 fn permute_matches_go_reference_vectors() {
344 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
345 assert!(!suite.vectors.permute.is_empty(), "permute vectors empty");
346
347 for (i, v) in suite.vectors.permute.iter().enumerate() {
348 let input = decode_fps(&v.input);
349 assert_eq!(input.len(), WIDTH, "vector {i}: input width");
350
351 let mut state = [Fp::ZERO; WIDTH];
352 state.copy_from_slice(&input);
353 permute(&mut state);
354
355 assert_eq!(encode_fps(&state), v.output, "vector {i}: permute output");
356 }
357 }
358
359 #[rstest]
360 fn sponge_matches_go_reference_vectors() {
361 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
362 assert!(!suite.vectors.sponge.is_empty(), "sponge vectors empty");
363
364 for (i, v) in suite.vectors.sponge.iter().enumerate() {
365 let input = decode_fps(&v.input);
366 let out = hash_n_to_m_no_pad(&input, v.num_outputs);
367
368 assert_eq!(out.len(), v.num_outputs, "vector {i}: sponge output length");
369 assert_eq!(encode_fps(&out), v.output, "vector {i}: sponge output");
370 }
371 }
372
373 #[rstest]
374 fn hash_to_quintic_matches_go_reference_vectors() {
375 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
376 assert!(
377 !suite.vectors.hash_to_quintic.is_empty(),
378 "hash_to_quintic vectors empty",
379 );
380
381 for (i, v) in suite.vectors.hash_to_quintic.iter().enumerate() {
382 let input = decode_fps(&v.input);
383 let out = hash_to_quintic_extension(&input);
384
385 assert_eq!(
386 bytes_to_hex(&out.to_le_bytes()),
387 v.output,
388 "vector {i}: hash_to_quintic output",
389 );
390 }
391 }
392
393 #[rstest]
394 fn hash_two_to_one_matches_concatenation() {
395 let a = [
396 Fp::from_u64_reduce(1),
397 Fp::from_u64_reduce(2),
398 Fp::from_u64_reduce(3),
399 Fp::from_u64_reduce(4),
400 ];
401 let b = [
402 Fp::from_u64_reduce(5),
403 Fp::from_u64_reduce(6),
404 Fp::from_u64_reduce(7),
405 Fp::from_u64_reduce(8),
406 ];
407 let buf = [a[0], a[1], a[2], a[3], b[0], b[1], b[2], b[3]];
408 assert_eq!(hash_two_to_one(a, b), hash_n_to_hash_no_pad(&buf));
409 }
410
411 #[rstest]
412 fn hash_n_to_one_single_input_is_identity() {
413 let a = [
414 Fp::from_u64_reduce(11),
415 Fp::from_u64_reduce(22),
416 Fp::from_u64_reduce(33),
417 Fp::from_u64_reduce(44),
418 ];
419 assert_eq!(hash_n_to_one(&[a]), a);
420 }
421
422 #[rstest]
423 fn hash_n_to_one_matches_go_reference_vectors() {
424 let suite: VectorsFile = serde_json::from_str(VECTORS_JSON).expect("parse vectors");
425 assert!(
426 !suite.vectors.hash_n_to_one.is_empty(),
427 "hash_n_to_one vectors empty",
428 );
429
430 for (i, v) in suite.vectors.hash_n_to_one.iter().enumerate() {
431 let inputs: Vec<[Fp; HASH_OUT]> = v
432 .inputs
433 .iter()
434 .map(|hex| {
435 let limbs = decode_fps(hex);
436 assert_eq!(
437 limbs.len(),
438 HASH_OUT,
439 "vector {i}: each input must encode {HASH_OUT} limbs, was {}",
440 limbs.len(),
441 );
442 [limbs[0], limbs[1], limbs[2], limbs[3]]
443 })
444 .collect();
445
446 let out = hash_n_to_one(&inputs);
447
448 assert_eq!(
449 encode_fps(&out),
450 v.output,
451 "vector {i}: hash_n_to_one output (n={})",
452 inputs.len(),
453 );
454 }
455 }
456
457 #[rstest]
458 #[should_panic(expected = "hash_n_to_one requires at least one input")]
459 fn hash_n_to_one_empty_panics() {
460 let _ = hash_n_to_one(&[]);
461 }
462
463 #[rstest]
468 #[case(0)]
469 #[case(1)]
470 #[case(RATE - 1)]
471 #[case(RATE)]
472 fn empty_input_squeezes_zeros_up_to_rate(#[case] num_outputs: usize) {
473 let out = hash_n_to_m_no_pad(&[], num_outputs);
474 assert_eq!(out.len(), num_outputs, "output length mismatch");
475 for (i, fp) in out.iter().enumerate() {
476 assert!(fp.is_zero(), "slot {i} must be zero, was {fp:?}");
477 }
478 }
479
480 #[rstest]
484 #[case(1)]
485 #[case(RATE - 1)]
486 #[case(RATE)]
487 #[case(RATE + 1)]
488 #[case(2 * RATE - 1)]
489 #[case(2 * RATE)]
490 #[case(2 * RATE + 1)]
491 #[case(3 * RATE)]
492 fn sponge_handles_input_length_at_rate_boundaries(#[case] input_len: usize) {
493 let input: Vec<Fp> = (0..input_len)
494 .map(|i| Fp::from_u64_reduce(i as u64 + 1))
495 .collect();
496 let out_a = hash_n_to_m_no_pad(&input, HASH_OUT);
497 let out_b = hash_n_to_m_no_pad(&input, HASH_OUT);
498 assert_eq!(out_a.len(), HASH_OUT, "input_len {input_len}: length");
499 assert_eq!(out_a, out_b, "input_len {input_len}: not deterministic");
500 }
501
502 proptest! {
503 #[rstest]
505 fn prop_permute_deterministic(s in any::<[u64; WIDTH]>()) {
506 let state: [Fp; WIDTH] = core::array::from_fn(|i| Fp::from_u64_reduce(s[i]));
507 let mut s1 = state;
508 let mut s2 = state;
509 permute(&mut s1);
510 permute(&mut s2);
511 prop_assert_eq!(s1, s2);
512 }
513
514 #[rstest]
517 fn prop_permute_injective_on_pairs(
518 s1 in any::<[u64; WIDTH]>(),
519 s2 in any::<[u64; WIDTH]>(),
520 ) {
521 let mut state1: [Fp; WIDTH] = core::array::from_fn(|i| Fp::from_u64_reduce(s1[i]));
522 let mut state2: [Fp; WIDTH] = core::array::from_fn(|i| Fp::from_u64_reduce(s2[i]));
523 prop_assume!(state1 != state2);
524 permute(&mut state1);
525 permute(&mut state2);
526 prop_assert_ne!(state1, state2);
527 }
528
529 #[rstest]
531 fn prop_hash_no_pad_deterministic(
532 input in proptest::collection::vec(arb_fp(), 0..32),
533 ) {
534 prop_assert_eq!(hash_no_pad(&input), hash_no_pad(&input));
535 }
536
537 #[rstest]
539 fn prop_hash_two_to_one_equals_concat(
540 a in any::<[u64; HASH_OUT]>(),
541 b in any::<[u64; HASH_OUT]>(),
542 ) {
543 let a_fp: [Fp; HASH_OUT] = core::array::from_fn(|i| Fp::from_u64_reduce(a[i]));
544 let b_fp: [Fp; HASH_OUT] = core::array::from_fn(|i| Fp::from_u64_reduce(b[i]));
545 let concat = [
546 a_fp[0], a_fp[1], a_fp[2], a_fp[3],
547 b_fp[0], b_fp[1], b_fp[2], b_fp[3],
548 ];
549 prop_assert_eq!(hash_two_to_one(a_fp, b_fp), hash_no_pad(&concat));
550 }
551
552 #[rstest]
555 fn prop_hash_n_to_one_left_fold(
556 inputs in proptest::collection::vec(any::<[u64; HASH_OUT]>(), 1..6),
557 ) {
558 let inputs_fp: Vec<[Fp; HASH_OUT]> = inputs
559 .iter()
560 .map(|raw| core::array::from_fn(|j| Fp::from_u64_reduce(raw[j])))
561 .collect();
562 let mut expected = inputs_fp[0];
563 for next in &inputs_fp[1..] {
564 expected = hash_two_to_one(expected, *next);
565 }
566 prop_assert_eq!(hash_n_to_one(&inputs_fp), expected);
567 }
568
569 #[rstest]
571 fn prop_hash_to_quintic_extension_deterministic(
572 input in proptest::collection::vec(arb_fp(), 0..32),
573 ) {
574 prop_assert_eq!(
575 hash_to_quintic_extension(&input),
576 hash_to_quintic_extension(&input),
577 );
578 }
579
580 #[rstest]
583 fn prop_hash_two_to_quintic_matches_concat(
584 a in any::<[u64; 5]>(),
585 b in any::<[u64; 5]>(),
586 ) {
587 let a_fp5 = Fp5::from_u64s_reduce(a);
588 let b_fp5 = Fp5::from_u64s_reduce(b);
589 let mut concat = [Fp::ZERO; 10];
590 concat[..5].copy_from_slice(&a_fp5.0);
591 concat[5..].copy_from_slice(&b_fp5.0);
592 prop_assert_eq!(
593 hash_two_to_quintic(a_fp5, b_fp5),
594 hash_to_quintic_extension(&concat),
595 );
596 }
597 }
598}