cyber_hemera/params.rs
1//! Hemera — Poseidon2 parameter set over the Goldilocks field.
2//!
3//! Single source of truth for every constant in the protocol.
4//! The WGSL shader (`gpu/poseidon2.wgsl`) duplicates a subset of
5//! these values because WGSL cannot import Rust; keep them in sync.
6//!
7//! ```text
8//! ┌──────────────────────────────────────────────────────────┐
9//! │ HEMERA — Complete Specification │
10//! │ │
11//! │ Field: p = 2⁶⁴ − 2³² + 1 (Goldilocks) │
12//! │ S-box: d = 7 (x → x⁷, minimum for field) │
13//! │ State width: t = 16 = 2⁴ │
14//! │ Full rounds: R_F = 8 (4 + 4) = 2³ │
15//! │ Partial rounds: R_P = 64 = 2⁶ │
16//! │ Rate: r = 8 elements (56 bytes) = 2³ │
17//! │ Capacity: c = 8 elements (64 bytes) = 2³ │
18//! │ Output: 8 elements (64 bytes) = 2³ │
19//! │ │
20//! │ Full round constants: 8 × 16 = 128 = 2⁷ │
21//! │ Partial round constants: 64 = 2⁶ │
22//! │ Total constants: 192 = 3 × 2⁶ │
23//! │ Total rounds: 72 = 9 × 2³ │
24//! │ │
25//! │ Classical collision resistance: 256 bits = 2⁸ │
26//! │ Quantum collision resistance: 170 bits │
27//! │ Algebraic degree: 2¹⁸⁰ │
28//! │ │
29//! │ Every parameter that appears in code is a power of 2. │
30//! └──────────────────────────────────────────────────────────┘
31//! ```
32
33use std::sync::LazyLock;
34
35use p3_field::PrimeField64;
36use p3_goldilocks::{Goldilocks, Poseidon2Goldilocks};
37use p3_symmetric::Permutation;
38use rand::RngCore;
39
40// ── Permutation parameters ──────────────────────────────────────────
41
42/// Width of the Poseidon2 state (number of Goldilocks field elements).
43pub const WIDTH: usize = 16;
44
45/// Number of full (external) rounds — 4 initial + 4 final.
46pub const ROUNDS_F: usize = 8;
47
48/// Number of partial (internal) rounds.
49pub const ROUNDS_P: usize = 64;
50
51/// S-box degree (x → x^d).
52pub const SBOX_DEGREE: usize = 7;
53
54// ── Sponge parameters ───────────────────────────────────────────────
55
56/// Number of rate elements in the sponge.
57pub const RATE: usize = 8;
58
59/// Number of capacity elements in the sponge.
60pub const CAPACITY: usize = WIDTH - RATE; // 8
61
62// ── Encoding parameters ─────────────────────────────────────────────
63
64/// Bytes per field element when encoding arbitrary input data.
65///
66/// We use 7 bytes per element because 2^56 − 1 < p (Goldilocks prime),
67/// so any 7-byte value fits without reduction.
68pub const INPUT_BYTES_PER_ELEMENT: usize = 7;
69
70/// Bytes per field element when encoding hash output.
71///
72/// For output we use the full canonical u64 representation (8 bytes),
73/// since output elements are already valid field elements.
74pub const OUTPUT_BYTES_PER_ELEMENT: usize = 8;
75
76// ── Derived constants ───────────────────────────────────────────────
77
78/// Number of input bytes that fill one rate block (8 elements × 7 bytes).
79pub const RATE_BYTES: usize = RATE * INPUT_BYTES_PER_ELEMENT; // 56
80
81/// Number of output elements extracted per squeeze (= rate).
82pub const OUTPUT_ELEMENTS: usize = RATE; // 8
83
84/// Number of output bytes per squeeze (8 elements × 8 bytes).
85pub const OUTPUT_BYTES: usize = OUTPUT_ELEMENTS * OUTPUT_BYTES_PER_ELEMENT; // 64
86
87// ── Security properties (informational) ─────────────────────────────
88
89/// Classical collision resistance in bits.
90pub const COLLISION_BITS: usize = 256;
91
92// ── Self-bootstrapping round constant generation ────────────────────
93
94/// Genesis seed: five bytes [0x63, 0x79, 0x62, 0x65, 0x72].
95///
96/// The cryptographic input is this byte sequence alone — no character set,
97/// no encoding convention. The fact that these bytes happen to spell "cyber"
98/// in ASCII is the human meaning; the specification is the hex literals.
99const GENESIS_SEED: &[u8] = &[0x63, 0x79, 0x62, 0x65, 0x72];
100
101/// Global singleton Poseidon2 permutation instance, self-bootstrapped.
102///
103/// Round constants are generated by Hemera₀ (the zero-constant permutation)
104/// operating as a sponge on the genesis seed. No external PRNG is used.
105static POSEIDON2: LazyLock<Poseidon2Goldilocks<WIDTH>> = LazyLock::new(bootstrap_hemera);
106
107/// Build the Hemera permutation via self-bootstrapping.
108///
109/// 1. Create Hemera₀ = Poseidon2 with all 192 round constants = 0
110/// 2. Run Hemera₀ as a sponge: absorb GENESIS_SEED, squeeze constants
111/// 3. Use those elements as round constants for the final Hemera
112fn bootstrap_hemera() -> Poseidon2Goldilocks<WIDTH> {
113 let (hemera0, state) = bootstrap_sponge_state();
114 let mut rng = SqueezeRng {
115 hemera0,
116 state,
117 buffer: [0u64; RATE],
118 pos: RATE, // empty — will squeeze on first read
119 };
120 Poseidon2Goldilocks::new_from_rng(ROUNDS_F, ROUNDS_P, &mut rng)
121}
122
123/// Create Hemera₀ and return the sponge state after absorbing the genesis seed.
124///
125/// This is the shared bootstrap logic used by both the CPU permutation
126/// and the GPU round constant export.
127fn bootstrap_sponge_state() -> (Poseidon2Goldilocks<WIDTH>, [Goldilocks; WIDTH]) {
128 // Hemera₀ — all-zero round constants.
129 let hemera0 = Poseidon2Goldilocks::new_from_rng(ROUNDS_F, ROUNDS_P, &mut ZeroRng);
130
131 // Absorb the genesis seed through Hemera₀ sponge.
132 let mut state = [Goldilocks::new(0); WIDTH];
133
134 // Pad: seed || 0x01 || 0x00* to RATE_BYTES (56 bytes).
135 let mut padded = [0u8; RATE_BYTES];
136 padded[..GENESIS_SEED.len()].copy_from_slice(GENESIS_SEED);
137 padded[GENESIS_SEED.len()] = 0x01;
138
139 // Encode padded bytes as rate elements (7 bytes per element).
140 let mut rate_block = [Goldilocks::new(0); RATE];
141 crate::encoding::bytes_to_rate_block(&padded, &mut rate_block);
142
143 // Absorb via Goldilocks field addition.
144 for i in 0..RATE {
145 state[i] = state[i] + rate_block[i];
146 }
147
148 // Store message length in capacity (state[10]), matching sponge convention.
149 state[RATE + 2] = Goldilocks::new(GENESIS_SEED.len() as u64);
150
151 // Permute with Hemera₀.
152 hemera0.permute_mut(&mut state);
153
154 (hemera0, state)
155}
156
157/// Squeeze the 192 round constants as raw u64 values from the bootstrap sponge.
158///
159/// Returns the canonical Goldilocks representations in the exact order consumed
160/// by `new_from_rng`: 128 external (8 rounds × 16 elements) then 64 internal.
161/// Used by the GPU module to upload constants to shaders.
162pub(crate) fn bootstrap_constants_u64() -> Vec<u64> {
163 let (hemera0, state) = bootstrap_sponge_state();
164 let mut rng = SqueezeRng {
165 hemera0,
166 state,
167 buffer: [0u64; RATE],
168 pos: RATE,
169 };
170 let total = ROUNDS_F * WIDTH + ROUNDS_P; // 192
171 (0..total).map(|_| rng.next_u64()).collect()
172}
173
174/// RNG that produces zeros (used to create Hemera₀).
175struct ZeroRng;
176
177impl RngCore for ZeroRng {
178 fn next_u32(&mut self) -> u32 {
179 0
180 }
181 fn next_u64(&mut self) -> u64 {
182 0
183 }
184 fn fill_bytes(&mut self, dest: &mut [u8]) {
185 dest.fill(0);
186 }
187}
188
189/// RNG that squeezes Goldilocks elements from a Hemera₀ sponge state.
190///
191/// Each squeeze extracts RATE (8) elements from the rate portion, then
192/// permutes the state for the next block. This produces an unlimited
193/// stream of pseudorandom field elements.
194struct SqueezeRng {
195 hemera0: Poseidon2Goldilocks<WIDTH>,
196 state: [Goldilocks; WIDTH],
197 buffer: [u64; RATE],
198 pos: usize,
199}
200
201impl SqueezeRng {
202 fn squeeze_block(&mut self) {
203 for i in 0..RATE {
204 self.buffer[i] = self.state[i].as_canonical_u64();
205 }
206 self.hemera0.permute_mut(&mut self.state);
207 self.pos = 0;
208 }
209}
210
211impl RngCore for SqueezeRng {
212 fn next_u32(&mut self) -> u32 {
213 self.next_u64() as u32
214 }
215
216 fn next_u64(&mut self) -> u64 {
217 if self.pos >= RATE {
218 self.squeeze_block();
219 }
220 let val = self.buffer[self.pos];
221 self.pos += 1;
222 val
223 }
224
225 fn fill_bytes(&mut self, dest: &mut [u8]) {
226 let mut written = 0;
227 while written < dest.len() {
228 let val = self.next_u64();
229 let bytes = val.to_le_bytes();
230 let remaining = dest.len() - written;
231 let n = remaining.min(8);
232 dest[written..written + n].copy_from_slice(&bytes[..n]);
233 written += n;
234 }
235 }
236}
237
238/// Apply the Poseidon2 permutation in-place.
239pub(crate) fn permute(state: &mut [Goldilocks; WIDTH]) {
240 POSEIDON2.permute_mut(state);
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use p3_goldilocks::Goldilocks;
247
248 #[test]
249 fn permutation_is_deterministic() {
250 let mut s1 = [Goldilocks::new(0); WIDTH];
251 let mut s2 = [Goldilocks::new(0); WIDTH];
252 permute(&mut s1);
253 permute(&mut s2);
254 assert_eq!(s1, s2);
255 }
256
257 #[test]
258 fn permutation_changes_state() {
259 let mut state = [Goldilocks::new(0); WIDTH];
260 let original = state;
261 permute(&mut state);
262 assert_ne!(state, original);
263 }
264
265 #[test]
266 fn different_inputs_different_outputs() {
267 let mut s1 = [Goldilocks::new(0); WIDTH];
268 let mut s2 = [Goldilocks::new(0); WIDTH];
269 s2[0] = Goldilocks::new(1);
270 permute(&mut s1);
271 permute(&mut s2);
272 assert_ne!(s1, s2);
273 }
274
275 #[test]
276 fn sponge_geometry() {
277 assert_eq!(WIDTH, RATE + CAPACITY);
278 assert_eq!(RATE_BYTES, 56);
279 assert_eq!(OUTPUT_BYTES, 64);
280 assert_eq!(OUTPUT_ELEMENTS, 8);
281 }
282}