Skip to main content

sha256_rhdl/
farm.rs

1//! Parallel SHA-256 compression lanes for the performance profile.
2//!
3//! [`crate::farm::CompressionFarm`] is the ordinary structural-array reference
4//! implementation.  [`crate::farm::ReplicatedCompressionFarm`] makes identical
5//! lane configuration explicit so RHDL lowers one child hierarchy and emits
6//! multiple hardware instances of it. Every lane receives its own chaining
7//! value, block, opaque scheduler tag, and valid bit; every lane returns its own
8//! digest, tag, and valid bit. There is no arbiter, runtime hash-profile mux, or
9//! input backpressure in either block.
10//!
11//! [`crate::farm::ThreeLaneCompressionFarm`] is a standalone generation and
12//! lane-equivalence target containing three complete 64-round lanes. It accepts
13//! up to three unrelated compression blocks per cycle and preserves each
14//! lane's exact 64-cycle latency. The production signer does not instantiate
15//! this farm: it instantiates one lane inside each of three autonomous
16//! four-context clusters. The figures below are architectural projections from
17//! the authored RHDL, not synthesis, placement, routing, or hardware evidence.
18
19use rhdl::core::ReplicatedSynchronous;
20use rhdl::prelude::*;
21
22use crate::{
23    LANE_PIPELINE_FFS, ROUND_COUNT,
24    lane::{
25        CompressionInput, CompressionLane, CompressionOutput, FULL_LANE_ADDRESS_BITS,
26        FULL_LANE_CHAINING_STORAGE_BITS,
27    },
28};
29
30/// Number of independent compression lanes in the performance farm.
31pub const PERFORMANCE_FARM_LANES: usize = 3;
32
33/// Projected round-data, tag, and valid register bits in the full farm.
34///
35/// This excludes the circular-memory pointers and the registered RAM outputs.
36/// It is a source-level count, not a mapped flip-flop count.
37pub const THREE_LANE_PROJECTED_PIPELINE_REGISTER_BITS: usize =
38    PERFORMANCE_FARM_LANES * LANE_PIPELINE_FFS;
39
40/// Projected circular-memory pointer bits in the full farm.
41pub const THREE_LANE_PROJECTED_POINTER_REGISTER_BITS: usize =
42    PERFORMANCE_FARM_LANES * FULL_LANE_ADDRESS_BITS;
43
44/// Projected registered synchronous-memory output bits in the full farm.
45///
46/// A physical implementation may absorb these bits into block-RAM output
47/// registers. Only synthesis can determine their mapped resource class.
48pub const THREE_LANE_PROJECTED_BRAM_OUTPUT_REGISTER_BITS: usize = PERFORMANCE_FARM_LANES * 8 * 32;
49
50/// Total projected architectural register bits in the full farm.
51///
52/// This is the arithmetic sum of the pipeline, pointer, and registered-memory
53/// output fields authored in RHDL. It must not be reported as FPGA utilization.
54pub const THREE_LANE_PROJECTED_ARCHITECTURAL_REGISTER_BITS: usize =
55    THREE_LANE_PROJECTED_PIPELINE_REGISTER_BITS
56        + THREE_LANE_PROJECTED_POINTER_REGISTER_BITS
57        + THREE_LANE_PROJECTED_BRAM_OUTPUT_REGISTER_BITS;
58
59/// Logical chaining-value memory bits in the full farm.
60///
61/// Whether these 49,152 logical bits infer block RAM, distributed RAM, or
62/// registers is a synthesis evidence gate.
63pub const THREE_LANE_PROJECTED_CHAINING_MEMORY_BITS: usize =
64    PERFORMANCE_FARM_LANES * FULL_LANE_CHAINING_STORAGE_BITS;
65
66const _: () = {
67    assert!(THREE_LANE_PROJECTED_PIPELINE_REGISTER_BITS == 153_792);
68    assert!(THREE_LANE_PROJECTED_POINTER_REGISTER_BITS == 18);
69    assert!(THREE_LANE_PROJECTED_BRAM_OUTPUT_REGISTER_BITS == 768);
70    assert!(THREE_LANE_PROJECTED_ARCHITECTURAL_REGISTER_BITS == 154_578);
71    assert!(THREE_LANE_PROJECTED_CHAINING_MEMORY_BITS == 49_152);
72};
73
74/// Input vector for a farm of `LANES` independent compression pipelines.
75pub type CompressionFarmInput<const LANES: usize> = [CompressionInput; LANES];
76
77/// Output vector for a farm of `LANES` independent compression pipelines.
78pub type CompressionFarmOutput<const LANES: usize> = [CompressionOutput; LANES];
79
80/// Structural farm of independent SHA-256 compression lanes.
81///
82/// The farm accepts one input vector on every rising edge. Each element is
83/// handled exclusively by the same-index lane and emerges at the same index
84/// exactly `ROUNDS` cycles later when not invalidated by reset. A bubble in one
85/// lane does not consume capacity in or alter the valid stream of another lane.
86/// There is no ready signal and no input backpressure.
87///
88/// `ROUNDS` and `ADDRESS_BITS` inherit [`CompressionLane`]'s constraints:
89/// `ROUNDS` must equal `2^ADDRESS_BITS`, be at least two, and not exceed 64.
90#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
91#[rhdl(dq_no_prefix)]
92pub struct CompressionFarm<const LANES: usize, const ROUNDS: usize, const ADDRESS_BITS: usize>
93where
94    rhdl::bits::W<ROUNDS>: BitWidth,
95    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
96{
97    lanes: [CompressionLane<ROUNDS, ADDRESS_BITS>; LANES],
98}
99
100impl<const LANES: usize, const ROUNDS: usize, const ADDRESS_BITS: usize> Default
101    for CompressionFarm<LANES, ROUNDS, ADDRESS_BITS>
102where
103    rhdl::bits::W<ROUNDS>: BitWidth,
104    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
105{
106    fn default() -> Self {
107        assert!(LANES > 0, "a compression farm needs at least one lane");
108        Self {
109            lanes: core::array::from_fn(|_| CompressionLane::default()),
110        }
111    }
112}
113
114impl<const LANES: usize, const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousIO
115    for CompressionFarm<LANES, ROUNDS, ADDRESS_BITS>
116where
117    rhdl::bits::W<ROUNDS>: BitWidth,
118    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
119{
120    type I = CompressionFarmInput<LANES>;
121    type O = CompressionFarmOutput<LANES>;
122    type Kernel = compression_farm_kernel<LANES, ROUNDS, ADDRESS_BITS>;
123}
124
125/// Pure structural connectivity for [`CompressionFarm`].
126#[kernel]
127pub fn compression_farm_kernel<const LANES: usize, const ROUNDS: usize, const ADDRESS_BITS: usize>(
128    clock_reset: ClockReset,
129    input: CompressionFarmInput<LANES>,
130    q: Q<LANES, ROUNDS, ADDRESS_BITS>,
131) -> (CompressionFarmOutput<LANES>, D<LANES, ROUNDS, ADDRESS_BITS>)
132where
133    rhdl::bits::W<ROUNDS>: BitWidth,
134    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
135{
136    let _ = clock_reset;
137    let d = D::<LANES, ROUNDS, ADDRESS_BITS> { lanes: input };
138    (q.lanes, d)
139}
140
141/// Structurally identical SHA-256 compression lanes lowered from one prototype.
142///
143/// Unlike [`CompressionFarm`], this type promises that every lane has the same
144/// configuration. RHDL can therefore lower one [`CompressionLane`] descriptor
145/// and instantiate that exact module `LANES` times. Behavioral simulation still
146/// allocates independent state per lane, so bubbles, tags, circular-memory
147/// pointers, and reset-visible valid bits cannot leak across lane boundaries.
148///
149/// `LANES` must be positive. `ROUNDS` and `ADDRESS_BITS` inherit
150/// [`CompressionLane`]'s constraints: `ROUNDS` must equal
151/// `2^ADDRESS_BITS`, be at least two, and not exceed 64.
152#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
153pub struct ReplicatedCompressionFarm<
154    const LANES: usize,
155    const ROUNDS: usize,
156    const ADDRESS_BITS: usize,
157> where
158    rhdl::bits::W<ROUNDS>: BitWidth,
159    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
160{
161    lanes: ReplicatedSynchronous<CompressionLane<ROUNDS, ADDRESS_BITS>, LANES>,
162}
163
164impl<const LANES: usize, const ROUNDS: usize, const ADDRESS_BITS: usize> Default
165    for ReplicatedCompressionFarm<LANES, ROUNDS, ADDRESS_BITS>
166where
167    rhdl::bits::W<ROUNDS>: BitWidth,
168    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
169{
170    fn default() -> Self {
171        Self {
172            lanes: ReplicatedSynchronous::new(CompressionLane::default()),
173        }
174    }
175}
176
177impl<const LANES: usize, const ROUNDS: usize, const ADDRESS_BITS: usize> SynchronousIO
178    for ReplicatedCompressionFarm<LANES, ROUNDS, ADDRESS_BITS>
179where
180    rhdl::bits::W<ROUNDS>: BitWidth,
181    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
182{
183    type I = CompressionFarmInput<LANES>;
184    type O = CompressionFarmOutput<LANES>;
185    type Kernel = replicated_compression_farm_kernel<LANES, ROUNDS, ADDRESS_BITS>;
186}
187
188/// Pure structural connectivity for [`ReplicatedCompressionFarm`].
189#[kernel]
190pub fn replicated_compression_farm_kernel<
191    const LANES: usize,
192    const ROUNDS: usize,
193    const ADDRESS_BITS: usize,
194>(
195    clock_reset: ClockReset,
196    input: CompressionFarmInput<LANES>,
197    q: ReplicatedCompressionFarmQ<LANES, ROUNDS, ADDRESS_BITS>,
198) -> (
199    CompressionFarmOutput<LANES>,
200    ReplicatedCompressionFarmD<LANES, ROUNDS, ADDRESS_BITS>,
201)
202where
203    rhdl::bits::W<ROUNDS>: BitWidth,
204    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
205{
206    let _ = clock_reset;
207    let d = ReplicatedCompressionFarmD::<LANES, ROUNDS, ADDRESS_BITS> { lanes: input };
208    (q.lanes, d)
209}
210
211/// Exact three-by-64-round standalone generation and equivalence target.
212///
213/// This alias demonstrates and validates three independent instances lowered
214/// from one repeated prototype. It is not a child of the production signer;
215/// that top owns three autonomous clusters, each with its own single shared
216/// compression lane.
217pub type ThreeLaneCompressionFarm =
218    ReplicatedCompressionFarm<PERFORMANCE_FARM_LANES, ROUND_COUNT, FULL_LANE_ADDRESS_BITS>;
219
220/// Emits and Icarus-checks a small farm specialization.
221///
222/// This function is intended for small lowering-equivalence gates. The pinned
223/// RHDL/Icarus path cannot lex the complete 3x64 hierarchy as one monolithic
224/// generated source file.
225///
226/// # Errors
227///
228/// Returns an error if the specialization is invalid, RHDL lowering fails, or
229/// Icarus rejects the generated module hierarchy.
230pub fn compression_farm_checked_verilog<
231    const LANES: usize,
232    const ROUNDS: usize,
233    const ADDRESS_BITS: usize,
234>() -> Result<String, RHDLError>
235where
236    rhdl::bits::W<ROUNDS>: BitWidth,
237    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
238{
239    let farm = CompressionFarm::<LANES, ROUNDS, ADDRESS_BITS>::default();
240    Ok(farm
241        .descriptor("sha256_compression_farm".into())?
242        .hdl()?
243        .modules
244        .pretty())
245}
246
247/// Renders the complete three-lane farm while bypassing its final Icarus check.
248///
249/// The word `unchecked` in this function's name is an evidence warning. The
250/// pinned RHDL core is patched locally so nested composition consumes raw child
251/// descriptors rather than re-running Icarus for every hierarchy level.
252/// Icarus Verilog 13 still overflows its scanner on the monolithic 64-stage
253/// functions, so callers must pass the result through a capable parser such as
254/// Verilator or Vivado. The returned text alone is only generated-Verilog
255/// evidence: it has not been parsed, simulated, proven equivalent, synthesized,
256/// placed, routed, timed, or executed on hardware.
257///
258/// # Errors
259///
260/// Returns an error if RHDL cannot construct or render the full descriptor.
261pub fn try_full_three_lane_farm_generation_only_unchecked_verilog() -> Result<String, RHDLError> {
262    let farm = ThreeLaneCompressionFarm::default();
263    let descriptor = farm.descriptor("sha256_compression_farm_3x64".into())?;
264    let hdl = descriptor.hdl_unchecked()?;
265    Ok(hdl.modules.pretty())
266}
267
268#[cfg(test)]
269mod tests {
270    use rhdl::core::sim::ResetOrData;
271
272    use super::*;
273    use crate::{
274        ROUND_CONSTANTS_RHDL, RoundState, WorkingState, compress_block, feed_forward_kernel,
275        round_kernel,
276    };
277
278    const SMALL_LANES: usize = PERFORMANCE_FARM_LANES;
279    const SMALL_ROUNDS: usize = 4;
280    const SMALL_ADDRESS_BITS: usize = 2;
281
282    fn word(value: u32) -> b32 {
283        b32(u128::from(value))
284    }
285
286    fn word_u32(value: b32) -> u32 {
287        let bytes = value.raw().to_le_bytes();
288        u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
289    }
290
291    fn prepared_input(lane: usize, marker: u32, sequence: u32) -> CompressionInput {
292        let lane = u32::try_from(lane).expect("the three lane indexes fit u32");
293        let chaining = core::array::from_fn(|index| {
294            let index = u32::try_from(index).expect("eight chaining words fit u32");
295            word(
296                0x1020_3040_u32
297                    .wrapping_add(index.wrapping_mul(0x1111_1111))
298                    .rotate_left(index)
299                    ^ marker.rotate_right(index)
300                    ^ lane.wrapping_mul(0x0101_0101),
301            )
302        });
303        let block = core::array::from_fn(|index| {
304            let index = u32::try_from(index).expect("sixteen block words fit u32");
305            word(
306                marker
307                    .wrapping_mul(index.wrapping_add(1))
308                    .rotate_left(index * 3)
309                    ^ index.wrapping_mul(0x9e37_79b9)
310                    ^ lane.rotate_left(index),
311            )
312        });
313        CompressionInput {
314            chaining,
315            block,
316            // The lane byte makes accidental cross-lane delivery obvious.
317            tag: word((lane << 24) | (sequence & 0x00ff_ffff)),
318            valid: true,
319        }
320    }
321
322    fn expected_output(input: &CompressionInput, rounds: usize) -> CompressionOutput {
323        let digest = if rounds == ROUND_COUNT {
324            let chaining = input.chaining.map(word_u32);
325            let block = input.block.map(word_u32);
326            compress_block(chaining, block).map(word)
327        } else {
328            let mut state = RoundState {
329                working: WorkingState {
330                    a: input.chaining[0],
331                    b: input.chaining[1],
332                    c: input.chaining[2],
333                    d: input.chaining[3],
334                    e: input.chaining[4],
335                    f: input.chaining[5],
336                    g: input.chaining[6],
337                    h: input.chaining[7],
338                },
339                schedule: input.block,
340            };
341            for round_constant in ROUND_CONSTANTS_RHDL.into_iter().take(rounds) {
342                state = round_kernel(state, round_constant);
343            }
344            feed_forward_kernel(input.chaining, state.working)
345        };
346        CompressionOutput {
347            digest,
348            tag: input.tag,
349            valid: true,
350        }
351    }
352
353    fn expected_lane_outputs<const LANES: usize>(
354        events: &[ResetOrData<CompressionFarmInput<LANES>>],
355        lane: usize,
356        rounds: usize,
357    ) -> Vec<(usize, CompressionOutput)> {
358        events
359            .iter()
360            .enumerate()
361            .filter_map(|(accepted_cycle, event)| {
362                let ResetOrData::Data(input) = event else {
363                    return None;
364                };
365                let input = input[lane];
366                if !input.valid {
367                    return None;
368                }
369                let output_cycle = accepted_cycle + rounds;
370                if output_cycle >= events.len()
371                    || events[accepted_cycle + 1..output_cycle]
372                        .iter()
373                        .any(|event| matches!(event, ResetOrData::Reset))
374                {
375                    return None;
376                }
377                Some((output_cycle, expected_output(&input, rounds)))
378            })
379            .collect()
380    }
381
382    fn observed_lane_outputs<const LANES: usize, Farm>(
383        farm: &Farm,
384        events: &[ResetOrData<CompressionFarmInput<LANES>>],
385    ) -> [Vec<(usize, CompressionOutput)>; LANES]
386    where
387        Farm: Synchronous<I = CompressionFarmInput<LANES>, O = CompressionFarmOutput<LANES>>,
388    {
389        let mut observed = core::array::from_fn(|_| Vec::new());
390        for (cycle, sample) in farm
391            .run(events.iter().copied().clock_pos_edge(10))
392            .synchronous_sample()
393            .enumerate()
394        {
395            for (lane, outputs) in observed.iter_mut().enumerate() {
396                if sample.output[lane].valid {
397                    outputs.push((cycle, sample.output[lane]));
398                }
399            }
400        }
401        observed
402    }
403
404    fn all_valid_inputs(sequence: u32, marker: u32) -> CompressionFarmInput<SMALL_LANES> {
405        core::array::from_fn(|lane| {
406            prepared_input(
407                lane,
408                marker ^ (u32::try_from(lane).expect("lane fits u32") * 0x1111_1111),
409                sequence,
410            )
411        })
412    }
413
414    fn small_farm_events() -> Vec<ResetOrData<CompressionFarmInput<SMALL_LANES>>> {
415        let mut events = vec![ResetOrData::Reset];
416
417        // Populate all four circular-memory addresses and all resetless round
418        // registers in every lane. Eight cycles also force two pointer wraps.
419        events.extend(
420            (0..8_u32).map(|cycle| ResetOrData::Data(all_valid_inputs(cycle, 0x1000_0000 + cycle))),
421        );
422
423        // Independent valid patterns and unrelated data exercise asymmetric
424        // concurrent traffic. The reset kills some but not all lane tokens.
425        events.extend([
426            ResetOrData::Data(all_valid_inputs(0x100, 0x2000_0001)),
427            ResetOrData::Data([
428                CompressionInput::default(),
429                prepared_input(1, 0x2100_0002, 0x101),
430                prepared_input(2, 0x2200_0002, 0x101),
431            ]),
432            ResetOrData::Data([
433                prepared_input(0, 0x2000_0003, 0x102),
434                CompressionInput::default(),
435                prepared_input(2, 0x2200_0003, 0x102),
436            ]),
437            ResetOrData::Data([
438                prepared_input(0, 0x2000_0004, 0x103),
439                prepared_input(1, 0x2100_0004, 0x103),
440                CompressionInput::default(),
441            ]),
442            ResetOrData::Reset,
443        ]);
444
445        // Run well beyond two additional wraps after reset. Each lane has a
446        // distinct bubble cadence, so valid isolation is visible cycle by cycle.
447        for cycle in 0..17_u32 {
448            let input = core::array::from_fn(|lane| {
449                let valid = match lane {
450                    0 => cycle % 5 != 1,
451                    1 => cycle % 4 != 2,
452                    2 => cycle % 3 != 0,
453                    _ => false,
454                };
455                if valid {
456                    prepared_input(
457                        lane,
458                        0x3000_0000
459                            ^ cycle
460                            ^ (u32::try_from(lane).expect("lane fits u32") * 0x0100_0000),
461                        0x200 + cycle,
462                    )
463                } else {
464                    CompressionInput::default()
465                }
466            });
467            events.push(ResetOrData::Data(input));
468        }
469        events.extend(std::iter::repeat_n(
470            ResetOrData::Data([CompressionInput::default(); SMALL_LANES]),
471            SMALL_ROUNDS,
472        ));
473        events
474    }
475
476    #[test]
477    fn small_farm_rhdl_and_iverilog_preserve_asymmetric_lane_isolation() -> Result<(), RHDLError> {
478        let farm = CompressionFarm::<SMALL_LANES, SMALL_ROUNDS, SMALL_ADDRESS_BITS>::default();
479        let events = small_farm_events();
480        let observed = observed_lane_outputs(&farm, &events);
481
482        for (lane, lane_outputs) in observed.iter().enumerate() {
483            assert_eq!(
484                lane_outputs,
485                &expected_lane_outputs(&events, lane, SMALL_ROUNDS),
486                "lane {lane} changed latency, data, tag, or valid behavior"
487            );
488            assert!(lane_outputs.iter().all(|(_, output)| {
489                usize::try_from(word_u32(output.tag) >> 24).expect("lane byte fits usize") == lane
490            }));
491        }
492        assert_ne!(observed[0].len(), observed[2].len());
493        assert!(events.len() > 4 * SMALL_ROUNDS);
494
495        let test_bench = farm
496            .run(events.into_iter().clock_pos_edge(10))
497            .collect::<SynchronousTestBench<_, _>>();
498        test_bench
499            // `clock_pos_edge` records multiple timed cases per issue cycle.
500            // Eighteen cases place comparison after the resetless four-stage
501            // datapaths and registered memory outputs have all become known.
502            .rtl(&farm, &TestBenchOptions::default().skip(18))?
503            .run_iverilog()?;
504        Ok(())
505    }
506
507    #[test]
508    fn small_replicated_farm_matches_the_ordinary_farm_and_software_oracle() {
509        let ordinary = CompressionFarm::<SMALL_LANES, SMALL_ROUNDS, SMALL_ADDRESS_BITS>::default();
510        let replicated =
511            ReplicatedCompressionFarm::<SMALL_LANES, SMALL_ROUNDS, SMALL_ADDRESS_BITS>::default();
512        let events = small_farm_events();
513        let ordinary_observed = observed_lane_outputs(&ordinary, &events);
514        let replicated_observed = observed_lane_outputs(&replicated, &events);
515
516        assert_eq!(replicated_observed, ordinary_observed);
517        for (lane, lane_outputs) in replicated_observed.iter().enumerate() {
518            assert_eq!(
519                lane_outputs,
520                &expected_lane_outputs(&events, lane, SMALL_ROUNDS),
521                "replicated lane {lane} diverged from the independent round model"
522            );
523        }
524    }
525
526    #[test]
527    fn small_replicated_farm_lowers_one_lane_module_and_three_instances() -> Result<(), RHDLError> {
528        let farm =
529            ReplicatedCompressionFarm::<SMALL_LANES, SMALL_ROUNDS, SMALL_ADDRESS_BITS>::default();
530        let descriptor = farm.descriptor("sha256_replicated_compression_farm".into())?;
531        let hdl = descriptor.hdl()?.modules.pretty();
532
533        assert_eq!(
534            hdl.matches("module sha256_replicated_compression_farm_lanes_prototype(")
535                .count(),
536            1
537        );
538        for index in 0..SMALL_LANES {
539            assert_eq!(
540                hdl.matches(&format!(
541                    "sha256_replicated_compression_farm_lanes_prototype c{index}("
542                ))
543                .count(),
544                1,
545                "replicated lane instance c{index} must appear exactly once"
546            );
547        }
548        assert_eq!(hdl.matches("reg [255:0] mem[3:0]").count(), 1);
549        Ok(())
550    }
551
552    #[test]
553    fn full_three_lane_farm_matches_software_for_more_than_two_wraps() {
554        // Three copies of the 64-stage simulation state exceed libtest's
555        // default worker-stack allocation before the first modeled cycle. The
556        // stack override is host-test scaffolding only; it is not part of the
557        // RHDL circuit or generated RTL.
558        std::thread::Builder::new()
559            .name("sha256-full-three-lane-rhdl".into())
560            .stack_size(64 * 1024 * 1024)
561            .spawn(run_full_three_lane_farm_test)
562            .expect("spawn full-farm RHDL test with an explicit stack")
563            .join()
564            .expect("full-farm RHDL test thread must not panic");
565    }
566
567    fn run_full_three_lane_farm_test() {
568        let mut events = vec![ResetOrData::Reset];
569        events.extend((0..7_u32).map(|cycle| {
570            ResetOrData::Data(core::array::from_fn(|lane| {
571                prepared_input(lane, 0x4000_0000 ^ cycle, cycle)
572            }))
573        }));
574        events.push(ResetOrData::Reset);
575
576        // 137 post-reset issue cycles exceed two complete 64-entry pointer
577        // wraps while retaining independent valid patterns in all three lanes.
578        for cycle in 0..137_u32 {
579            let input = core::array::from_fn(|lane| {
580                let valid = match lane {
581                    0 => cycle % 11 != 7,
582                    1 => cycle % 7 != 3,
583                    2 => cycle % 5 != 1,
584                    _ => false,
585                };
586                if valid {
587                    prepared_input(
588                        lane,
589                        0x5000_0000
590                            ^ cycle.rotate_left(u32::try_from(lane).expect("lane fits u32")),
591                        0x1000 + cycle,
592                    )
593                } else {
594                    CompressionInput::default()
595                }
596            });
597            events.push(ResetOrData::Data(input));
598        }
599        events.extend(std::iter::repeat_n(
600            ResetOrData::Data([CompressionInput::default(); PERFORMANCE_FARM_LANES]),
601            ROUND_COUNT,
602        ));
603
604        let farm = ThreeLaneCompressionFarm::default();
605        let observed = observed_lane_outputs(&farm, &events);
606        for (lane, lane_outputs) in observed.iter().enumerate() {
607            assert_eq!(
608                lane_outputs,
609                &expected_lane_outputs(&events, lane, ROUND_COUNT),
610                "full lane {lane} diverged from the independent SHA-256 model"
611            );
612            assert!(lane_outputs.len() > FULL_LANE_ADDRESS_BITS * 8);
613            assert!(lane_outputs.iter().all(|(_, output)| {
614                usize::try_from(word_u32(output.tag) >> 24).expect("lane byte fits usize") == lane
615            }));
616        }
617    }
618
619    #[test]
620    fn checked_small_farm_generator_contains_three_independent_memories() -> Result<(), RHDLError> {
621        let hdl =
622            compression_farm_checked_verilog::<SMALL_LANES, SMALL_ROUNDS, SMALL_ADDRESS_BITS>()?;
623        assert!(hdl.contains("module sha256_compression_farm"));
624        assert_eq!(hdl.matches("reg [255:0] mem[3:0]").count(), SMALL_LANES);
625        Ok(())
626    }
627
628    #[test]
629    #[ignore = "explicit full-hierarchy render gate; excluded from routine unit tests"]
630    fn full_replicated_farm_generation_only_unchecked_render() -> Result<(), RHDLError> {
631        let hdl = try_full_three_lane_farm_generation_only_unchecked_verilog()?;
632        assert!(hdl.contains("module sha256_compression_farm_3x64"));
633        assert_eq!(hdl.matches("reg [255:0] mem[63:0]").count(), 1);
634        assert_eq!(
635            hdl.matches("module sha256_compression_farm_3x64_lanes_prototype(")
636                .count(),
637            1
638        );
639        for index in 0..PERFORMANCE_FARM_LANES {
640            assert_eq!(
641                hdl.matches(&format!(
642                    "sha256_compression_farm_3x64_lanes_prototype c{index}("
643                ))
644                .count(),
645                1
646            );
647        }
648        Ok(())
649    }
650}