neptune/
poseidon_alt.rs

1//! This module contains the 'correct' and 'dynamic' versions of Poseidon hashing.
2//! These are tested (in `poseidon::test`) to be equivalent to the 'static optimized' version
3//! used for actual hashing by the neptune library.
4use crate::poseidon::{Arity, Poseidon};
5use crate::{matrix, quintic_s_box};
6use ff::PrimeField;
7
8////////////////////////////////////////////////////////////////////////////////
9/// Correct
10///
11/// This code path implements a naive and evidently correct poseidon hash.
12
13/// The returned element is the second poseidon element, the first is the arity tag.
14pub(crate) fn hash_correct<F, A>(p: &mut Poseidon<'_, F, A>) -> F
15where
16    F: PrimeField,
17    A: Arity<F>,
18{
19    // This counter is incremented when a round constants is read. Therefore, the round constants never repeat.
20    // The first full round should use the initial constants.
21    full_round(p);
22
23    for _ in 1..p.constants.half_full_rounds {
24        full_round(p);
25    }
26
27    partial_round(p);
28
29    for _ in 1..p.constants.partial_rounds {
30        partial_round(p);
31    }
32
33    for _ in 0..p.constants.half_full_rounds {
34        full_round(p);
35    }
36
37    p.elements[1]
38}
39
40pub(crate) fn full_round<F, A>(p: &mut Poseidon<'_, F, A>)
41where
42    F: PrimeField,
43    A: Arity<F>,
44{
45    // Apply the quintic S-Box to all elements, after adding the round key.
46    // Round keys are added in the S-box to match circuits (where the addition is free)
47    // and in preparation for the shift to adding round keys after (rather than before) applying the S-box.
48
49    let pre_round_keys = p
50        .constants
51        .round_constants
52        .as_ref()
53        .unwrap()
54        .iter()
55        .skip(p.constants_offset)
56        .map(Some);
57
58    p.elements
59        .iter_mut()
60        .zip(pre_round_keys)
61        .for_each(|(l, pre)| {
62            quintic_s_box(l, pre, None);
63        });
64
65    p.constants_offset += p.elements.len();
66
67    // M(B)
68    // Multiply the elements by the constant MDS matrix
69    p.product_mds();
70}
71
72/// The partial round is the same as the full round, with the difference that we apply the S-Box only to the first bitflags poseidon leaf.
73pub(crate) fn partial_round<F, A>(p: &mut Poseidon<'_, F, A>)
74where
75    F: PrimeField,
76    A: Arity<F>,
77{
78    // Every element of the hash buffer is incremented by the round constants
79    add_round_constants(p);
80
81    // Apply the quintic S-Box to the first element
82    quintic_s_box(&mut p.elements[0], None, None);
83
84    // Multiply the elements by the constant MDS matrix
85    p.product_mds();
86}
87
88////////////////////////////////////////////////////////////////////////////////
89/// Dynamic
90///
91/// This code path implements a code path which dynamically calculates compressed round constants one-deep.
92/// It serves as a bridge between the 'correct' and fully, statically optimized implementations.
93/// Comments reference notation also expanded in matrix.rs and help clarify the relationship between
94/// our optimizations and those described in the paper.
95
96pub(crate) fn hash_optimized_dynamic<F, A>(p: &mut Poseidon<'_, F, A>) -> F
97where
98    F: PrimeField,
99    A: Arity<F>,
100{
101    // The first full round should use the initial constants.
102    full_round_dynamic(p, true, true);
103
104    for _ in 1..(p.constants.half_full_rounds) {
105        full_round_dynamic(p, false, true);
106    }
107
108    partial_round_dynamic(p);
109
110    for _ in 1..p.constants.partial_rounds {
111        partial_round(p);
112    }
113
114    for _ in 0..p.constants.half_full_rounds {
115        full_round_dynamic(p, true, false);
116    }
117
118    p.elements[1]
119}
120
121pub(crate) fn full_round_dynamic<F, A>(
122    p: &mut Poseidon<'_, F, A>,
123    add_current_round_keys: bool,
124    absorb_next_round_keys: bool,
125) where
126    F: PrimeField,
127    A: Arity<F>,
128{
129    // NOTE: decrease in performance is expected when using this pathway.
130    // We seek to preserve correctness while transforming the algorithm to an eventually more performant one.
131
132    // Round keys are added in the S-box to match circuits (where the addition is free).
133    // If requested, add round keys synthesized from following round after (rather than before) applying the S-box.
134    let pre_round_keys = p
135        .constants
136        .round_constants
137        .as_ref()
138        .unwrap()
139        .iter()
140        .skip(p.constants_offset)
141        .map(|x| {
142            if add_current_round_keys {
143                Some(x)
144            } else {
145                None
146            }
147        });
148
149    if absorb_next_round_keys {
150        // Using the notation from `test_inverse` in matrix.rs:
151        // S
152        let post_vec = p
153            .constants
154            .round_constants
155            .as_ref()
156            .unwrap()
157            .iter()
158            .skip(
159                p.constants_offset
160                    + if add_current_round_keys {
161                        p.elements.len()
162                    } else {
163                        0
164                    },
165            )
166            .take(p.elements.len())
167            .copied()
168            .collect::<Vec<_>>();
169
170        // Compute the constants which should be added *before* the next `product_mds`.
171        // in order to have the same effect as adding the given constants *after* the next `product_mds`.
172
173        // M^-1(S)
174        let inverted_vec = matrix::left_apply_matrix(&p.constants.mds_matrices.m_inv, &post_vec);
175
176        // M(M^-1(S))
177        let original = matrix::left_apply_matrix(&p.constants.mds_matrices.m, &inverted_vec);
178
179        // S = M(M^-1(S))
180        assert_eq!(&post_vec, &original, "Oh no, the inversion trick failed.");
181
182        let post_round_keys = inverted_vec.iter();
183
184        // S-Box Output = B.
185        // With post-add, result is B + M^-1(S).
186        p.elements
187            .iter_mut()
188            .zip(pre_round_keys.zip(post_round_keys))
189            .for_each(|(l, (pre, post))| {
190                quintic_s_box(l, pre, Some(post));
191            });
192    } else {
193        p.elements
194            .iter_mut()
195            .zip(pre_round_keys)
196            .for_each(|(l, pre)| {
197                quintic_s_box(l, pre, None);
198            });
199    }
200    let mut consumed = 0;
201    if add_current_round_keys {
202        consumed += p.elements.len()
203    };
204    if absorb_next_round_keys {
205        consumed += p.elements.len()
206    };
207    p.constants_offset += consumed;
208
209    // If absorb_next_round_keys
210    //   M(B + M^-1(S)
211    // else
212    //   M(B)
213    // Multiply the elements by the constant MDS matrix
214    p.product_mds();
215}
216
217pub(crate) fn partial_round_dynamic<F, A>(p: &mut Poseidon<'_, F, A>)
218where
219    F: PrimeField,
220    A: Arity<F>,
221{
222    // Apply the quintic S-Box to the first element
223    quintic_s_box(&mut p.elements[0], None, None);
224
225    // Multiply the elements by the constant MDS matrix
226    p.product_mds();
227}
228
229/// For every leaf, add the round constants with index defined by the constants offset, and increment the
230/// offset.
231fn add_round_constants<F, A>(p: &mut Poseidon<'_, F, A>)
232where
233    F: PrimeField,
234    A: Arity<F>,
235{
236    for (element, round_constant) in p.elements.iter_mut().zip(
237        p.constants
238            .round_constants
239            .as_ref()
240            .unwrap()
241            .iter()
242            .skip(p.constants_offset),
243    ) {
244        element.add_assign(round_constant);
245    }
246
247    p.constants_offset += p.elements.len();
248}