miden_core/chiplets/hasher.rs
1//! Low-level Poseidon2 hasher functions and constants.
2//!
3//! This module provides core hashing primitives for the Poseidon2 hash function, including:
4//! - Constants defining the hasher state layout (STATE_WIDTH, RATE_LEN, NUM_ROUNDS)
5//! - Pass-through functions for common hash operations (merge, hash_elements)
6//! - Step-by-step permutation functions for fine-grained control (apply_round, apply_permutation)
7//!
8//! This module serves as a thin wrapper around `miden_crypto::hash::Poseidon2`, providing
9//! a consistent interface for the Miden VM's hashing needs. For higher-level hasher chiplet
10//! functionality, see the trace and processor modules.
11
12use miden_crypto::Word as Digest;
13
14use super::Felt;
15pub use crate::crypto::hash::Poseidon2 as Hasher;
16
17/// Number of field element needed to represent the sponge state for the hash function.
18///
19/// This value is set to 12: 8 elements are reserved for rate and the remaining 4 elements are
20/// reserved for capacity. This configuration enables computation of 2-to-1 hash in a single
21/// permutation.
22pub const STATE_WIDTH: usize = Hasher::STATE_WIDTH;
23
24/// Number of field elements in the rate portion of the hasher's state.
25pub const RATE_LEN: usize = 8;
26
27/// Number of "round steps" used by the hasher chiplet per permutation.
28///
29/// For Poseidon2, we model the permutation as 31 step transitions. This corresponds to an
30/// initial external linear layer, 4 initial external (partial) rounds, 22 internal (full) rounds,
31/// and 4 terminal external (partial) rounds:
32/// - step 0: initial external linear layer
33/// - steps 1..=4: initial external rounds
34/// - steps 5..=26: internal rounds
35/// - steps 27..=30: terminal external rounds
36///
37/// This yields a 32-row hasher cycle (input row + 31 steps).
38pub const NUM_ROUNDS: usize = 31;
39
40// PASS-THROUGH FUNCTIONS
41// ================================================================================================
42
43/// Returns a hash of two digests. This method is intended for use in construction of Merkle trees.
44#[inline(always)]
45pub fn merge(values: &[Digest; 2]) -> Digest {
46 Hasher::merge(values)
47}
48
49/// Returns a hash of two digests with a specified domain.
50#[inline(always)]
51pub fn merge_in_domain(values: &[Digest; 2], domain: Felt) -> Digest {
52 Hasher::merge_in_domain(values, domain)
53}
54
55/// Returns a hash of the provided list of field elements.
56#[inline(always)]
57pub fn hash_elements(elements: &[Felt]) -> Digest {
58 Hasher::hash_elements(elements)
59}
60
61/// Applies a single Poseidon2 "step" to the provided state.
62///
63/// The step number must be specified via `round` parameter, which must be between 0 and 30
64/// (both inclusive).
65#[inline(always)]
66pub fn apply_round(state: &mut [Felt; STATE_WIDTH], round: usize) {
67 apply_poseidon2_step(state, round)
68}
69
70/// Applies the Poseidon2 permutation to the provided state.
71#[inline(always)]
72pub fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) {
73 Hasher::apply_permutation(state)
74}
75
76// POSEIDON2 STEP IMPLEMENTATION
77// ================================================================================================
78
79/// Applies a single Poseidon2 permutation step to the state.
80///
81/// The step number maps to the hasher chiplet trace rows:
82/// - step 0: initial external linear layer
83/// - steps 1..=4: initial external rounds
84/// - steps 5..=26: internal rounds
85/// - steps 27..=30: terminal external rounds
86#[inline(always)]
87fn apply_poseidon2_step(state: &mut [Felt; STATE_WIDTH], step: usize) {
88 match step {
89 0 => {
90 // Initial external linear layer.
91 Hasher::apply_matmul_external(state);
92 },
93 1..=4 => {
94 // Initial external partial rounds.
95 Hasher::add_rc(state, &Hasher::ARK_EXT_INITIAL[step - 1]);
96 Hasher::apply_sbox(state);
97 Hasher::apply_matmul_external(state);
98 },
99 5..=26 => {
100 // Internal full rounds.
101 state[0] += Hasher::ARK_INT[step - 5];
102 state[0] = state[0].exp_const_u64::<7>();
103 Hasher::matmul_internal(state, Hasher::MAT_DIAG);
104 },
105 27..=30 => {
106 // Terminal external partial rounds.
107 Hasher::add_rc(state, &Hasher::ARK_EXT_TERMINAL[step - 27]);
108 Hasher::apply_sbox(state);
109 Hasher::apply_matmul_external(state);
110 },
111 _ => panic!("invalid poseidon2 step {step}, expected 0..30"),
112 }
113}
114
115// TESTS
116// ================================================================================================
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 /// Verifies that applying all 31 steps produces the same result as `apply_permutation`.
123 #[test]
124 fn apply_round_matches_permutation() {
125 // Test with zeros
126 let mut state_stepwise = [Felt::ZERO; STATE_WIDTH];
127 let mut state_permutation = [Felt::ZERO; STATE_WIDTH];
128
129 for i in 0..NUM_ROUNDS {
130 apply_round(&mut state_stepwise, i);
131 }
132 apply_permutation(&mut state_permutation);
133
134 assert_eq!(state_stepwise, state_permutation, "mismatch with zero state");
135
136 // Test with sequential values
137 let mut state_stepwise: [Felt; STATE_WIDTH] = core::array::from_fn(|i| Felt::new(i as u64));
138 let mut state_permutation = state_stepwise;
139
140 for i in 0..NUM_ROUNDS {
141 apply_round(&mut state_stepwise, i);
142 }
143 apply_permutation(&mut state_permutation);
144
145 assert_eq!(state_stepwise, state_permutation, "mismatch with sequential state");
146
147 // Test with arbitrary values
148 let mut state_stepwise: [Felt; STATE_WIDTH] = [
149 Felt::new(0x123456789abcdef0_u64),
150 Felt::new(0xfedcba9876543210_u64),
151 Felt::new(0x0011223344556677_u64),
152 Felt::new(0x8899aabbccddeeff_u64),
153 Felt::new(0xdeadbeefcafebabe_u64),
154 Felt::new(0x1234567890abcdef_u64),
155 Felt::new(0x1234567890abcdef_u64),
156 Felt::new(0x0badc0debadf00d0_u64),
157 Felt::new(0x1111111111111111_u64),
158 Felt::new(0x2222222222222222_u64),
159 Felt::new(0x3333333333333333_u64),
160 Felt::new(0x4444444444444444_u64),
161 ];
162 let mut state_permutation = state_stepwise;
163
164 for i in 0..NUM_ROUNDS {
165 apply_round(&mut state_stepwise, i);
166 }
167 apply_permutation(&mut state_permutation);
168
169 assert_eq!(state_stepwise, state_permutation, "mismatch with random state");
170 }
171
172 /// Verifies that intermediate steps are computed correctly by checking that two
173 /// half-permutations produce the same result as a full permutation.
174 #[test]
175 fn apply_round_intermediate_states() {
176 let init_state: [Felt; STATE_WIDTH] = core::array::from_fn(|i| Felt::new((i + 1) as u64));
177
178 // Apply first half of rounds
179 let mut state_half1 = init_state;
180 for i in 0..15 {
181 apply_round(&mut state_half1, i);
182 }
183
184 // Apply second half of rounds
185 let mut state_half2 = state_half1;
186 for i in 15..NUM_ROUNDS {
187 apply_round(&mut state_half2, i);
188 }
189
190 // Compare with full permutation
191 let mut state_full = init_state;
192 apply_permutation(&mut state_full);
193
194 assert_eq!(state_half2, state_full, "split application doesn't match full permutation");
195 }
196}