Skip to main content

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}