sp1_lib/poseidon2.rs
1pub const WIDTH: usize = 16;
2pub const RATE: usize = 8;
3pub const BYTE_BLOCK_SIZE: usize = RATE * 3;
4
5use crate::syscall_poseidon2;
6
7#[repr(C)]
8#[repr(align(8))]
9pub struct Poseidon2State([u32; WIDTH]);
10
11impl Default for Poseidon2State {
12 fn default() -> Self {
13 Self([0; WIDTH])
14 }
15}
16
17impl Poseidon2State {
18 #[inline]
19 pub fn permute(&mut self) {
20 unsafe {
21 syscall_poseidon2(self);
22 }
23 }
24
25 #[inline]
26 pub fn as_mut_ptr(&mut self) -> *mut u32 {
27 self.0.as_mut_ptr()
28 }
29
30 /// Absorb a [`RATE`] size block of field elements
31 ///
32 /// # Safety
33 /// This function assumes that the elements are within the `SP1Field` range. Breaking this
34 /// constraint will lead to prover failure (the proof will not verify).
35 pub fn absorb_field_block_unchecked(&mut self, block: &[u32; RATE]) {
36 self.0[0..RATE].copy_from_slice(block);
37 self.permute();
38 }
39
40 /// Absorb a single byte block into the sponge state (raw, no padding).
41 ///
42 /// Encodes `BYTE_BLOCK_SIZE` (24) bytes into `RATE` (8) field elements using a little-endian
43 /// 3-bytes-per-element packing (max 24 bits per element), then overwrites the rate portion of
44 /// the state and permutes.
45 ///
46 /// **No padding is applied.** If you are hashing a variable-length message, use
47 /// [`Poseidon2ByteHash::hash`] which handles padding, or apply your own padding scheme
48 /// before calling this method.
49 pub fn absorb_byte_block(&mut self, block: &[u8; BYTE_BLOCK_SIZE]) {
50 let mut field_block = [0u32; RATE];
51 for (i, element) in field_block.iter_mut().enumerate() {
52 let start_idx = 3 * i;
53 *element += block[start_idx] as u32;
54 *element += (block[start_idx + 1] as u32) << 8;
55 *element += (block[start_idx + 2] as u32) << 16;
56 }
57 self.absorb_field_block_unchecked(&field_block);
58 }
59
60 /// Returns the rate portion of the current sponge state as the hash output.
61 ///
62 /// # Warning
63 /// This method does **not** apply any finalization padding. It returns the raw state after
64 /// the last permutation. If you are hashing variable-length input, you must ensure that a
65 /// padding-safe protocol was followed (e.g., by prefixing the message length). Consider using
66 /// [`Poseidon2ByteHash::hash`] which handles this automatically.
67 pub fn output(self) -> [u32; RATE] {
68 let mut output = [0; RATE];
69 output.copy_from_slice(&self.0[0..RATE]);
70 output
71 }
72}
73
74/// Poseidon2 byte hasher with safe padding (length-prefixed sponge).
75///
76/// Accepts an arbitrary-length byte slice and returns a `RATE`-sized field element digest.
77/// The input length is absorbed as the first message block before the data blocks,
78/// which prevents collisions between inputs that differ only in trailing zeros.
79pub struct Poseidon2ByteHash;
80
81impl Poseidon2ByteHash {
82 pub fn hash(input: &[u8]) -> [u32; RATE] {
83 let mut state = Poseidon2State::default();
84
85 // Absorb the input length (in bytes) as the first block for safe padding.
86 // This ensures inputs of different lengths that zero-pad identically still
87 // produce different hashes.
88 // The length is encoded as little-endian bytes and packed into field elements
89 // using the same 3-bytes-per-element scheme, supporting lengths up to 2^192.
90 let len_bytes = input.len().to_le_bytes();
91 let mut len_block = [0u8; BYTE_BLOCK_SIZE];
92 len_block[..len_bytes.len()].copy_from_slice(&len_bytes);
93 state.absorb_byte_block(&len_block);
94
95 // Absorb full blocks.
96 let chunks = input.chunks_exact(BYTE_BLOCK_SIZE);
97 let remainder = chunks.remainder();
98 for chunk in chunks {
99 state.absorb_byte_block(chunk.try_into().unwrap());
100 }
101
102 // Absorb the final partial block (zero-padded).
103 if !remainder.is_empty() {
104 let mut last_block = [0u8; BYTE_BLOCK_SIZE];
105 last_block[..remainder.len()].copy_from_slice(remainder);
106 state.absorb_byte_block(&last_block);
107 }
108
109 state.output()
110 }
111}