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}