Skip to main content

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}