1use rhdl::{core::circuit::descriptor::SyncKind, prelude::*};
20use rhdl_fpga::core::{
21 dff::DFF,
22 ram::synchronous::{In as SyncBramIn, SyncBRAM, Write as SyncBramWrite},
23};
24
25use crate::{
26 PipelineWord, ROUND_COUNT, RoundPipeline, RoundState, WorkingState, feed_forward_kernel,
27};
28
29pub const FULL_LANE_ADDRESS_BITS: usize = 6;
31
32pub const SHA256_COMPRESSION_LANE_MODULE: &str = "sha256_compression_lane";
39
40pub const FULL_LANE_DELAY_DEPTH: usize = 1 << FULL_LANE_ADDRESS_BITS;
42
43pub const FULL_LANE_CHAINING_STORAGE_BITS: usize = FULL_LANE_DELAY_DEPTH * 8 * 32;
45
46pub const THREE_LANE_CHAINING_STORAGE_BITS: usize = 3 * FULL_LANE_CHAINING_STORAGE_BITS;
48
49pub const AVOIDED_CHAINING_DELAY_FFS: usize = ROUND_COUNT * 8 * 32;
55
56pub const THREE_LANE_AVOIDED_CHAINING_DELAY_FFS: usize = 3 * AVOIDED_CHAINING_DELAY_FFS;
58
59const _: () = {
60 assert!(FULL_LANE_DELAY_DEPTH == ROUND_COUNT);
61 assert!(FULL_LANE_CHAINING_STORAGE_BITS == 16_384);
62 assert!(THREE_LANE_CHAINING_STORAGE_BITS == 49_152);
63 assert!(AVOIDED_CHAINING_DELAY_FFS == 16_384);
64 assert!(THREE_LANE_AVOIDED_CHAINING_DELAY_FFS == 49_152);
65};
66
67#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
74pub struct CompressionInput {
75 pub chaining: [b32; 8],
77 pub block: [b32; 16],
79 pub tag: b32,
81 pub valid: bool,
83}
84
85#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
89pub struct CompressionOutput {
90 pub digest: [b32; 8],
92 pub tag: b32,
94 pub valid: bool,
96}
97
98#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
117#[rhdl(dq_no_prefix)]
118pub struct CompressionLane<const ROUNDS: usize, const ADDRESS_BITS: usize>
119where
120 rhdl::bits::W<ROUNDS>: BitWidth,
121 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
122{
123 rounds: RoundPipeline<ROUNDS>,
124 chaining_delay: SyncBRAM<[b32; 8], ADDRESS_BITS>,
125 pointer: DFF<Bits<ADDRESS_BITS>>,
126}
127
128impl<const ROUNDS: usize, const ADDRESS_BITS: usize> Default
129 for CompressionLane<ROUNDS, ADDRESS_BITS>
130where
131 rhdl::bits::W<ROUNDS>: BitWidth,
132 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
133{
134 fn default() -> Self {
135 assert!(ROUNDS >= 2, "a BRAM-backed lane needs at least two stages");
136 assert!(
137 ADDRESS_BITS < usize::BITS as usize,
138 "the address-width shift must fit usize"
139 );
140 assert_eq!(
141 ROUNDS,
142 1_usize << ADDRESS_BITS,
143 "round count must equal the circular-memory depth"
144 );
145 assert!(
146 ROUNDS <= ROUND_COUNT,
147 "a SHA-256 compression lane cannot exceed 64 rounds"
148 );
149 Self {
150 rounds: RoundPipeline::default(),
151 chaining_delay: SyncBRAM::default(),
152 pointer: DFF::new(Bits::default()),
153 }
154 }
155}
156
157impl<const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousIO
158 for CompressionLane<ROUNDS, ADDRESS_BITS>
159where
160 rhdl::bits::W<ROUNDS>: BitWidth,
161 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
162{
163 type I = CompressionInput;
164 type O = CompressionOutput;
165 type Kernel = compression_lane_kernel<ROUNDS, ADDRESS_BITS>;
166}
167
168#[kernel]
170pub fn compression_lane_kernel<const ROUNDS: usize, const ADDRESS_BITS: usize>(
171 clock_reset: ClockReset,
172 input: CompressionInput,
173 q: Q<ROUNDS, ADDRESS_BITS>,
174) -> (CompressionOutput, D<ROUNDS, ADDRESS_BITS>)
175where
176 rhdl::bits::W<ROUNDS>: BitWidth,
177 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
178{
179 let _ = clock_reset;
180 let next_pointer = q.pointer + 1;
181 let d = D::<ROUNDS, ADDRESS_BITS> {
182 rounds: PipelineWord {
183 state: RoundState {
184 working: WorkingState {
185 a: input.chaining[0],
186 b: input.chaining[1],
187 c: input.chaining[2],
188 d: input.chaining[3],
189 e: input.chaining[4],
190 f: input.chaining[5],
191 g: input.chaining[6],
192 h: input.chaining[7],
193 },
194 schedule: input.block,
195 },
196 tag: input.tag,
197 valid: input.valid,
198 },
199 chaining_delay: SyncBramIn::<[b32; 8], ADDRESS_BITS> {
200 read_addr: next_pointer,
201 write: SyncBramWrite::<[b32; 8], ADDRESS_BITS> {
202 addr: q.pointer,
203 value: input.chaining,
204 enable: input.valid,
205 },
206 },
207 pointer: next_pointer,
208 };
209 let output = CompressionOutput {
210 digest: feed_forward_kernel(q.chaining_delay, q.rounds.state.working),
211 tag: q.rounds.tag,
212 valid: q.rounds.valid,
213 };
214 (output, d)
215}
216
217pub type FullCompressionLane = CompressionLane<ROUND_COUNT, FULL_LANE_ADDRESS_BITS>;
219
220#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
231pub struct InlineCompressionLane<const ROUNDS: usize, const ADDRESS_BITS: usize>;
232
233impl<const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousDQ
234 for InlineCompressionLane<ROUNDS, ADDRESS_BITS>
235where
236 rhdl::bits::W<ROUNDS>: BitWidth,
237 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
238{
239 type D = ();
240 type Q = ();
241}
242
243impl<const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousIO
244 for InlineCompressionLane<ROUNDS, ADDRESS_BITS>
245where
246 rhdl::bits::W<ROUNDS>: BitWidth,
247 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
248{
249 type I = CompressionInput;
250 type O = CompressionOutput;
251 type Kernel = NoSynchronousKernel<ClockReset, CompressionInput, (), (CompressionOutput, ())>;
252}
253
254impl<const ROUNDS: usize, const ADDRESS_BITS: usize> Synchronous
255 for InlineCompressionLane<ROUNDS, ADDRESS_BITS>
256where
257 rhdl::bits::W<ROUNDS>: BitWidth,
258 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
259{
260 type S = <CompressionLane<ROUNDS, ADDRESS_BITS> as Synchronous>::S;
261
262 fn init(&self) -> Self::S {
263 CompressionLane::<ROUNDS, ADDRESS_BITS>::default().init()
264 }
265
266 fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
267 CompressionLane::<ROUNDS, ADDRESS_BITS>::default().sim(clock_reset, input, state)
268 }
269
270 fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
271 CompressionLane::<ROUNDS, ADDRESS_BITS>::default().descriptor(scoped_name)
272 }
273}
274
275#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
285pub struct ExternalFullCompressionLane;
286
287impl SynchronousDQ for ExternalFullCompressionLane {
288 type D = ();
289 type Q = ();
290}
291
292impl SynchronousIO for ExternalFullCompressionLane {
293 type I = CompressionInput;
294 type O = CompressionOutput;
295 type Kernel = NoSynchronousKernel<ClockReset, CompressionInput, (), (CompressionOutput, ())>;
296}
297
298impl Synchronous for ExternalFullCompressionLane {
299 type S = <FullCompressionLane as Synchronous>::S;
300
301 fn init(&self) -> Self::S {
302 FullCompressionLane::default().init()
303 }
304
305 fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
306 FullCompressionLane::default().sim(clock_reset, input, state)
307 }
308
309 fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
310 Descriptor::<SyncKind> {
311 name: scoped_name,
312 input_kind: CompressionInput::static_kind(),
313 output_kind: CompressionOutput::static_kind(),
314 d_kind: Kind::Empty,
315 q_kind: Kind::Empty,
316 kernel: None,
317 netlist: None,
318 hdl: Some(HDLDescriptor {
319 name: SHA256_COMPRESSION_LANE_MODULE.into(),
320 modules: vlog::ModuleList { modules: vec![] },
321 }),
322 _phantom: core::marker::PhantomData,
323 }
324 .with_external_netlist_black_box()
325 }
326}
327
328pub fn compression_lane_checked_verilog<const ROUNDS: usize, const ADDRESS_BITS: usize>()
339-> Result<String, RHDLError>
340where
341 rhdl::bits::W<ROUNDS>: BitWidth,
342 rhdl::bits::W<ADDRESS_BITS>: BitWidth,
343{
344 let lane = CompressionLane::<ROUNDS, ADDRESS_BITS>::default();
345 Ok(lane
346 .descriptor(SHA256_COMPRESSION_LANE_MODULE.into())?
347 .hdl()?
348 .modules
349 .pretty())
350}
351
352pub fn try_full_compression_lane_unchecked_verilog() -> Result<String, RHDLError> {
368 let lane = FullCompressionLane::default();
369 let descriptor = lane.descriptor(SHA256_COMPRESSION_LANE_MODULE.into())?;
370 let hdl = descriptor.hdl_unchecked()?;
371 Ok(hdl.modules.pretty())
372}
373
374#[cfg(test)]
375mod tests {
376 use std::collections::BTreeMap;
377
378 use rhdl::core::sim::ResetOrData;
379
380 use super::*;
381 use crate::{ROUND_CONSTANTS_RHDL, compress_block, round_kernel};
382
383 #[derive(Clone, Debug, Default, Synchronous, SynchronousDQ)]
384 struct ExternalLaneParent {
385 lane: ExternalFullCompressionLane,
386 }
387
388 impl SynchronousIO for ExternalLaneParent {
389 type I = CompressionInput;
390 type O = CompressionOutput;
391 type Kernel = external_lane_parent_kernel;
392 }
393
394 #[kernel]
395 fn external_lane_parent_kernel(
396 clock_reset: ClockReset,
397 input: CompressionInput,
398 q: ExternalLaneParentQ,
399 ) -> (CompressionOutput, ExternalLaneParentD) {
400 let _ = clock_reset;
401 (q.lane, ExternalLaneParentD { lane: input })
402 }
403
404 fn word(value: u32) -> b32 {
405 b32(u128::from(value))
406 }
407
408 fn word_u32(value: b32) -> u32 {
409 let bytes = value.raw().to_le_bytes();
410 u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
411 }
412
413 fn prepared_input(marker: u32, tag: u32) -> CompressionInput {
414 let chaining = core::array::from_fn(|index| {
415 let index = u32::try_from(index).expect("eight chaining words fit u32");
416 word(
417 0x1020_3040_u32
418 .wrapping_add(index.wrapping_mul(0x1111_1111))
419 .rotate_left(index)
420 ^ marker.rotate_right(index),
421 )
422 });
423 let block = core::array::from_fn(|index| {
424 let index = u32::try_from(index).expect("sixteen block words fit u32");
425 word(
426 marker
427 .wrapping_mul(index.wrapping_add(1))
428 .rotate_left(index * 3)
429 ^ index.wrapping_mul(0x9e37_79b9),
430 )
431 });
432 CompressionInput {
433 chaining,
434 block,
435 tag: word(tag),
436 valid: true,
437 }
438 }
439
440 fn partial_output(input: &CompressionInput, rounds: usize) -> CompressionOutput {
441 let mut state = RoundState {
442 working: WorkingState {
443 a: input.chaining[0],
444 b: input.chaining[1],
445 c: input.chaining[2],
446 d: input.chaining[3],
447 e: input.chaining[4],
448 f: input.chaining[5],
449 g: input.chaining[6],
450 h: input.chaining[7],
451 },
452 schedule: input.block,
453 };
454 for round_constant in ROUND_CONSTANTS_RHDL.into_iter().take(rounds) {
455 state = round_kernel(state, round_constant);
456 }
457 CompressionOutput {
458 digest: feed_forward_kernel(input.chaining, state.working),
459 tag: input.tag,
460 valid: true,
461 }
462 }
463
464 fn expected_outputs(
465 events: &[ResetOrData<CompressionInput>],
466 rounds: usize,
467 ) -> Vec<(usize, CompressionOutput)> {
468 events
469 .iter()
470 .enumerate()
471 .filter_map(|(accepted_cycle, event)| {
472 let ResetOrData::Data(input) = event else {
473 return None;
474 };
475 if !input.valid {
476 return None;
477 }
478 let output_cycle = accepted_cycle + rounds;
479 if output_cycle >= events.len()
480 || events[accepted_cycle + 1..output_cycle]
481 .iter()
482 .any(|event| matches!(event, ResetOrData::Reset))
483 {
484 return None;
485 }
486 Some((output_cycle, partial_output(input, rounds)))
487 })
488 .collect()
489 }
490
491 fn observed_outputs<L>(
492 lane: &L,
493 events: &[ResetOrData<CompressionInput>],
494 ) -> Vec<(usize, CompressionOutput)>
495 where
496 L: Synchronous<I = CompressionInput, O = CompressionOutput>,
497 {
498 lane.run(events.iter().copied().clock_pos_edge(10))
499 .synchronous_sample()
500 .enumerate()
501 .filter_map(|(cycle, sample)| sample.output.valid.then_some((cycle, sample.output)))
502 .collect()
503 }
504
505 fn small_lane_events() -> Vec<ResetOrData<CompressionInput>> {
506 let mut events = vec![ResetOrData::Reset];
507 events.extend((0..4).map(|index| {
510 ResetOrData::Data(prepared_input(0x1000_0000 + index, 0xa000_0000 + index))
511 }));
512 events.extend([
513 ResetOrData::Data(prepared_input(0x2000_0001, 0xb000_0001)),
514 ResetOrData::Data(prepared_input(0x2000_0002, 0xb000_0002)),
515 ResetOrData::Data(CompressionInput::default()),
516 ResetOrData::Data(prepared_input(0x2000_0003, 0xb000_0003)),
517 ResetOrData::Reset,
519 ]);
520 for index in 0..13_u32 {
521 let input = if index % 5 == 3 {
522 CompressionInput::default()
523 } else {
524 prepared_input(0x3000_0000 + index, 0xc000_0000 + index)
525 };
526 events.push(ResetOrData::Data(input));
527 }
528 events.extend(std::iter::repeat_n(
529 ResetOrData::Data(CompressionInput::default()),
530 4,
531 ));
532 events
533 }
534
535 #[test]
536 fn small_lane_handles_consecutive_tokens_bubbles_reset_and_wraparound() -> Result<(), RHDLError>
537 {
538 let lane = CompressionLane::<4, 2>::default();
539 let events = small_lane_events();
540 let observed = observed_outputs(&lane, &events);
541 let expected = expected_outputs(&events, 4);
542 assert_eq!(observed, expected);
543 assert!(observed.windows(2).any(|pair| pair[1].0 == pair[0].0 + 1));
544 assert!(
545 !observed
546 .iter()
547 .any(|(_, output)| output.tag == word(0xb000_0002))
548 );
549 assert!(events.len() > 2 * 4);
550
551 let test_bench = lane
552 .run(events.into_iter().clock_pos_edge(10))
553 .collect::<SynchronousTestBench<_, _>>();
554 test_bench
555 .rtl(&lane, &TestBenchOptions::default().skip(18))?
556 .run_iverilog()?;
557 Ok(())
558 }
559
560 #[test]
561 fn full_lane_matches_compression_for_more_than_two_pointer_wraps() {
562 let mut events = vec![ResetOrData::Reset];
563 for cycle in 1..=20_u32 {
564 events.push(ResetOrData::Data(prepared_input(
565 0x4000_0000 + cycle,
566 0xd000_0000 + cycle,
567 )));
568 }
569 events.push(ResetOrData::Reset);
570 for cycle in 0..145_u32 {
571 let input = if cycle % 11 == 7 {
572 CompressionInput::default()
573 } else {
574 prepared_input(0x5000_0000 + cycle, 0xe000_0000 + cycle)
575 };
576 events.push(ResetOrData::Data(input));
577 }
578 events.extend(std::iter::repeat_n(
579 ResetOrData::Data(CompressionInput::default()),
580 ROUND_COUNT,
581 ));
582
583 let lane = FullCompressionLane::default();
584 let observed = observed_outputs(&lane, &events);
585 let expected = expected_outputs(&events, ROUND_COUNT);
586 assert_eq!(observed, expected);
587 assert!(observed.len() > FULL_LANE_DELAY_DEPTH);
588
589 let input_by_tag = events
590 .iter()
591 .filter_map(|event| match event {
592 ResetOrData::Data(input) if input.valid => Some((word_u32(input.tag), *input)),
593 _ => None,
594 })
595 .collect::<BTreeMap<_, _>>();
596 for (_, output) in observed {
597 let input = input_by_tag
598 .get(&word_u32(output.tag))
599 .expect("every output tag came from one prepared input");
600 let chaining = input.chaining.map(word_u32);
601 let block = input.block.map(word_u32);
602 assert_eq!(output.digest.map(word_u32), compress_block(chaining, block));
603 }
604 }
605
606 #[test]
607 fn external_full_lane_behavior_delegates_to_the_native_full_lane() {
608 let mut events = vec![ResetOrData::Reset];
609 for cycle in 0..70_u32 {
610 let input = if cycle.is_multiple_of(9) {
611 CompressionInput::default()
612 } else {
613 prepared_input(0x6000_0000 + cycle, 0xf000_0000 + cycle)
614 };
615 events.push(ResetOrData::Data(input));
616 }
617 events.push(ResetOrData::Reset);
618 events.push(ResetOrData::Data(prepared_input(0x7000_0001, 0x0bad_f00d)));
619 events.extend(std::iter::repeat_n(
620 ResetOrData::Data(CompressionInput::default()),
621 ROUND_COUNT,
622 ));
623
624 let native = observed_outputs(&FullCompressionLane::default(), &events);
625 let external = observed_outputs(&ExternalFullCompressionLane, &events);
626 assert_eq!(external, native);
627 assert_eq!(external.len(), 7);
628 assert_eq!(
629 external.last().expect("post-reset result").1.tag,
630 word(0x0bad_f00d)
631 );
632 }
633
634 #[test]
635 fn external_full_lane_descriptor_is_typed_and_unresolved() -> Result<(), RHDLError> {
636 let lane = ExternalFullCompressionLane;
637 let descriptor = lane.descriptor("ignored_parent_scope".into())?;
638 assert_eq!(descriptor.name.to_string(), "ignored_parent_scope");
639 assert_eq!(descriptor.input_kind, CompressionInput::static_kind());
640 assert_eq!(descriptor.output_kind, CompressionOutput::static_kind());
641 assert_eq!(descriptor.input_kind.bits(), 801);
642 assert_eq!(descriptor.output_kind.bits(), 289);
643 assert!(descriptor.hdl().is_err());
644 assert!(descriptor.hdl_unchecked()?.modules.modules.is_empty());
645
646 let wrapper = descriptor
647 .netlist()?
648 .as_vlog("sha256_external_lane_wrapper")?
649 .modules
650 .pretty();
651 assert!(wrapper.contains("sha256_compression_lane bb_0("));
652 assert!(!wrapper.contains("module sha256_compression_lane("));
653 Ok(())
654 }
655
656 #[test]
657 fn external_reference_resolves_against_separate_checked_lane_source() -> Result<(), RHDLError> {
658 let descriptor =
659 ExternalLaneParent::default().descriptor("sha256_external_lane_wrapper".into())?;
660 assert!(descriptor.hdl().is_err());
661 let wrapper = descriptor.hdl_unchecked()?.modules.pretty();
662 assert!(wrapper.contains("module sha256_external_lane_wrapper("));
663 assert!(
664 wrapper.contains("sha256_compression_lane c0("),
665 "unexpected modular wrapper:\n{wrapper}"
666 );
667 assert!(!wrapper.contains("module sha256_compression_lane("));
668 let lane = compression_lane_checked_verilog::<4, 2>()?;
669 assert_eq!(
670 wrapper.matches("module sha256_compression_lane(").count()
671 + lane.matches("module sha256_compression_lane(").count(),
672 1
673 );
674
675 let wrapper_path = std::env::temp_dir().join(format!(
676 "hashsigs_sha256_external_lane_wrapper_{}.v",
677 std::process::id()
678 ));
679 let lane_path = std::env::temp_dir().join(format!(
680 "hashsigs_sha256_external_lane_source_{}.v",
681 std::process::id()
682 ));
683 std::fs::write(&wrapper_path, wrapper).expect("write temporary modular wrapper");
684 std::fs::write(&lane_path, lane).expect("write temporary generated lane source");
685 let result = std::process::Command::new("iverilog")
686 .args(["-g2012", "-s", "sha256_external_lane_wrapper", "-t", "null"])
687 .arg(&wrapper_path)
688 .arg(&lane_path)
689 .output()
690 .expect("Icarus Verilog must be installed for RHDL RTL gates");
691 std::fs::remove_file(&wrapper_path).expect("remove temporary modular wrapper");
692 std::fs::remove_file(&lane_path).expect("remove temporary generated lane source");
693 assert!(
694 result.status.success(),
695 "separately generated lane did not resolve the external reference:\n{}\n{}",
696 String::from_utf8_lossy(&result.stdout),
697 String::from_utf8_lossy(&result.stderr)
698 );
699 Ok(())
700 }
701
702 #[test]
703 fn checked_small_lane_generator_exposes_resetless_memory() -> Result<(), RHDLError> {
704 let hdl = compression_lane_checked_verilog::<4, 2>()?;
705 assert!(hdl.contains("module sha256_compression_lane"));
706 assert!(hdl.contains("reg [255:0] mem[3:0]"));
707 assert_eq!(hdl.matches("if (reset)").count(), 5);
708 Ok(())
709 }
710
711 #[test]
712 #[ignore = "slow explicit external Verilator parser gate"]
713 fn full_lane_unchecked_render_passes_verilator_lint() -> Result<(), RHDLError> {
714 let hdl = try_full_compression_lane_unchecked_verilog()?;
715 assert!(hdl.contains("module sha256_compression_lane"));
716 assert!(hdl.contains("reg [255:0] mem[63:0]"));
717 let path = std::env::temp_dir().join(format!(
718 "hashsigs_sha256_full_lane_{}.v",
719 std::process::id()
720 ));
721 std::fs::write(&path, hdl).expect("write temporary full-lane Verilog");
722 let result = std::process::Command::new("verilator")
723 .args([
724 "--lint-only",
725 "--Wno-fatal",
726 "--top-module",
727 "sha256_compression_lane",
728 ])
729 .arg(&path)
730 .output()
731 .expect("Verilator must be installed for this explicitly ignored gate");
732 std::fs::remove_file(&path).expect("remove temporary full-lane Verilog");
733 assert!(
734 result.status.success(),
735 "Verilator lint failed:\n{}\n{}",
736 String::from_utf8_lossy(&result.stdout),
737 String::from_utf8_lossy(&result.stderr)
738 );
739 Ok(())
740 }
741}