Skip to main content

rhdl_dsig_scalable_core/
blake3_core.rs

1//! BLAKE3 compression and a fully pipelined Rust/RHDL implementation.
2//!
3//! # Why this module exists
4//!
5//! Haraka does the chain steps, but everything else a DSig key needs is
6//! BLAKE3, in every profile: deriving the one-time seed, expanding it into
7//! the 68 chain secrets and the nonces (BLAKE3's extendable output, "XOF"),
8//! committing to the 68 chain ends, building the Merkle tree, and hashing the
9//! message. That is about 47 BLAKE3 compressions per signature
10//! ([`sustained`](crate::sustained)).
11//!
12//! BLAKE3 hashes any input by cutting it into 64-byte blocks and running a
13//! *compression function* on each one. A tree-hashing controller decides the
14//! order, the counters and the flags; the compression function does the
15//! actual mixing. This module is only the compression function. The callers
16//! that drive it:
17//!
18//! | Caller | What it hashes |
19//! |---|---|
20//! | [`crate::slice_factory`] | seeds, XOF blocks, chain-end commitment, subtree, message digest, on a fixed 68-cycle timetable ([`crate::slice_schedule`]) |
21//! | [`crate::cert_unit`] | the Merkle levels shared by all slices |
22//! | [`crate::chain_lane`] | the chain step itself in the `DSIG_CHAIN=blake3` profile |
23//! | [`crate::slice_model`] | the same work in plain Rust, using [`compress`] |
24//!
25//! Because one BLAKE3 lane per slice serves all of that work, the lane must
26//! accept a new block **every clock**. So this file builds a fully pipelined
27//! compression: 28 register stages, one block in and one block out per clock.
28//!
29//! # What one compression does
30//!
31//! The state is 16 words of 32 bits, viewed as a 4 × 4 matrix. It starts as
32//! the 8-word chaining value, 4 words of [`IV`], the 64-bit counter, the block
33//! length and the flags. Seven rounds then mix in the 16 message words:
34//!
35//! ```text
36//!   state (16 × 32-bit words)          one round = 8 G functions
37//!   ┌─────┬─────┬─────┬─────┐
38//!   │ v0  │ v1  │ v2  │ v3  │          column step:   G(v0,v4,v8,v12)  G(v1,v5,v9,v13)
39//!   ├─────┼─────┼─────┼─────┤                         G(v2,v6,v10,v14) G(v3,v7,v11,v15)
40//!   │ v4  │ v5  │ v6  │ v7  │
41//!   ├─────┼─────┼─────┼─────┤          diagonal step: G(v0,v5,v10,v15) G(v1,v6,v11,v12)
42//!   │ v8  │ v9  │ v10 │ v11 │                         G(v2,v7,v8,v13)  G(v3,v4,v9,v14)
43//!   ├─────┼─────┼─────┼─────┤
44//!   │ v12 │ v13 │ v14 │ v15 │          then permute the 16 message words
45//!   └─────┴─────┴─────┴─────┘          (MESSAGE_PERMUTATION) for the next round
46//!
47//!   G(a, b, c, d) with message words x, y:
48//!     first half:   a += b + x;  d = (d ⊕ a) ⋙ 16;  c += d;  b = (b ⊕ c) ⋙ 12
49//!     second half:  a += b + y;  d = (d ⊕ a) ⋙ 8;   c += d;  b = (b ⊕ c) ⋙ 7
50//! ```
51//!
52//! (`⋙` is a rotate right; `+` wraps at 2^32.) After seven rounds a
53//! *feed-forward* XORs the two halves of the state together and the original
54//! chaining value into the upper half, giving 16 output words.
55//!
56//! # From the picture to a pipeline
57//!
58//! The four G functions of a step are independent, so they run side by side.
59//! Each G is cut in half, so one round is four register stages:
60//!
61//! ```text
62//!   CompressionInput ─▶ initialize ─▶ ┌ phase 0: column G, first half   ┐ ─reg─▶
63//!                                     │ phase 1: column G, second half  │ ─reg─▶  × 7 rounds
64//!                                     │ phase 2: diagonal G, first half │ ─reg─▶  = 28 stages
65//!                                     └ phase 3: diagonal G, second half┘ ─reg─▶
66//!                                       + message permutation
67//!                                                          ─▶ feed_forward ─▶ CompressionOutput
68//! ```
69//!
70//! Why half a G per stage: each half has two additions in series. A whole G
71//! per stage would put four in series between two registers and lower the
72//! clock the stage can reach. The out-of-context measurement of one BLAKE3
73//! lane (~325 MHz alone, see [`sustained`](crate::sustained)) is the evidence
74//! that the 205 MHz target has margin; nothing in this file measures it.
75//!
76//! # Reading order
77//!
78//! 1. Constants: [`ROUNDS`], [`PIPELINE_STAGES`], the flags, [`IV`].
79//! 2. The software model: [`compress`], [`salted_digest_16`].
80//! 3. Ports: [`CompressionInput`], [`CompressionOutput`].
81//! 4. Kernels: [`g_first_kernel`], [`g_second_kernel`],
82//!    [`quarter_stage_kernel`], [`feed_forward_kernel`].
83//! 5. The pipeline: [`RoundPipeline`], [`round_pipeline_kernel`],
84//!    [`CompressionPipeline`], [`InlineCompressionPipeline`].
85//!
86//! # Summary of the contract
87//!
88//! [`CompressionPipeline`](crate::blake3_core::CompressionPipeline) implements all seven BLAKE3 rounds, with four
89//! register transfers per round. Each stage executes one half of four parallel
90//! G functions. Inputs are accepted every cycle; a corresponding output is
91//! visible after exactly [`PIPELINE_STAGES`](crate::blake3_core::PIPELINE_STAGES) registered transfers. There is no
92//! backpressure. The caller must provision downstream capacity before sending
93//! a valid token.
94//!
95//! Message and chaining words use BLAKE3's **little-endian** encoding. This is a
96//! compression primitive, not a general-purpose tree-hashing controller. Its
97//! caller owns block padding, chunk counters, flag selection, and chaining.
98//! [`salted_digest_16`](crate::blake3_core::salted_digest_16) is the complete fixed-size online message operation:
99//! the first 64 XOF bytes of `BLAKE3(pk_hash || nonce || message)`, with a 32-byte
100//! public-key hash, 16-byte nonce, and exactly 16 message bytes.
101//!
102//! The round state, original chaining value, message schedule, and tag reside
103//! in resetless registers. Only the valid bits reset. Data is unspecified when
104//! `valid` is false, and reset discards every in-flight token. No resource,
105//! routed-frequency, or board-throughput claim follows from this architecture.
106
107use rhdl::{core::circuit::descriptor::SyncKind, prelude::*};
108use rhdl_fpga::core::dff::DFF;
109
110/// Number of BLAKE3 compression rounds.
111///
112/// Fixed by the BLAKE3 specification.
113pub const ROUNDS: usize = 7;
114/// Number of registered G-half stages in a complete compression.
115///
116/// 4 stages per round × 7 rounds = 28: the lane latency in clock cycles.
117/// The factory's timetable and the BLAKE3 chain lane read this value.
118pub const PIPELINE_STAGES: usize = 4 * ROUNDS;
119/// Number of input bytes in a BLAKE3 compression block.
120///
121/// 64 bytes = 16 little-endian 32-bit message words.
122pub const BLOCK_BYTES: usize = 64;
123/// Number of bytes returned by one BLAKE3 XOF output block.
124///
125/// All 16 output words; a plain 32-byte hash uses only the first half.
126pub const OUTPUT_BYTES: usize = 64;
127/// Marks the first block of a chunk.
128///
129/// The flags below are single bits ORed together into the 32-bit `flags`
130/// word. They tell the compression where this block sits in BLAKE3's tree,
131/// so that otherwise identical blocks in different roles hash differently.
132/// A *chunk* is up to 1 KiB of input (16 blocks).
133pub const CHUNK_START: u32 = 1;
134/// Marks the final block of a chunk.
135pub const CHUNK_END: u32 = 2;
136/// Marks a tree-parent block made from two child chaining values.
137pub const PARENT: u32 = 4;
138/// Selects root/XOF output rather than an intermediate chaining value.
139pub const ROOT: u32 = 8;
140/// Selects BLAKE3's keyed-hash mode.
141pub const KEYED_HASH: u32 = 16;
142/// Selects the context-hashing step of BLAKE3 key derivation.
143pub const DERIVE_KEY_CONTEXT: u32 = 32;
144/// Selects the material-hashing step of BLAKE3 key derivation.
145pub const DERIVE_KEY_MATERIAL: u32 = 64;
146/// Flags for one complete, unkeyed, single-block root hash.
147///
148/// `1 | 2 | 8 = 11`: the block is the first and last of the only chunk, and
149/// it is the root, so its output is the final hash.
150pub const SINGLE_BLOCK_ROOT: u32 = CHUNK_START | CHUNK_END | ROOT;
151
152/// BLAKE3 initialization vector, as specified by the BLAKE3 team.
153///
154/// Eight 32-bit words (the same as SHA-256's). They are the chaining value
155/// of an unkeyed hash's first block, and words 8..12 of every compression's
156/// starting state.
157pub const IV: [u32; 8] = [
158    0x6a09_e667,
159    0xbb67_ae85,
160    0x3c6e_f372,
161    0xa54f_f53a,
162    0x510e_527f,
163    0x9b05_688c,
164    0x1f83_d9ab,
165    0x5be0_cd19,
166];
167
168/// Word permutation applied after each complete round.
169///
170/// After round `r`, message word `i` of round `r + 1` is word
171/// `MESSAGE_PERMUTATION[i]` of round `r`. The hardware applies it as wiring
172/// in phase 3 of [`quarter_stage_kernel`].
173pub const MESSAGE_PERMUTATION: [usize; 16] = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
174
175/// Official seven-round message schedule, used by the independent Rust model.
176///
177/// Row `r` lists which original message word each G input uses in round
178/// `r`. It is [`MESSAGE_PERMUTATION`] applied `r` times, written out so the
179/// software model does not share the hardware's way of computing it.
180pub const MESSAGE_SCHEDULE: [[usize; 16]; ROUNDS] = [
181    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
182    [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8],
183    [3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1],
184    [10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6],
185    [12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4],
186    [9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7],
187    [11, 15, 5, 0, 1, 9, 8, 6, 14, 10, 2, 12, 3, 4, 7, 13],
188];
189
190/// The phase (0..4) of every pipeline stage: stage `s` has phase `s mod 4`.
191///
192/// Written out as a constant table because RHDL kernels avoid `%`; the
193/// pipeline kernel indexes it with the constant stage number, so each stage
194/// gets a fixed phase and only that phase's logic survives synthesis.
195/// 2-bit values.
196const STAGE_PHASES: [b2; PIPELINE_STAGES] = [
197    b2(0),
198    b2(1),
199    b2(2),
200    b2(3),
201    b2(0),
202    b2(1),
203    b2(2),
204    b2(3),
205    b2(0),
206    b2(1),
207    b2(2),
208    b2(3),
209    b2(0),
210    b2(1),
211    b2(2),
212    b2(3),
213    b2(0),
214    b2(1),
215    b2(2),
216    b2(3),
217    b2(0),
218    b2(1),
219    b2(2),
220    b2(3),
221    b2(0),
222    b2(1),
223    b2(2),
224    b2(3),
225];
226
227/// Performs one software-model G function using wrapping 32-bit arithmetic.
228///
229/// `indices` names the four state words `(a, b, c, d)` this G mixes; `x`
230/// and `y` are its two message words. Returns the whole updated state.
231#[allow(clippy::many_single_char_names)]
232const fn g(mut words: [u32; 16], indices: [usize; 4], x: u32, y: u32) -> [u32; 16] {
233    // Positions of the four words in the 16-word state.
234    let [a, b, c, d] = indices;
235    // First half (rotations 16 and 12), using message word x.
236    words[a] = words[a].wrapping_add(words[b]).wrapping_add(x);
237    words[d] = (words[d] ^ words[a]).rotate_right(16);
238    words[c] = words[c].wrapping_add(words[d]);
239    words[b] = (words[b] ^ words[c]).rotate_right(12);
240    // Second half (rotations 8 and 7), using message word y.
241    words[a] = words[a].wrapping_add(words[b]).wrapping_add(y);
242    words[d] = (words[d] ^ words[a]).rotate_right(8);
243    words[c] = words[c].wrapping_add(words[d]);
244    words[b] = (words[b] ^ words[c]).rotate_right(7);
245    words
246}
247
248/// Compresses a single BLAKE3 block, returning all sixteen feed-forward words.
249///
250/// The first eight words are the next chaining value when `ROOT` is absent;
251/// all sixteen words are one output block when `ROOT` is present. For root
252/// output, `counter` is the XOF output-block number, not the input chunk number.
253/// This low-level function does not validate caller-owned lengths or flags.
254///
255/// Inputs: `cv` (8 words: the chaining value, or [`IV`] for a first block),
256/// `block` (16 message words), `counter` (64-bit chunk or output-block
257/// number), `block_len` (bytes used, 0..=64) and `flags`. This is the
258/// reference the hardware pipeline is tested against.
259#[must_use]
260pub const fn compress(
261    cv: [u32; 8],
262    block: [u32; 16],
263    counter: u64,
264    block_len: u32,
265    flags: u32,
266) -> [u32; 16] {
267    // The counter split into bytes, to form its low and high 32-bit words.
268    let counter_bytes = counter.to_le_bytes();
269    // The starting state: cv, four IV words, counter low/high, length, flags.
270    let mut words = [
271        cv[0],
272        cv[1],
273        cv[2],
274        cv[3],
275        cv[4],
276        cv[5],
277        cv[6],
278        cv[7],
279        IV[0],
280        IV[1],
281        IV[2],
282        IV[3],
283        u32::from_le_bytes([
284            counter_bytes[0],
285            counter_bytes[1],
286            counter_bytes[2],
287            counter_bytes[3],
288        ]),
289        u32::from_le_bytes([
290            counter_bytes[4],
291            counter_bytes[5],
292            counter_bytes[6],
293            counter_bytes[7],
294        ]),
295        block_len,
296        flags,
297    ];
298    let mut round = 0;
299    while round < ROUNDS {
300        // This round's message word order.
301        let schedule = MESSAGE_SCHEDULE[round];
302        // Column step: four G functions down the four columns.
303        words = g(words, [0, 4, 8, 12], block[schedule[0]], block[schedule[1]]);
304        words = g(words, [1, 5, 9, 13], block[schedule[2]], block[schedule[3]]);
305        words = g(
306            words,
307            [2, 6, 10, 14],
308            block[schedule[4]],
309            block[schedule[5]],
310        );
311        words = g(
312            words,
313            [3, 7, 11, 15],
314            block[schedule[6]],
315            block[schedule[7]],
316        );
317        // Diagonal step: four G functions along the diagonals.
318        words = g(
319            words,
320            [0, 5, 10, 15],
321            block[schedule[8]],
322            block[schedule[9]],
323        );
324        words = g(
325            words,
326            [1, 6, 11, 12],
327            block[schedule[10]],
328            block[schedule[11]],
329        );
330        words = g(
331            words,
332            [2, 7, 8, 13],
333            block[schedule[12]],
334            block[schedule[13]],
335        );
336        words = g(
337            words,
338            [3, 4, 9, 14],
339            block[schedule[14]],
340            block[schedule[15]],
341        );
342        round += 1;
343    }
344    // Feed-forward: low half ^= high half; high half ^= original cv.
345    let mut index = 0;
346    while index < 8 {
347        words[index] ^= words[index + 8];
348        words[index + 8] ^= cv[index];
349        index += 1;
350    }
351    words
352}
353
354/// Decodes a complete 64-byte block as sixteen little-endian words.
355#[must_use]
356pub const fn decode_block(bytes: [u8; BLOCK_BYTES]) -> [u32; 16] {
357    // The 16 decoded words.
358    let mut words = [0; 16];
359    let mut index = 0;
360    while index < 16 {
361        // First byte of word `index`.
362        let offset = index * 4;
363        words[index] = u32::from_le_bytes([
364            bytes[offset],
365            bytes[offset + 1],
366            bytes[offset + 2],
367            bytes[offset + 3],
368        ]);
369        index += 1;
370    }
371    words
372}
373
374/// Encodes sixteen compression-output words as 64 little-endian XOF bytes.
375#[must_use]
376pub const fn encode_words(words: [u32; 16]) -> [u8; OUTPUT_BYTES] {
377    // The 64 output bytes.
378    let mut bytes = [0; OUTPUT_BYTES];
379    let mut index = 0;
380    while index < 16 {
381        // Word `index` as four little-endian bytes.
382        let encoded = words[index].to_le_bytes();
383        // Where those bytes go.
384        let offset = index * 4;
385        bytes[offset] = encoded[0];
386        bytes[offset + 1] = encoded[1];
387        bytes[offset + 2] = encoded[2];
388        bytes[offset + 3] = encoded[3];
389        index += 1;
390    }
391    bytes
392}
393
394/// Constructs the exact single block `pk_hash || nonce || message`.
395///
396/// ```text
397///   byte 0           32         48          64
398///        ├───────────┼──────────┼───────────┤
399///        │ pk_hash   │ nonce    │ message   │   exactly one full block,
400///        │ 32 bytes  │ 16 bytes │ 16 bytes  │   no padding needed
401///        └───────────┴──────────┴───────────┘
402/// ```
403#[must_use]
404pub const fn salted_block_16(
405    pk_hash: [u8; 32],
406    nonce: [u8; 16],
407    message: [u8; 16],
408) -> [u8; BLOCK_BYTES] {
409    let mut block = [0; BLOCK_BYTES];
410    // Bytes 0..32: the public-key hash.
411    let mut index = 0;
412    while index < 32 {
413        block[index] = pk_hash[index];
414        index += 1;
415    }
416    // Bytes 32..48: the nonce; bytes 48..64: the message.
417    index = 0;
418    while index < 16 {
419        block[index + 32] = nonce[index];
420        block[index + 48] = message[index];
421        index += 1;
422    }
423    block
424}
425
426/// Returns 64 BLAKE3 XOF bytes for the fixed 16-byte online-message profile.
427///
428/// The input is exactly one full block, so flags are `CHUNK_START | CHUNK_END |
429/// ROOT` (11), the block length is 64, and the output-block counter is zero.
430///
431/// The software reference for [`salted_input_kernel`] followed by the
432/// pipeline.
433#[must_use]
434pub const fn salted_digest_16(
435    pk_hash: [u8; 32],
436    nonce: [u8; 16],
437    message: [u8; 16],
438) -> [u8; OUTPUT_BYTES] {
439    encode_words(compress(
440        IV,
441        decode_block(salted_block_16(pk_hash, nonce, message)),
442        0,
443        64,
444        SINGLE_BLOCK_ROOT,
445    ))
446}
447
448/// One compression request; all fields except `valid` are ignored in bubbles.
449///
450/// Everything [`compress`] takes, as RHDL bit vectors, plus a tag and a
451/// valid bit. 8 + 16 + 5 words of 32 bits and one bit of valid.
452#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
453pub struct CompressionInput {
454    /// Original chaining value, or IV/key words for a first block.
455    pub cv: [b32; 8],
456    /// Zero-padded message block decoded as little-endian words.
457    pub block: [b32; 16],
458    /// Low 32 bits of the chunk counter, or root XOF output-block counter.
459    pub counter_low: b32,
460    /// High 32 bits of the chunk counter, or root XOF output-block counter.
461    pub counter_high: b32,
462    /// Number of meaningful bytes in the input block, in `0..=64`.
463    pub block_len: b32,
464    /// Caller-supplied BLAKE3 compression flags.
465    pub flags: b32,
466    /// Opaque caller tag, preserved without interpretation.
467    pub tag: b32,
468    /// Marks a real input token; false inserts a pipeline bubble.
469    pub valid: bool,
470}
471
472/// Host-side helpers for building requests in tests and models.
473impl CompressionInput {
474    /// Encodes a valid request from ordinary Rust integer words.
475    ///
476    /// The 64-bit `counter` is split into `counter_low` and `counter_high`.
477    #[must_use]
478    pub fn new(
479        cv: [u32; 8],
480        block: [u32; 16],
481        counter: u64,
482        block_len: u32,
483        flags: u32,
484        tag: u32,
485    ) -> Self {
486        Self {
487            cv: cv.map(|word| b32(u128::from(word))),
488            block: block.map(|word| b32(u128::from(word))),
489            counter_low: b32(u128::from(counter & 0xffff_ffff)),
490            counter_high: b32(u128::from(counter >> 32)),
491            block_len: b32(u128::from(block_len)),
492            flags: b32(u128::from(flags)),
493            tag: b32(u128::from(tag)),
494            valid: true,
495        }
496    }
497
498    /// Prepares a valid fixed-size salted-message request.
499    ///
500    /// The same request [`salted_input_kernel`] builds in hardware.
501    #[must_use]
502    pub fn salted_16(pk_hash: [u8; 32], nonce: [u8; 16], message: [u8; 16], tag: u32) -> Self {
503        Self::new(
504            IV,
505            decode_block(salted_block_16(pk_hash, nonce, message)),
506            0,
507            64,
508            SINGLE_BLOCK_ROOT,
509            tag,
510        )
511    }
512}
513
514/// Compression result; `words` and `tag` are meaningful only when valid.
515///
516/// Appears exactly [`PIPELINE_STAGES`] (28) clocks after its input.
517#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
518pub struct CompressionOutput {
519    /// Sixteen output words after both BLAKE3 XOR feed-forward operations.
520    pub words: [b32; 16],
521    /// Unmodified tag belonging to this result.
522    pub tag: b32,
523    /// True only for an input token that survived reset and all stages.
524    pub valid: bool,
525}
526
527/// Host-side helpers for reading results.
528impl CompressionOutput {
529    /// Converts the datapath words into 64 little-endian bytes.
530    ///
531    /// Callers must check `valid` before using the returned data.
532    #[must_use]
533    pub fn to_bytes(self) -> [u8; OUTPUT_BYTES] {
534        encode_words(self.words.map(bits_to_word))
535    }
536}
537
538/// Convert an RHDL 32-bit value to a Rust `u32` (host side only).
539fn bits_to_word(value: b32) -> u32 {
540    // `raw()` is a u128; keep its four low bytes.
541    let bytes = value.raw().to_le_bytes();
542    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
543}
544
545/// Resetless token carried between the quarter-round stages.
546///
547/// 40 words of 32 bits (1,280 bits) before synthesis optimizes it. Each of
548/// the 28 stages holds one of these.
549#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
550pub struct RoundState {
551    /// Sixteen working-state words.
552    pub words: [b32; 16],
553    /// Message schedule, permuted after each complete round.
554    pub message: [b32; 16],
555    /// Original chaining value needed for the second eight output words.
556    pub cv: [b32; 8],
557}
558
559/// Four words manipulated by one BLAKE3 G function.
560///
561/// One column (or diagonal) of the 4 × 4 state. Exists so the two G-half
562/// kernels can take and return the four words as one value.
563#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
564pub struct GState {
565    /// First word, receiving the message-word additions.
566    pub a: b32,
567    /// Second word, rotated by 12 or 7 bits.
568    pub b: b32,
569    /// Third word, receiving additions from the fourth word.
570    pub c: b32,
571    /// Fourth word, rotated by 16 or 8 bits.
572    pub d: b32,
573}
574
575/// Synthesizable first half of G: rotations by 16 and 12 bits.
576///
577/// Inputs: the four words and one 32-bit message word. Output: the four
578/// updated words. Combinational: a 3-input add, then an XOR, then an add,
579/// then an XOR. Rotations are pure wiring.
580#[kernel]
581pub fn g_first_kernel(input: GState, message: b32) -> GState {
582    // a = a + b + m (wraps at 2^32).
583    let a = input.a + input.b + message;
584    // d = (d ⊕ a) rotated right by 16.
585    let mixed_d = input.d ^ a;
586    let d = (mixed_d >> 16) | (mixed_d << 16);
587    // c = c + d.
588    let c = input.c + d;
589    // b = (b ⊕ c) rotated right by 12 (= right 12 OR left 20).
590    let mixed_b = input.b ^ c;
591    let b = (mixed_b >> 12) | (mixed_b << 20);
592    GState { a, b, c, d }
593}
594
595/// Synthesizable second half of G: rotations by 8 and 7 bits.
596///
597/// Same shape as [`g_first_kernel`] with the second message word and the
598/// other two rotation amounts.
599#[kernel]
600pub fn g_second_kernel(input: GState, message: b32) -> GState {
601    // a = a + b + m.
602    let a = input.a + input.b + message;
603    // d = (d ⊕ a) rotated right by 8.
604    let mixed_d = input.d ^ a;
605    let d = (mixed_d >> 8) | (mixed_d << 24);
606    // c = c + d.
607    let c = input.c + d;
608    // b = (b ⊕ c) rotated right by 7.
609    let mixed_b = input.b ^ c;
610    let b = (mixed_b >> 7) | (mixed_b << 25);
611    GState { a, b, c, d }
612}
613
614/// Constructs the round-zero working state without modifying input words.
615///
616/// Pure wiring plus four constant words ([`IV`] words 0..4, written out as
617/// literals). Combinational; feeds stage 0 directly.
618#[kernel]
619pub fn initialize_kernel(input: CompressionInput) -> RoundState {
620    // Words 0..8: cv; 8..12: IV; 12, 13: counter; 14: length; 15: flags.
621    RoundState {
622        words: [
623            input.cv[0],
624            input.cv[1],
625            input.cv[2],
626            input.cv[3],
627            input.cv[4],
628            input.cv[5],
629            input.cv[6],
630            input.cv[7],
631            b32(0x6a09_e667),
632            b32(0xbb67_ae85),
633            b32(0x3c6e_f372),
634            b32(0xa54f_f53a),
635            input.counter_low,
636            input.counter_high,
637            input.block_len,
638            input.flags,
639        ],
640        message: input.block,
641        cv: input.cv,
642    }
643}
644
645/// Executes one quarter-round with compile-time phase selection in the pipeline.
646///
647/// Phases 0/1 are first/second halves of the four column G functions. Phases
648/// 2/3 are first/second halves of the diagonal G functions. Phase 3 also
649/// rewires the message schedule for the next round. All state and schedule
650/// selections use constant indices; no data-dependent indexing is needed.
651///
652/// Inputs: the stage's incoming [`RoundState`] and its 2-bit `phase`.
653/// Output: the state after this half-step. Combinational; the pipeline
654/// registers the result. In the pipeline `phase` is always a constant, so
655/// three of the four branches are removed by synthesis and each stage holds
656/// only four G halves.
657///
658/// | Phase | G inputs `(a, b, c, d)` | Message words |
659/// |---|---|---|
660/// | 0 | columns, first half | 0, 2, 4, 6 |
661/// | 1 | columns, second half | 1, 3, 5, 7 |
662/// | 2 | diagonals, first half | 8, 10, 12, 14 |
663/// | 3 | diagonals, second half (+ permute message) | 9, 11, 13, 15 |
664#[allow(clippy::many_single_char_names)]
665#[kernel]
666pub fn quarter_stage_kernel(input: RoundState, phase: b2) -> RoundState {
667    // Short names: `w` the 16 state words, `m` the 16 message words.
668    let w = input.words;
669    let m = input.message;
670    // Copy everything (message, cv); the branches overwrite what changes.
671    let mut output = input;
672    // `p`, `q`, `r`, `s` below are the four G functions of this half-step.
673    if phase == b2(0) {
674        // Columns, first half.
675        let p = g_first_kernel(
676            GState {
677                a: w[0],
678                b: w[4],
679                c: w[8],
680                d: w[12],
681            },
682            m[0],
683        );
684        let q = g_first_kernel(
685            GState {
686                a: w[1],
687                b: w[5],
688                c: w[9],
689                d: w[13],
690            },
691            m[2],
692        );
693        let r = g_first_kernel(
694            GState {
695                a: w[2],
696                b: w[6],
697                c: w[10],
698                d: w[14],
699            },
700            m[4],
701        );
702        let s = g_first_kernel(
703            GState {
704                a: w[3],
705                b: w[7],
706                c: w[11],
707                d: w[15],
708            },
709            m[6],
710        );
711        // Put each column's words back in their column.
712        output.words = [
713            p.a, q.a, r.a, s.a, p.b, q.b, r.b, s.b, p.c, q.c, r.c, s.c, p.d, q.d, r.d, s.d,
714        ];
715    } else if phase == b2(1) {
716        // Columns, second half.
717        let p = g_second_kernel(
718            GState {
719                a: w[0],
720                b: w[4],
721                c: w[8],
722                d: w[12],
723            },
724            m[1],
725        );
726        let q = g_second_kernel(
727            GState {
728                a: w[1],
729                b: w[5],
730                c: w[9],
731                d: w[13],
732            },
733            m[3],
734        );
735        let r = g_second_kernel(
736            GState {
737                a: w[2],
738                b: w[6],
739                c: w[10],
740                d: w[14],
741            },
742            m[5],
743        );
744        let s = g_second_kernel(
745            GState {
746                a: w[3],
747                b: w[7],
748                c: w[11],
749                d: w[15],
750            },
751            m[7],
752        );
753        output.words = [
754            p.a, q.a, r.a, s.a, p.b, q.b, r.b, s.b, p.c, q.c, r.c, s.c, p.d, q.d, r.d, s.d,
755        ];
756    } else if phase == b2(2) {
757        // Diagonals, first half: p = (0,5,10,15), q = (1,6,11,12),
758        // r = (2,7,8,13), s = (3,4,9,14).
759        let p = g_first_kernel(
760            GState {
761                a: w[0],
762                b: w[5],
763                c: w[10],
764                d: w[15],
765            },
766            m[8],
767        );
768        let q = g_first_kernel(
769            GState {
770                a: w[1],
771                b: w[6],
772                c: w[11],
773                d: w[12],
774            },
775            m[10],
776        );
777        let r = g_first_kernel(
778            GState {
779                a: w[2],
780                b: w[7],
781                c: w[8],
782                d: w[13],
783            },
784            m[12],
785        );
786        let s = g_first_kernel(
787            GState {
788                a: w[3],
789                b: w[4],
790                c: w[9],
791                d: w[14],
792            },
793            m[14],
794        );
795        // Put each diagonal's words back where they came from (e.g. p.b
796        // goes to word 5, s.b to word 4).
797        output.words = [
798            p.a, q.a, r.a, s.a, s.b, p.b, q.b, r.b, r.c, s.c, p.c, q.c, q.d, r.d, s.d, p.d,
799        ];
800    } else {
801        // Phase 3: diagonals, second half.
802        let p = g_second_kernel(
803            GState {
804                a: w[0],
805                b: w[5],
806                c: w[10],
807                d: w[15],
808            },
809            m[9],
810        );
811        let q = g_second_kernel(
812            GState {
813                a: w[1],
814                b: w[6],
815                c: w[11],
816                d: w[12],
817            },
818            m[11],
819        );
820        let r = g_second_kernel(
821            GState {
822                a: w[2],
823                b: w[7],
824                c: w[8],
825                d: w[13],
826            },
827            m[13],
828        );
829        let s = g_second_kernel(
830            GState {
831                a: w[3],
832                b: w[4],
833                c: w[9],
834                d: w[14],
835            },
836            m[15],
837        );
838        // Diagonal words back to their positions, as in phase 2.
839        output.words = [
840            p.a, q.a, r.a, s.a, s.b, p.b, q.b, r.b, r.c, s.c, p.c, q.c, q.d, r.d, s.d, p.d,
841        ];
842        // End of the round: permute the message words (MESSAGE_PERMUTATION)
843        // so the next round sees them in its order. Wiring only.
844        output.message = [
845            m[2], m[6], m[3], m[10], m[7], m[0], m[4], m[13], m[1], m[11], m[12], m[5], m[9],
846            m[14], m[15], m[8],
847        ];
848    }
849    output
850}
851
852/// Applies BLAKE3's final XORs, without any additional registered transfer.
853///
854/// Input: the state leaving the last stage. Output: the 16 result words,
855/// `out[i] = v[i] ⊕ v[i+8]` and `out[i+8] = v[i+8] ⊕ cv[i]` for `i < 8`.
856/// Combinational, on the output side of the last register.
857#[allow(clippy::needless_range_loop)]
858#[kernel]
859pub fn feed_forward_kernel(state: RoundState) -> [b32; 16] {
860    // Placeholder value; every word is overwritten.
861    let mut words = state.words;
862    for index in 0..8 {
863        words[index] = state.words[index] ^ state.words[index + 8];
864        words[index + 8] = state.words[index + 8] ^ state.cv[index];
865    }
866    words
867}
868
869/// Builds the fixed 16-byte-message input directly from RHDL word arrays.
870///
871/// Every word is little-endian. This adapter is wiring and constants only;
872/// unlike a software prehash, it preserves the complete defined message.
873///
874/// Inputs: the 8-word public-key hash, 4-word nonce, 4-word message, a tag
875/// and a valid bit. Output: a [`CompressionInput`] equal to
876/// [`CompressionInput::salted_16`].
877#[kernel]
878pub fn salted_input_kernel(
879    pk_hash: [b32; 8],
880    nonce: [b32; 4],
881    message: [b32; 4],
882    tag: b32,
883    valid: bool,
884) -> CompressionInput {
885    CompressionInput {
886        // cv = IV (an unkeyed hash's first block), written as literals.
887        cv: [
888            b32(0x6a09_e667),
889            b32(0xbb67_ae85),
890            b32(0x3c6e_f372),
891            b32(0xa54f_f53a),
892            b32(0x510e_527f),
893            b32(0x9b05_688c),
894            b32(0x1f83_d9ab),
895            b32(0x5be0_cd19),
896        ],
897        // block = pk_hash || nonce || message (see salted_block_16).
898        block: [
899            pk_hash[0], pk_hash[1], pk_hash[2], pk_hash[3], pk_hash[4], pk_hash[5], pk_hash[6],
900            pk_hash[7], nonce[0], nonce[1], nonce[2], nonce[3], message[0], message[1], message[2],
901            message[3],
902        ],
903        // Counter 0, a full 64-byte block, flags SINGLE_BLOCK_ROOT (11).
904        counter_low: b32(0),
905        counter_high: b32(0),
906        block_len: b32(64),
907        flags: b32(11),
908        tag,
909        valid,
910    }
911}
912
913/// Registered prefix of the BLAKE3 compression pipeline.
914///
915/// Only `STAGES = 28` produces complete BLAKE3 output. Smaller specializations
916/// exist for bounded lowering and RTL-equivalence tests; they return XORs of
917/// incomplete working states and must **not** be used as a hash. The complete
918/// specialization is [`CompressionPipeline`].
919///
920/// The initiation interval is one cycle, with `STAGES` registered transfers
921/// of latency. Data and tags do not reset. Synchronous reset clears all valid
922/// bits and therefore flushes every in-flight token.
923///
924/// ```text
925///   stage 0        stage 1    stage 2    stage 3          stage 27
926///   init+phase 0 ─▶ phase 1 ─▶ phase 2 ─▶ phase 3 ─▶ ... ─▶ phase 3 ─▶ feed_forward ─▶ output
927///   (reg)           (reg)      (reg)      (reg)            (reg)      (no register)
928///
929///   tag, valid: copied one stage per clock beside the data
930/// ```
931///
932/// `#[rhdl(dq_no_prefix)]` names the generated input/output bundles plain
933/// `D` and `Q` (instead of `RoundPipelineD`/`RoundPipelineQ`); the kernel
934/// below uses those names.
935#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
936#[rhdl(dq_no_prefix)]
937pub struct RoundPipeline<const STAGES: usize>
938where
939    rhdl::bits::W<STAGES>: BitWidth,
940{
941    /// The working state, message schedule and cv after each stage.
942    data: [DFF<RoundState>; STAGES],
943    /// The caller's tag, delayed to match the data.
944    tag: [DFF<b32>; STAGES],
945    /// "This stage holds a real token." The only registers that reset.
946    valid: [DFF<bool>; STAGES],
947}
948
949/// Build a pipeline of `STAGES` stages, checking that `STAGES` is legal.
950impl<const STAGES: usize> Default for RoundPipeline<STAGES>
951where
952    rhdl::bits::W<STAGES>: BitWidth,
953{
954    /// Data registers start as "don't care"; every valid bit starts false.
955    ///
956    /// # Panics
957    /// Panics if `STAGES` is 0 or more than 28.
958    fn default() -> Self {
959        assert!(
960            STAGES > 0 && STAGES <= PIPELINE_STAGES,
961            "invalid BLAKE3 stage count"
962        );
963        Self {
964            data: core::array::from_fn(|_| DFF::resetless()),
965            tag: core::array::from_fn(|_| DFF::resetless()),
966            valid: core::array::from_fn(|_| DFF::new(false)),
967        }
968    }
969}
970
971/// Ports of [`RoundPipeline`]: one request in, one result out per clock.
972impl<const STAGES: usize> SynchronousIO for RoundPipeline<STAGES>
973where
974    rhdl::bits::W<STAGES>: BitWidth,
975{
976    /// A compression request.
977    type I = CompressionInput;
978    /// Its result, `STAGES` clocks later.
979    type O = CompressionOutput;
980    /// The stage wiring below.
981    type Kernel = round_pipeline_kernel<STAGES>;
982}
983
984/// Connects fixed-phase stages and aligns resettable validity with data/tag.
985///
986/// Inputs: `clock_reset`, the new request `input`, and `q` (every register's
987/// current value). Outputs: the result leaving the last stage and `d` (every
988/// register's next value). Timing: a request entering at clock `t` leaves at
989/// clock `t + STAGES`, and a new one may enter every clock.
990#[allow(clippy::needless_range_loop)]
991#[kernel]
992pub fn round_pipeline_kernel<const STAGES: usize>(
993    clock_reset: ClockReset,
994    input: CompressionInput,
995    q: Q<STAGES>,
996) -> (CompressionOutput, D<STAGES>)
997where
998    rhdl::bits::W<STAGES>: BitWidth,
999{
1000    // True while reset is asserted; hides the output during reset.
1001    let resetting = clock_reset.reset.any();
1002    // Start with "every register keeps its value"; each stage is set below.
1003    let mut d = D::<STAGES> {
1004        data: q.data,
1005        tag: q.tag,
1006        valid: q.valid,
1007    };
1008    // Stage 0: build the starting state and run phase 0 on it.
1009    d.data[0] = quarter_stage_kernel(initialize_kernel(input), b2(0));
1010    d.tag[0] = input.tag;
1011    d.valid[0] = input.valid;
1012    // Stages 1..STAGES: each runs its fixed phase on the previous stage.
1013    for stage in 1..STAGES {
1014        d.data[stage] = quarter_stage_kernel(q.data[stage - 1], STAGE_PHASES[stage]);
1015        d.tag[stage] = q.tag[stage - 1];
1016        d.valid[stage] = q.valid[stage - 1];
1017    }
1018    // Output: feed-forward on the last register (combinational), with
1019    // `valid` forced low during reset.
1020    (
1021        CompressionOutput {
1022            words: feed_forward_kernel(q.data[STAGES - 1]),
1023            tag: q.tag[STAGES - 1],
1024            valid: q.valid[STAGES - 1] && !resetting,
1025        },
1026        d,
1027    )
1028}
1029
1030/// All seven BLAKE3 rounds, 28-cycle latency, and one input per clock.
1031///
1032/// The one specialization of [`RoundPipeline`] that computes real BLAKE3.
1033pub type CompressionPipeline = RoundPipeline<PIPELINE_STAGES>;
1034
1035/// Copyable, zero-sized adapter for embedding the complete pipeline in a parent.
1036///
1037/// Pinned RHDL derives conservatively require injected child types to be
1038/// `Copy`. This adapter delegates state, native simulation, and elaboration to
1039/// the exact [`CompressionPipeline`]; it adds no datapath or authored HDL.
1040///
1041/// Why it is safe to build a fresh `CompressionPipeline` in every call:
1042/// every fresh one is identical (same stages, same reset values). The
1043/// register values themselves live in the simulation state `S`, which the
1044/// caller owns and passes in.
1045#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1046pub struct InlineCompressionPipeline;
1047
1048/// No RHDL-visible children; everything is delegated.
1049impl SynchronousDQ for InlineCompressionPipeline {
1050    /// Nothing to drive.
1051    type D = ();
1052    /// Nothing to read.
1053    type Q = ();
1054}
1055
1056/// Same ports as [`CompressionPipeline`].
1057impl SynchronousIO for InlineCompressionPipeline {
1058    /// A compression request.
1059    type I = CompressionInput;
1060    /// Its result, 28 clocks later.
1061    type O = CompressionOutput;
1062    /// No kernel of its own; see the hand-written [`Synchronous`] impl.
1063    type Kernel = NoSynchronousKernel<ClockReset, CompressionInput, (), (CompressionOutput, ())>;
1064}
1065
1066/// Every method forwards to a [`CompressionPipeline`].
1067impl Synchronous for InlineCompressionPipeline {
1068    /// The full pipeline's simulation state.
1069    type S = <CompressionPipeline as Synchronous>::S;
1070
1071    /// The full pipeline's power-up state.
1072    fn init(&self) -> Self::S {
1073        CompressionPipeline::default().init()
1074    }
1075
1076    /// One simulation step of the full pipeline.
1077    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
1078        CompressionPipeline::default().sim(clock_reset, input, state)
1079    }
1080
1081    /// The full pipeline's hardware description, so the Verilog is identical.
1082    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
1083        CompressionPipeline::default().descriptor(scoped_name)
1084    }
1085}
1086
1087/// Emits a checked BLAKE3 pipeline or small test prefix as Verilog.
1088///
1089/// # Errors
1090///
1091/// Returns RHDL lowering/rendering errors or an error from its external syntax
1092/// checker. A successful string is not a synthesis, timing, or hardware result.
1093pub fn round_pipeline_verilog<const STAGES: usize>() -> Result<String, RHDLError>
1094where
1095    rhdl::bits::W<STAGES>: BitWidth,
1096{
1097    // Build, describe under a fixed module name, lower to HDL, print.
1098    Ok(RoundPipeline::<STAGES>::default()
1099        .descriptor("blake3_compression_pipeline".into())?
1100        .hdl()?
1101        .modules
1102        .pretty())
1103}
1104
1105/// Emits the complete checked 28-stage BLAKE3 compression hierarchy.
1106///
1107/// # Errors
1108///
1109/// Propagates lowering, rendering, and external syntax-check errors.
1110pub fn compression_pipeline_verilog() -> Result<String, RHDLError> {
1111    round_pipeline_verilog::<PIPELINE_STAGES>()
1112}
1113
1114/// Renders the complete pipeline without invoking RHDL's external syntax check.
1115///
1116/// This is only generation evidence. Before FPGA use, an independent HDL
1117/// parser, equivalence simulation, and synthesis must accept the exact bytes.
1118///
1119/// # Errors
1120///
1121/// Propagates lowering and rendering errors.
1122pub fn compression_pipeline_unchecked_verilog() -> Result<String, RHDLError> {
1123    // As round_pipeline_verilog, but `hdl_unchecked` skips the syntax check.
1124    Ok(CompressionPipeline::default()
1125        .descriptor("blake3_compression_pipeline".into())?
1126        .hdl_unchecked()?
1127        .modules
1128        .pretty())
1129}