Skip to main content

sha256_rhdl/
lib.rs

1#![doc = "RHDL SHA-256 compression implementations and performance-lane building blocks."]
2#![forbid(unsafe_code)]
3
4//! This crate is the hash foundation for the separately typed
5//! `HASHSIGS_SHA256_GENERIC_V1` profile. It is deliberately incompatible with
6//! the legacy Keccak profile.
7//!
8//! The correctness path consists of:
9//!
10//! - allocation-free, compile-time-capable SHA-256 primitives and compression;
11//! - a byte-oriented padding/block-stream helper;
12//! - an RHDL round kernel whose token carries the eight working words and a
13//!   sixteen-word rolling message-schedule window;
14//! - an RHDL combinational compression kernel used as a lowering oracle; and
15//! - a resetless round-pipeline foundation with separately reset valid bits; and
16//! - [`lane::CompressionLane`], which aligns the original chaining value in a
17//!   circular synchronous memory and performs feed-forward; and
18//! - [`farm::ThreeLaneCompressionFarm`], a standalone generation and
19//!   equivalence target with independent inputs, outputs, tags, and valid
20//!   streams.
21//!
22//! The production WOTS signer does not embed that farm. It uses three
23//! autonomous four-context clusters, with one complete compression lane owned
24//! by each cluster.
25
26use rhdl::prelude::*;
27use rhdl_fpga::core::dff::DFF;
28use rhdl_primitives::NoResetDff;
29
30/// Parallel compression-lane farm for the performance profile.
31pub mod farm;
32
33/// Complete streaming compression-lane implementation.
34pub mod lane;
35
36/// Number of rounds in one SHA-256 compression operation.
37pub const ROUND_COUNT: usize = 64;
38
39/// SHA-256 compression-block size in bytes.
40pub const BLOCK_BYTES: usize = 64;
41
42/// SHA-256 digest size in bytes.
43pub const DIGEST_BYTES: usize = 32;
44
45/// Number of bits in one resetless round-pipeline token.
46pub const ROUND_TOKEN_BITS: usize = (8 + 16) * 32;
47
48/// Resetless datapath-register count in one complete 64-round lane.
49pub const LANE_DATAPATH_FFS: usize = ROUND_COUNT * ROUND_TOKEN_BITS;
50
51/// Provisional resetless datapath-register count in three complete lanes.
52pub const THREE_LANE_DATAPATH_FFS: usize = 3 * LANE_DATAPATH_FFS;
53
54/// Width of the opaque scheduler tag carried beside every pipeline token.
55pub const SCHEDULER_TAG_BITS: usize = 32;
56
57/// Resetless tag-register count in one complete lane.
58pub const LANE_TAG_FFS: usize = ROUND_COUNT * SCHEDULER_TAG_BITS;
59
60/// Resettable valid-register count in one complete lane.
61pub const LANE_VALID_FFS: usize = ROUND_COUNT;
62
63/// Total round-pipeline registers in one lane, excluding chaining-value memory.
64pub const LANE_PIPELINE_FFS: usize = LANE_DATAPATH_FFS + LANE_TAG_FFS + LANE_VALID_FFS;
65
66/// Total round-pipeline registers projected for three lanes.
67pub const THREE_LANE_PIPELINE_FFS: usize = 3 * LANE_PIPELINE_FFS;
68
69/// Padded block count for a 32-byte hash input.
70pub const BLOCKS_FOR_32_BYTES: usize = padded_block_count(32);
71
72/// Padded block count for a 35-byte `HashSigs` PRF input.
73pub const BLOCKS_FOR_35_BYTES: usize = padded_block_count(35);
74
75/// Padded block count for a 64-byte secret-segment input.
76pub const BLOCKS_FOR_64_BYTES: usize = padded_block_count(64);
77
78/// Padded block count for the 2,144-byte concatenated public-key segments.
79pub const BLOCKS_FOR_2144_BYTES: usize = padded_block_count(2_144);
80
81/// SHA-256 initial chaining value from FIPS 180-4 section 5.3.3.
82pub const INITIAL_STATE: [u32; 8] = [
83    0x6a09_e667,
84    0xbb67_ae85,
85    0x3c6e_f372,
86    0xa54f_f53a,
87    0x510e_527f,
88    0x9b05_688c,
89    0x1f83_d9ab,
90    0x5be0_cd19,
91];
92
93/// The 64 SHA-256 round constants from FIPS 180-4 section 4.2.2.
94pub const ROUND_CONSTANTS: [u32; ROUND_COUNT] = [
95    0x428a_2f98,
96    0x7137_4491,
97    0xb5c0_fbcf,
98    0xe9b5_dba5,
99    0x3956_c25b,
100    0x59f1_11f1,
101    0x923f_82a4,
102    0xab1c_5ed5,
103    0xd807_aa98,
104    0x1283_5b01,
105    0x2431_85be,
106    0x550c_7dc3,
107    0x72be_5d74,
108    0x80de_b1fe,
109    0x9bdc_06a7,
110    0xc19b_f174,
111    0xe49b_69c1,
112    0xefbe_4786,
113    0x0fc1_9dc6,
114    0x240c_a1cc,
115    0x2de9_2c6f,
116    0x4a74_84aa,
117    0x5cb0_a9dc,
118    0x76f9_88da,
119    0x983e_5152,
120    0xa831_c66d,
121    0xb003_27c8,
122    0xbf59_7fc7,
123    0xc6e0_0bf3,
124    0xd5a7_9147,
125    0x06ca_6351,
126    0x1429_2967,
127    0x27b7_0a85,
128    0x2e1b_2138,
129    0x4d2c_6dfc,
130    0x5338_0d13,
131    0x650a_7354,
132    0x766a_0abb,
133    0x81c2_c92e,
134    0x9272_2c85,
135    0xa2bf_e8a1,
136    0xa81a_664b,
137    0xc24b_8b70,
138    0xc76c_51a3,
139    0xd192_e819,
140    0xd699_0624,
141    0xf40e_3585,
142    0x106a_a070,
143    0x19a4_c116,
144    0x1e37_6c08,
145    0x2748_774c,
146    0x34b0_bcb5,
147    0x391c_0cb3,
148    0x4ed8_aa4a,
149    0x5b9c_ca4f,
150    0x682e_6ff3,
151    0x748f_82ee,
152    0x78a5_636f,
153    0x84c8_7814,
154    0x8cc7_0208,
155    0x90be_fffa,
156    0xa450_6ceb,
157    0xbef9_a3f7,
158    0xc671_78f2,
159];
160
161/// RHDL-width-preserving form of [`ROUND_CONSTANTS`].
162pub const ROUND_CONSTANTS_RHDL: [b32; ROUND_COUNT] = [
163    b32(0x428a_2f98),
164    b32(0x7137_4491),
165    b32(0xb5c0_fbcf),
166    b32(0xe9b5_dba5),
167    b32(0x3956_c25b),
168    b32(0x59f1_11f1),
169    b32(0x923f_82a4),
170    b32(0xab1c_5ed5),
171    b32(0xd807_aa98),
172    b32(0x1283_5b01),
173    b32(0x2431_85be),
174    b32(0x550c_7dc3),
175    b32(0x72be_5d74),
176    b32(0x80de_b1fe),
177    b32(0x9bdc_06a7),
178    b32(0xc19b_f174),
179    b32(0xe49b_69c1),
180    b32(0xefbe_4786),
181    b32(0x0fc1_9dc6),
182    b32(0x240c_a1cc),
183    b32(0x2de9_2c6f),
184    b32(0x4a74_84aa),
185    b32(0x5cb0_a9dc),
186    b32(0x76f9_88da),
187    b32(0x983e_5152),
188    b32(0xa831_c66d),
189    b32(0xb003_27c8),
190    b32(0xbf59_7fc7),
191    b32(0xc6e0_0bf3),
192    b32(0xd5a7_9147),
193    b32(0x06ca_6351),
194    b32(0x1429_2967),
195    b32(0x27b7_0a85),
196    b32(0x2e1b_2138),
197    b32(0x4d2c_6dfc),
198    b32(0x5338_0d13),
199    b32(0x650a_7354),
200    b32(0x766a_0abb),
201    b32(0x81c2_c92e),
202    b32(0x9272_2c85),
203    b32(0xa2bf_e8a1),
204    b32(0xa81a_664b),
205    b32(0xc24b_8b70),
206    b32(0xc76c_51a3),
207    b32(0xd192_e819),
208    b32(0xd699_0624),
209    b32(0xf40e_3585),
210    b32(0x106a_a070),
211    b32(0x19a4_c116),
212    b32(0x1e37_6c08),
213    b32(0x2748_774c),
214    b32(0x34b0_bcb5),
215    b32(0x391c_0cb3),
216    b32(0x4ed8_aa4a),
217    b32(0x5b9c_ca4f),
218    b32(0x682e_6ff3),
219    b32(0x748f_82ee),
220    b32(0x78a5_636f),
221    b32(0x84c8_7814),
222    b32(0x8cc7_0208),
223    b32(0x90be_fffa),
224    b32(0xa450_6ceb),
225    b32(0xbef9_a3f7),
226    b32(0xc671_78f2),
227];
228
229const _: () = {
230    assert!(BLOCKS_FOR_32_BYTES == 1);
231    assert!(BLOCKS_FOR_35_BYTES == 1);
232    assert!(BLOCKS_FOR_64_BYTES == 2);
233    assert!(BLOCKS_FOR_2144_BYTES == 34);
234    assert!(ROUND_TOKEN_BITS == 768);
235    assert!(LANE_DATAPATH_FFS == 49_152);
236    assert!(THREE_LANE_DATAPATH_FFS == 147_456);
237    assert!(LANE_TAG_FFS == 2_048);
238    assert!(LANE_PIPELINE_FFS == 51_264);
239    assert!(THREE_LANE_PIPELINE_FFS == 153_792);
240};
241
242/// Rotates a software-model word right by a constant or dynamic count.
243#[must_use]
244pub const fn rotate_right(word: u32, count: u32) -> u32 {
245    word.rotate_right(count)
246}
247
248/// SHA-256 choice function.
249#[must_use]
250pub const fn choose(x: u32, y: u32, z: u32) -> u32 {
251    (x & y) ^ (!x & z)
252}
253
254/// SHA-256 majority function.
255#[must_use]
256pub const fn majority(x: u32, y: u32, z: u32) -> u32 {
257    (x & y) ^ (x & z) ^ (y & z)
258}
259
260/// SHA-256 uppercase sigma zero.
261#[must_use]
262pub const fn big_sigma_zero(x: u32) -> u32 {
263    rotate_right(x, 2) ^ rotate_right(x, 13) ^ rotate_right(x, 22)
264}
265
266/// SHA-256 uppercase sigma one.
267#[must_use]
268pub const fn big_sigma_one(x: u32) -> u32 {
269    rotate_right(x, 6) ^ rotate_right(x, 11) ^ rotate_right(x, 25)
270}
271
272/// SHA-256 lowercase sigma zero.
273#[must_use]
274pub const fn small_sigma_zero(x: u32) -> u32 {
275    rotate_right(x, 7) ^ rotate_right(x, 18) ^ (x >> 3)
276}
277
278/// SHA-256 lowercase sigma one.
279#[must_use]
280pub const fn small_sigma_one(x: u32) -> u32 {
281    rotate_right(x, 17) ^ rotate_right(x, 19) ^ (x >> 10)
282}
283
284/// Expands sixteen input words into the complete 64-word SHA-256 schedule.
285#[must_use]
286pub const fn expand_schedule(block: [u32; 16]) -> [u32; ROUND_COUNT] {
287    let mut schedule = [0_u32; ROUND_COUNT];
288    let mut index = 0;
289    while index < 16 {
290        schedule[index] = block[index];
291        index += 1;
292    }
293    while index < ROUND_COUNT {
294        schedule[index] = small_sigma_one(schedule[index - 2])
295            .wrapping_add(schedule[index - 7])
296            .wrapping_add(small_sigma_zero(schedule[index - 15]))
297            .wrapping_add(schedule[index - 16]);
298        index += 1;
299    }
300    schedule
301}
302
303/// Compresses one block and performs the mandated chaining-value feed-forward.
304///
305/// Both arguments and the result use SHA-256's word order. Message words must
306/// already be decoded as big-endian values.
307#[allow(clippy::many_single_char_names)]
308#[must_use]
309pub const fn compress_block(chaining: [u32; 8], block: [u32; 16]) -> [u32; 8] {
310    let schedule = expand_schedule(block);
311    let mut a = chaining[0];
312    let mut b = chaining[1];
313    let mut c = chaining[2];
314    let mut d = chaining[3];
315    let mut e = chaining[4];
316    let mut f = chaining[5];
317    let mut g = chaining[6];
318    let mut h = chaining[7];
319    let mut round = 0;
320    while round < ROUND_COUNT {
321        let temporary_one = h
322            .wrapping_add(big_sigma_one(e))
323            .wrapping_add(choose(e, f, g))
324            .wrapping_add(ROUND_CONSTANTS[round])
325            .wrapping_add(schedule[round]);
326        let temporary_two = big_sigma_zero(a).wrapping_add(majority(a, b, c));
327        h = g;
328        g = f;
329        f = e;
330        e = d.wrapping_add(temporary_one);
331        d = c;
332        c = b;
333        b = a;
334        a = temporary_one.wrapping_add(temporary_two);
335        round += 1;
336    }
337    [
338        chaining[0].wrapping_add(a),
339        chaining[1].wrapping_add(b),
340        chaining[2].wrapping_add(c),
341        chaining[3].wrapping_add(d),
342        chaining[4].wrapping_add(e),
343        chaining[5].wrapping_add(f),
344        chaining[6].wrapping_add(g),
345        chaining[7].wrapping_add(h),
346    ]
347}
348
349/// Returns the number of 64-byte blocks after SHA-256 padding.
350///
351/// The formula avoids `message_len + 9`, so it cannot overflow `usize`.
352#[must_use]
353pub const fn padded_block_count(message_len: usize) -> usize {
354    let complete = message_len / BLOCK_BYTES;
355    let remainder = message_len % BLOCK_BYTES;
356    complete + if remainder <= 55 { 1 } else { 2 }
357}
358
359/// Produces one padded SHA-256 block from an immutable byte stream.
360///
361/// This is the canonical block-stream contract used by the fixed `HashSigs`
362/// sizes. It inserts `0x80`, zero fill, and the 64-bit big-endian message length.
363/// `None` is returned for an out-of-range block or a message whose bit length
364/// does not fit SHA-256's 64-bit length field.
365#[must_use]
366pub fn padded_block(message: &[u8], block_index: usize) -> Option<[u8; BLOCK_BYTES]> {
367    let bit_len = u64::try_from(message.len()).ok()?.checked_mul(8)?;
368    let block_count = padded_block_count(message.len());
369    if block_index >= block_count {
370        return None;
371    }
372    let final_len_start = block_count * BLOCK_BYTES - 8;
373    let start = block_index * BLOCK_BYTES;
374    let length_bytes = bit_len.to_be_bytes();
375    let mut block = [0_u8; BLOCK_BYTES];
376    let mut offset = 0;
377    while offset < BLOCK_BYTES {
378        let absolute = start + offset;
379        block[offset] = if absolute < message.len() {
380            message[absolute]
381        } else if absolute == message.len() {
382            0x80
383        } else if absolute >= final_len_start {
384            length_bytes[absolute - final_len_start]
385        } else {
386            0
387        };
388        offset += 1;
389    }
390    Some(block)
391}
392
393/// Decodes a 64-byte compression block into sixteen big-endian words.
394#[must_use]
395pub const fn decode_block(block: [u8; BLOCK_BYTES]) -> [u32; 16] {
396    let mut words = [0_u32; 16];
397    let mut index = 0;
398    while index < 16 {
399        let offset = index * 4;
400        words[index] = u32::from_be_bytes([
401            block[offset],
402            block[offset + 1],
403            block[offset + 2],
404            block[offset + 3],
405        ]);
406        index += 1;
407    }
408    words
409}
410
411/// Hashes one byte slice with the correctness-first software model.
412///
413/// # Panics
414///
415/// Panics only if an in-range block cannot be produced. That would require a
416/// byte slice whose bit length exceeds SHA-256's 64-bit length field, which
417/// cannot be backed by an addressable allocation on supported targets.
418#[must_use]
419pub fn sha256(message: &[u8]) -> [u8; DIGEST_BYTES] {
420    let mut chaining = INITIAL_STATE;
421    let blocks = padded_block_count(message.len());
422    let mut block_index = 0;
423    while block_index < blocks {
424        let block = padded_block(message, block_index)
425            .expect("padded SHA-256 block must exist for an in-range index");
426        chaining = compress_block(chaining, decode_block(block));
427        block_index += 1;
428    }
429    let mut digest = [0_u8; DIGEST_BYTES];
430    let mut word = 0;
431    while word < 8 {
432        let bytes = chaining[word].to_be_bytes();
433        digest[word * 4] = bytes[0];
434        digest[word * 4 + 1] = bytes[1];
435        digest[word * 4 + 2] = bytes[2];
436        digest[word * 4 + 3] = bytes[3];
437        word += 1;
438    }
439    digest
440}
441
442/// Eight SHA-256 working variables carried by every round token.
443#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
444pub struct WorkingState {
445    /// Working variable `a`.
446    pub a: b32,
447    /// Working variable `b`.
448    pub b: b32,
449    /// Working variable `c`.
450    pub c: b32,
451    /// Working variable `d`.
452    pub d: b32,
453    /// Working variable `e`.
454    pub e: b32,
455    /// Working variable `f`.
456    pub f: b32,
457    /// Working variable `g`.
458    pub g: b32,
459    /// Working variable `h`.
460    pub h: b32,
461}
462
463impl WorkingState {
464    /// Converts software-model words into an RHDL working state.
465    #[must_use]
466    pub fn from_words(words: [u32; 8]) -> Self {
467        Self {
468            a: b32(u128::from(words[0])),
469            b: b32(u128::from(words[1])),
470            c: b32(u128::from(words[2])),
471            d: b32(u128::from(words[3])),
472            e: b32(u128::from(words[4])),
473            f: b32(u128::from(words[5])),
474            g: b32(u128::from(words[6])),
475            h: b32(u128::from(words[7])),
476        }
477    }
478
479    /// Converts an RHDL working state into software-model words.
480    #[must_use]
481    pub fn to_words(self) -> [u32; 8] {
482        [
483            bits32_to_u32(self.a),
484            bits32_to_u32(self.b),
485            bits32_to_u32(self.c),
486            bits32_to_u32(self.d),
487            bits32_to_u32(self.e),
488            bits32_to_u32(self.f),
489            bits32_to_u32(self.g),
490            bits32_to_u32(self.h),
491        ]
492    }
493}
494
495fn bits32_to_u32(word: b32) -> u32 {
496    let bytes = word.raw().to_le_bytes();
497    u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
498}
499
500/// Complete 768-bit datapath token for one SHA-256 round.
501///
502/// At the start of round `t`, `schedule[0]` is `W[t]` and the complete array is
503/// `W[t]..W[t+15]`. The round consumes element zero, shifts the window, and
504/// appends `W[t+16]`. Consequently every stage has constant schedule indices.
505#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
506pub struct RoundState {
507    /// Eight SHA-256 working variables.
508    pub working: WorkingState,
509    /// Sixteen-word rolling schedule window.
510    pub schedule: [b32; 16],
511}
512
513impl RoundState {
514    /// Builds a round-zero token from a chaining value and decoded message block.
515    #[must_use]
516    pub fn new(chaining: [u32; 8], block: [u32; 16]) -> Self {
517        Self {
518            working: WorkingState::from_words(chaining),
519            schedule: block.map(|word| b32(u128::from(word))),
520        }
521    }
522}
523
524/// Pipeline payload. `state` is meaningful only while `valid` is true.
525#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
526pub struct PipelineWord {
527    /// Resetless SHA-256 datapath token.
528    pub state: RoundState,
529    /// Opaque scheduler-owned metadata associated with this compression block.
530    pub tag: b32,
531    /// Separately reset token-valid marker.
532    pub valid: bool,
533}
534
535/// RHDL uppercase sigma zero with constant rotates.
536#[kernel]
537pub fn big_sigma_zero_kernel(x: b32) -> b32 {
538    ((x >> 2) | (x << 30)) ^ ((x >> 13) | (x << 19)) ^ ((x >> 22) | (x << 10))
539}
540
541/// RHDL uppercase sigma one with constant rotates.
542#[kernel]
543pub fn big_sigma_one_kernel(x: b32) -> b32 {
544    ((x >> 6) | (x << 26)) ^ ((x >> 11) | (x << 21)) ^ ((x >> 25) | (x << 7))
545}
546
547/// RHDL lowercase sigma zero with constant shifts and rotates.
548#[kernel]
549pub fn small_sigma_zero_kernel(x: b32) -> b32 {
550    ((x >> 7) | (x << 25)) ^ ((x >> 18) | (x << 14)) ^ (x >> 3)
551}
552
553/// RHDL lowercase sigma one with constant shifts and rotates.
554#[kernel]
555pub fn small_sigma_one_kernel(x: b32) -> b32 {
556    ((x >> 17) | (x << 15)) ^ ((x >> 19) | (x << 13)) ^ (x >> 10)
557}
558
559/// RHDL SHA-256 choice function.
560#[kernel]
561pub fn choose_kernel(x: b32, y: b32, z: b32) -> b32 {
562    (x & y) ^ (!x & z)
563}
564
565/// RHDL SHA-256 majority function.
566#[kernel]
567pub fn majority_kernel(x: b32, y: b32, z: b32) -> b32 {
568    (x & y) ^ (x & z) ^ (y & z)
569}
570
571/// Advances the sixteen-word rolling message-schedule window.
572#[kernel]
573pub fn schedule_kernel(schedule: [b32; 16]) -> [b32; 16] {
574    let appended = small_sigma_one_kernel(schedule[14])
575        + schedule[9]
576        + small_sigma_zero_kernel(schedule[1])
577        + schedule[0];
578    let mut next = schedule;
579    for index in 0..15 {
580        next[index] = schedule[index + 1];
581    }
582    next[15] = appended;
583    next
584}
585
586/// Executes exactly one SHA-256 round and advances the rolling schedule.
587///
588/// `round_constant` is a separate input so this kernel is an isolated Vivado
589/// resource/timing target. Pipeline stages connect it to a compile-time member
590/// of [`ROUND_CONSTANTS_RHDL`]. All additions are 32-bit modular additions.
591#[kernel]
592pub fn round_kernel(state: RoundState, round_constant: b32) -> RoundState {
593    let temporary_one = state.working.h
594        + big_sigma_one_kernel(state.working.e)
595        + choose_kernel(state.working.e, state.working.f, state.working.g)
596        + round_constant
597        + state.schedule[0];
598    let temporary_two = big_sigma_zero_kernel(state.working.a)
599        + majority_kernel(state.working.a, state.working.b, state.working.c);
600    RoundState {
601        working: WorkingState {
602            a: temporary_one + temporary_two,
603            b: state.working.a,
604            c: state.working.b,
605            d: state.working.c,
606            e: state.working.d + temporary_one,
607            f: state.working.e,
608            g: state.working.f,
609            h: state.working.g,
610        },
611        schedule: schedule_kernel(state.schedule),
612    }
613}
614
615/// Adds the final working variables to the input chaining value.
616#[kernel]
617pub fn feed_forward_kernel(chaining: [b32; 8], working: WorkingState) -> [b32; 8] {
618    [
619        chaining[0] + working.a,
620        chaining[1] + working.b,
621        chaining[2] + working.c,
622        chaining[3] + working.d,
623        chaining[4] + working.e,
624        chaining[5] + working.f,
625        chaining[6] + working.g,
626        chaining[7] + working.h,
627    ]
628}
629
630/// RHDL lowering oracle for one complete compression operation.
631///
632/// This is intentionally combinational. The performance implementation is
633/// [`RoundPipeline`], which places a resetless register after every round.
634#[allow(clippy::needless_range_loop)]
635#[kernel]
636pub fn compression_kernel(chaining: [b32; 8], block: [b32; 16]) -> [b32; 8] {
637    let mut state = RoundState {
638        working: WorkingState {
639            a: chaining[0],
640            b: chaining[1],
641            c: chaining[2],
642            d: chaining[3],
643            e: chaining[4],
644            f: chaining[5],
645            g: chaining[6],
646            h: chaining[7],
647        },
648        schedule: block,
649    };
650    for round in 0..64 {
651        state = round_kernel(state, ROUND_CONSTANTS_RHDL[round]);
652    }
653    feed_forward_kernel(chaining, state.working)
654}
655
656/// A resetless, fully registered prefix of the 64-round SHA-256 pipeline.
657///
658/// `ROUNDS = 64` is the complete round datapath. It accepts one prepared
659/// [`PipelineWord`] on every rising edge and produces the corresponding final
660/// working state after 64 registered round transfers. There is no backpressure;
661/// invalid bubbles are represented solely by the resettable `valid` pipeline.
662///
663/// The data registers use [`NoResetDff`]. Only the 64 valid bits use the stock
664/// resettable [`DFF`]. A resetless opaque 32-bit tag follows every token. The
665/// input chaining value is intentionally absent from this internal token; the
666/// complete [`lane::CompressionLane`] aligns it in synchronous memory.
667#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
668#[rhdl(dq_no_prefix)]
669pub struct RoundPipeline<const ROUNDS: usize>
670where
671    rhdl::bits::W<ROUNDS>: BitWidth,
672{
673    data: [NoResetDff<RoundState>; ROUNDS],
674    tag: [NoResetDff<b32>; ROUNDS],
675    valid: [DFF<bool>; ROUNDS],
676}
677
678impl<const ROUNDS: usize> Default for RoundPipeline<ROUNDS>
679where
680    rhdl::bits::W<ROUNDS>: BitWidth,
681{
682    fn default() -> Self {
683        assert!(
684            ROUNDS > 0,
685            "a SHA-256 round pipeline needs at least one stage"
686        );
687        assert!(
688            ROUNDS <= ROUND_COUNT,
689            "a SHA-256 round pipeline cannot exceed 64 stages"
690        );
691        Self {
692            data: core::array::from_fn(|_| NoResetDff::new()),
693            tag: core::array::from_fn(|_| NoResetDff::new()),
694            valid: core::array::from_fn(|_| DFF::new(false)),
695        }
696    }
697}
698
699impl<const ROUNDS: usize> SynchronousIO for RoundPipeline<ROUNDS>
700where
701    rhdl::bits::W<ROUNDS>: BitWidth,
702{
703    type I = PipelineWord;
704    type O = PipelineWord;
705    type Kernel = round_pipeline_kernel<ROUNDS>;
706}
707
708/// Connectivity kernel for [`RoundPipeline`].
709#[allow(clippy::needless_range_loop)]
710#[kernel]
711pub fn round_pipeline_kernel<const ROUNDS: usize>(
712    clock_reset: ClockReset,
713    input: PipelineWord,
714    q: Q<ROUNDS>,
715) -> (PipelineWord, D<ROUNDS>)
716where
717    rhdl::bits::W<ROUNDS>: BitWidth,
718{
719    let _ = clock_reset;
720    // Avoid adding a single `D::dont_care()` literal wider than 98,000 bits at
721    // 64 stages. Every field is overwritten below, so seed it from `q` instead;
722    // RHDL's dead-code pass can remove these otherwise redundant copies.
723    let mut d = D::<ROUNDS> {
724        data: q.data,
725        tag: q.tag,
726        valid: q.valid,
727    };
728    d.data[0] = round_kernel(input.state, ROUND_CONSTANTS_RHDL[0]);
729    d.tag[0] = input.tag;
730    d.valid[0] = input.valid;
731    for stage in 1..ROUNDS {
732        d.data[stage] = round_kernel(q.data[stage - 1], ROUND_CONSTANTS_RHDL[stage]);
733        d.tag[stage] = q.tag[stage - 1];
734        d.valid[stage] = q.valid[stage - 1];
735    }
736    (
737        PipelineWord {
738            state: q.data[ROUNDS - 1],
739            tag: q.tag[ROUNDS - 1],
740            valid: q.valid[ROUNDS - 1],
741        },
742        d,
743    )
744}
745
746/// Emits the isolated RHDL round kernel as Verilog for synthesis gates.
747///
748/// # Errors
749///
750/// Returns an error if RHDL cannot compile, lower, or render the kernel.
751pub fn round_verilog() -> Result<String, RHDLError> {
752    let rtl = compile_design::<round_kernel>(CompilationMode::Synchronous)?;
753    Ok(rtl.as_vlog()?.pretty())
754}
755
756/// Emits the combinational compression oracle as Verilog.
757///
758/// # Errors
759///
760/// Returns an error if RHDL cannot compile, lower, or render the kernel.
761pub fn compression_verilog() -> Result<String, RHDLError> {
762    let rtl = compile_design::<compression_kernel>(CompilationMode::Synchronous)?;
763    Ok(rtl.as_vlog()?.pretty())
764}
765
766/// Emits a registered round-pipeline prefix as Verilog.
767///
768/// # Errors
769///
770/// Returns an error if the specialization is invalid or RHDL cannot compile,
771/// lower, or render the synchronous circuit.
772pub fn round_pipeline_verilog<const ROUNDS: usize>() -> Result<String, RHDLError>
773where
774    rhdl::bits::W<ROUNDS>: BitWidth,
775{
776    let pipeline = RoundPipeline::<ROUNDS>::default();
777    Ok(pipeline
778        .descriptor("sha256_round_pipeline".into())?
779        .hdl()?
780        .modules
781        .pretty())
782}
783
784/// Emits a registered round-pipeline prefix without RHDL's Icarus syntax check.
785///
786/// This entry point exists because Icarus Verilog 13 cannot lex the monolithic
787/// function emitted for the complete 64-stage specialization: its scanner
788/// reports an input-buffer overflow. Returning this string proves only that
789/// RHDL constructed and rendered the descriptor. It is **not** evidence that
790/// the output parsed, simulated, synthesized, or is equivalent to the RHDL
791/// model. Use [`round_pipeline_verilog`] whenever the specialization is small
792/// enough for the built-in checker, and gate a complete lane with Verilator and
793/// Vivado before treating it as synthesis-ready.
794///
795/// # Errors
796///
797/// Returns an error if the specialization is invalid or RHDL cannot compile,
798/// lower, or render the synchronous circuit.
799pub fn round_pipeline_unchecked_verilog<const ROUNDS: usize>() -> Result<String, RHDLError>
800where
801    rhdl::bits::W<ROUNDS>: BitWidth,
802{
803    let pipeline = RoundPipeline::<ROUNDS>::default();
804    let descriptor = pipeline.descriptor("sha256_round_pipeline".into())?;
805    let hdl = descriptor.hdl_unchecked()?;
806    Ok(hdl.modules.pretty())
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812    use rhdl::core::sim::ResetOrData;
813    use rhdl::core::sim::testbench::kernel::test_kernel_vm_and_verilog_synchronous;
814    use sha2::{Digest, Sha256};
815
816    const ABC_BLOCK: [u32; 16] = [
817        0x6162_6380,
818        0,
819        0,
820        0,
821        0,
822        0,
823        0,
824        0,
825        0,
826        0,
827        0,
828        0,
829        0,
830        0,
831        0,
832        0x0000_0018,
833    ];
834
835    fn words_to_bits<const N: usize>(words: [u32; N]) -> [b32; N] {
836        words.map(|word| b32(u128::from(word)))
837    }
838
839    fn bits_to_words<const N: usize>(words: [b32; N]) -> [u32; N] {
840        words.map(bits32_to_u32)
841    }
842
843    fn deterministic_bytes(length: usize) -> Vec<u8> {
844        let mut state = 0x6d2b_79f5_u32;
845        (0..length)
846            .map(|_| {
847                state ^= state << 13;
848                state ^= state >> 17;
849                state ^= state << 5;
850                state.to_le_bytes()[0]
851            })
852            .collect()
853    }
854
855    fn pipeline_token(marker: u32, tag: u128) -> PipelineWord {
856        let mut block = ABC_BLOCK;
857        block[0] ^= marker;
858        block[7] = marker.rotate_left(11);
859        PipelineWord {
860            state: RoundState::new(INITIAL_STATE, block),
861            tag: b32(tag),
862            valid: true,
863        }
864    }
865
866    fn after_two_rounds(token: &PipelineWord) -> PipelineWord {
867        PipelineWord {
868            state: round_kernel(
869                round_kernel(token.state, ROUND_CONSTANTS_RHDL[0]),
870                ROUND_CONSTANTS_RHDL[1],
871            ),
872            ..*token
873        }
874    }
875
876    fn two_round_pipeline_stream()
877    -> impl Iterator<Item = TimedSample<(ClockReset, PipelineWord)>> + Clone {
878        let bubble = PipelineWord::default();
879        [
880            ResetOrData::Reset,
881            ResetOrData::Data(pipeline_token(0x1111_1111, 1)),
882            ResetOrData::Data(bubble),
883            ResetOrData::Data(pipeline_token(0x2222_2222, 2)),
884            // Context two is resident in stage zero here. The synchronous
885            // reset must clear its valid bit before it can be observed.
886            ResetOrData::Reset,
887            ResetOrData::Data(pipeline_token(0x9999_9999, 9)),
888            ResetOrData::Data(bubble),
889            ResetOrData::Data(pipeline_token(0xffff_ffff, 15)),
890            ResetOrData::Data(bubble),
891            ResetOrData::Data(bubble),
892        ]
893        .into_iter()
894        .clock_pos_edge(10)
895    }
896
897    #[test]
898    fn constants_and_fixed_block_counts_are_exact() {
899        assert_eq!(ROUND_CONSTANTS.len(), 64);
900        assert_eq!(ROUND_CONSTANTS_RHDL.len(), 64);
901        assert_eq!(BLOCKS_FOR_32_BYTES, 1);
902        assert_eq!(BLOCKS_FOR_35_BYTES, 1);
903        assert_eq!(BLOCKS_FOR_64_BYTES, 2);
904        assert_eq!(BLOCKS_FOR_2144_BYTES, 34);
905        for (software, hardware) in ROUND_CONSTANTS.into_iter().zip(ROUND_CONSTANTS_RHDL) {
906            assert_eq!(software, bits32_to_u32(hardware));
907        }
908    }
909
910    #[test]
911    fn standard_known_answer_tests() {
912        assert_eq!(
913            hex::encode(sha256(b"")),
914            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
915        );
916        assert_eq!(
917            hex::encode(sha256(b"abc")),
918            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
919        );
920        assert_eq!(
921            compress_block(INITIAL_STATE, ABC_BLOCK),
922            [
923                0xba78_16bf,
924                0x8f01_cfea,
925                0x4141_40de,
926                0x5dae_2223,
927                0xb003_61a3,
928                0x9617_7a9c,
929                0xb410_ff61,
930                0xf200_15ad,
931            ]
932        );
933    }
934
935    #[test]
936    fn fixed_sizes_and_boundaries_match_sha2() {
937        for length in [0, 1, 3, 32, 35, 55, 56, 63, 64, 65, 127, 128, 2_144] {
938            let message = deterministic_bytes(length);
939            let expected: [u8; 32] = Sha256::digest(&message).into();
940            assert_eq!(sha256(&message), expected, "length {length}");
941        }
942    }
943
944    #[test]
945    fn padding_stream_places_delimiter_and_length_exactly() {
946        let message = [0xa5_u8; 64];
947        let first = padded_block(&message, 0).expect("first block");
948        let second = padded_block(&message, 1).expect("padding block");
949        assert_eq!(first, message);
950        assert_eq!(second[0], 0x80);
951        assert!(second[1..56].iter().all(|byte| *byte == 0));
952        assert_eq!(&second[56..], &512_u64.to_be_bytes());
953        assert!(padded_block(&message, 2).is_none());
954    }
955
956    #[test]
957    fn rolling_rounds_match_expanded_schedule_and_software_compression() {
958        let schedule = expand_schedule(ABC_BLOCK);
959        let mut token = RoundState::new(INITIAL_STATE, ABC_BLOCK);
960        let chaining = words_to_bits(INITIAL_STATE);
961        for (schedule_word, round_constant) in schedule.into_iter().zip(ROUND_CONSTANTS_RHDL) {
962            assert_eq!(bits32_to_u32(token.schedule[0]), schedule_word);
963            token = round_kernel(token, round_constant);
964        }
965        let digest = feed_forward_kernel(chaining, token.working);
966        assert_eq!(
967            bits_to_words(digest),
968            compress_block(INITIAL_STATE, ABC_BLOCK)
969        );
970    }
971
972    #[test]
973    fn round_kernel_passes_rhdl_vm_and_emitted_verilog() -> Result<(), RHDLError> {
974        let mut state = RoundState::new(INITIAL_STATE, ABC_BLOCK);
975        let mut cases = Vec::new();
976        for constant in ROUND_CONSTANTS_RHDL.into_iter().take(4) {
977            cases.push((state, constant));
978            state = round_kernel(state, constant);
979        }
980        test_kernel_vm_and_verilog_synchronous::<round_kernel, _, _, _>(
981            round_kernel,
982            cases.into_iter(),
983        )
984    }
985
986    #[test]
987    fn compression_kernel_passes_rhdl_vm_and_emitted_verilog() -> Result<(), RHDLError> {
988        let chaining = words_to_bits(INITIAL_STATE);
989        let block = words_to_bits(ABC_BLOCK);
990        let expected = words_to_bits(compress_block(INITIAL_STATE, ABC_BLOCK));
991        assert_eq!(compression_kernel(chaining, block), expected);
992        test_kernel_vm_and_verilog_synchronous::<compression_kernel, _, _, _>(
993            compression_kernel,
994            [(chaining, block)].into_iter(),
995        )
996    }
997
998    #[test]
999    fn round_pipeline_preserves_valid_state_and_tag_across_reset() -> Result<(), RHDLError> {
1000        let pipeline = RoundPipeline::<2>::default();
1001        let samples = pipeline
1002            .run(two_round_pipeline_stream())
1003            .synchronous_sample()
1004            .collect::<Vec<_>>();
1005
1006        let observed = samples
1007            .iter()
1008            .enumerate()
1009            .filter_map(|(cycle, sample)| sample.output.valid.then_some((cycle, sample.output)))
1010            .collect::<Vec<_>>();
1011        let expected = [
1012            (3, after_two_rounds(&pipeline_token(0x1111_1111, 1))),
1013            (7, after_two_rounds(&pipeline_token(0x9999_9999, 9))),
1014            (9, after_two_rounds(&pipeline_token(0xffff_ffff, 15))),
1015        ];
1016        assert_eq!(observed, expected);
1017        assert!(
1018            !observed.iter().any(|(_, output)| output.tag == b32(2)),
1019            "reset must suppress every valid token already in flight"
1020        );
1021
1022        // The first two rising edges initialize the resetless data stages. From
1023        // that point onward the complete pipeline output is deterministic, so
1024        // the same trace can directly check the emitted registered Verilog.
1025        let test_bench = pipeline
1026            .run(two_round_pipeline_stream())
1027            .collect::<SynchronousTestBench<_, _>>();
1028        test_bench
1029            .rtl(&pipeline, &TestBenchOptions::default().skip(6))?
1030            .run_iverilog()?;
1031        Ok(())
1032    }
1033
1034    #[test]
1035    fn complete_round_pipeline_has_exact_latency_and_opaque_tag() {
1036        let input = pipeline_token(0xa5a5_5a5a, 9);
1037        let mut expected = input;
1038        for round_constant in ROUND_CONSTANTS_RHDL {
1039            expected.state = round_kernel(expected.state, round_constant);
1040        }
1041
1042        let mut stimulus = vec![ResetOrData::Reset, ResetOrData::Data(input)];
1043        stimulus.extend(std::iter::repeat_n(
1044            ResetOrData::Data(PipelineWord::default()),
1045            ROUND_COUNT,
1046        ));
1047        let pipeline = RoundPipeline::<ROUND_COUNT>::default();
1048        let observed = pipeline
1049            .run(stimulus.into_iter().clock_pos_edge(10))
1050            .synchronous_sample()
1051            .enumerate()
1052            .filter_map(|(cycle, sample)| sample.output.valid.then_some((cycle, sample.output)))
1053            .collect::<Vec<_>>();
1054        assert_eq!(observed, [(ROUND_COUNT + 1, expected)]);
1055    }
1056
1057    #[test]
1058    #[ignore = "full 64-stage RHDL lowering takes about four minutes"]
1059    fn complete_round_pipeline_specialization_renders_unchecked() -> Result<(), RHDLError> {
1060        let hdl = round_pipeline_unchecked_verilog::<ROUND_COUNT>()?;
1061        assert!(hdl.contains("module sha256_round_pipeline"));
1062        assert_eq!(hdl.matches("if (reset)").count(), ROUND_COUNT);
1063        assert_eq!(
1064            hdl.matches("always @(posedge clock)").count(),
1065            3 * ROUND_COUNT
1066        );
1067        Ok(())
1068    }
1069
1070    #[test]
1071    fn generators_expose_round_compression_and_resetless_pipeline() -> Result<(), RHDLError> {
1072        let round = round_verilog()?;
1073        assert!(round.contains("function [767:0] kernel_round_kernel"));
1074        let compression = compression_verilog()?;
1075        assert!(compression.contains("function [255:0] kernel_compression_kernel"));
1076        let pipeline = round_pipeline_verilog::<2>()?;
1077        assert!(pipeline.contains("module sha256_round_pipeline"));
1078        assert!(pipeline.contains("always @(posedge clock)"));
1079        assert!(pipeline.contains("if (reset)"));
1080        assert_eq!(pipeline.matches("if (reset)").count(), 2);
1081        Ok(())
1082    }
1083}