Skip to main content

wots_rhdl/
top.rs

1//! Three-cluster SHA-profile signer top.
2//!
3//! [`SignerTop`] is the narrow global layer above three structurally identical
4//! four-context lane clusters. It admits at most one job per cycle, preserves
5//! each cluster's local `0..=3` tag namespace, and merges only registered
6//! 512-bit cluster beats plus compact error metadata. Complete context results
7//! and context RAM ports never cross this boundary.
8//!
9//! The data path has two explicit ownership stages. A rotating arbiter first
10//! registers one cluster as frame owner. On the following cycle, a canonical
11//! cluster beat may enter the one-entry global elastic register. The owner stays
12//! locked through beat 34, so frames cannot interleave. When that final beat is
13//! captured, its cluster may release local credit because the global register
14//! now owns the transfer. External delivery is counted separately when the
15//! downstream consumer accepts the registered final beat.
16//!
17//! While an output beat is stalled, every data and metadata bit remains stable.
18//! If the consumer accepts it, the register may capture the next beat in the
19//! same cycle. Capturing a final beat also pre-grants a different already-primed
20//! cluster. Consequently that next cluster can refill the global register in
21//! the stalled final beat's eventual downstream-handshake cycle, without an
22//! extra inter-frame output bubble after the registered grant stage.
23//!
24//! Error completions use an independent one-entry elastic merge. The selected
25//! cluster alone receives `error_ready`; after capture, the global register
26//! owns the job identity, local context, and cluster identity until the host
27//! accepts exactly one notification.
28//!
29//! Canonical `keep`, `last`, final padding, beat count, and frame job identity
30//! are checked before a cluster receives data `ready`. A violation is never
31//! captured, latches a structural fault, and requires shared reset. Shared reset
32//! also flushes all three clusters, grant state, output-valid bits, and error
33//! ownership. Resetless beat/job registers are ignored while their resettable
34//! valid bits are clear.
35//!
36//! This module is Rust/RHDL source. Behavioral or generated-hierarchy evidence
37//! does not imply synthesis, placement, routing, resource, timing, throughput,
38//! side-channel, or FPGA-card evidence.
39
40use rhdl::{core::ReplicatedSynchronous, prelude::*};
41use rhdl_fpga::core::dff::DFF;
42use rhdl_primitives::NoResetDff;
43use sha256_rhdl::lane::{ExternalFullCompressionLane, InlineCompressionLane};
44
45use crate::{
46    blocks::HashBytes,
47    cluster::{CompressionLaneComponent, LaneLocalBeat, LaneLocalCluster, LaneLocalClusterInput},
48    digits::MessageBytes,
49};
50
51/// Number of independently placed lane-local signer clusters.
52pub const SIGNER_CLUSTER_COUNT: usize = 3;
53/// Number of beats in one canonical fused output frame.
54pub const SIGNER_FRAME_BEATS: usize = 35;
55
56const _: () = {
57    assert!(SIGNER_CLUSTER_COUNT == 3);
58    assert!(SIGNER_FRAME_BEATS == 35);
59};
60
61/// One selectable registered cluster beat.
62#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
63pub struct GlobalFrameSource {
64    /// A complete local registered beat is available.
65    pub valid: bool,
66    /// Registered cluster beat and opaque job metadata.
67    pub beat: LaneLocalBeat,
68}
69
70/// Global output beat with source-cluster identity outside SHA state.
71#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
72pub struct SignerOutputBeat {
73    /// Canonical 512-bit fused-result transfer.
74    pub beat: LaneLocalBeat,
75    /// Cluster `0..=2` that produced the frame.
76    pub cluster: b2,
77}
78
79/// Inputs for defensive canonical-control and frame-identity validation.
80#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
81pub struct GlobalBeatValidationInput {
82    /// Candidate registered cluster beat.
83    pub beat: LaneLocalBeat,
84    /// Expected frame beat index `0..=34`.
85    pub expected_beat: b6,
86    /// Job identity retained from beat zero.
87    pub expected_job_id: b32,
88    /// Whether the retained identity must already match.
89    pub check_job_id: bool,
90}
91
92/// Validate one candidate before granting its cluster `ready`.
93///
94/// Beats 0 through 33 require all 64 byte lanes and must not assert `last`.
95/// Beat 34 requires only its low 32 lanes, asserts `last`, and keeps invalid high
96/// lanes zero. Every beat after beat zero must retain the same opaque job ID.
97#[kernel]
98#[allow(clippy::comparison_chain, clippy::needless_range_loop)] // Pinned RHDL does not lower ordering matches or iterators.
99pub fn global_beat_is_canonical_kernel(input: GlobalBeatValidationInput) -> bool {
100    let job_matches = !input.check_job_id || input.beat.job_id == input.expected_job_id;
101    let mut final_padding_zero = true;
102    for lane in 32..64 {
103        if input.beat.data[lane] != b8(0) {
104            final_padding_zero = false;
105        }
106    }
107    let control_matches = if input.expected_beat < b6(34) {
108        input.beat.keep == b64(0xffff_ffff_ffff_ffff) && !input.beat.last
109    } else if input.expected_beat == b6(34) {
110        input.beat.keep == b64(0xffff_ffff) && input.beat.last && final_padding_zero
111    } else {
112        false
113    };
114    job_matches && control_matches
115}
116
117/// Three-way rotating selection result.
118#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
119pub struct GlobalLaneSelection {
120    /// At least one eligible cluster exists.
121    pub valid: bool,
122    /// Selected cluster `0..=2`.
123    pub cluster: b2,
124    /// Cursor following the selected cluster.
125    pub next_cursor: b2,
126}
127
128/// Select one of three eligible clusters with rotating fairness.
129#[kernel]
130#[allow(clippy::assign_op_pattern)] // Compound assignment is not lowered by pinned RHDL.
131pub fn select_global_lane_kernel(
132    eligible: [bool; SIGNER_CLUSTER_COUNT],
133    cursor: b2,
134) -> GlobalLaneSelection {
135    let normalized_cursor = if cursor < b2(3) { cursor } else { b2(0) };
136    let mut selection = GlobalLaneSelection {
137        next_cursor: normalized_cursor,
138        ..GlobalLaneSelection::default()
139    };
140    let mut scan = normalized_cursor;
141    for _offset in 0..SIGNER_CLUSTER_COUNT {
142        if !selection.valid && eligible[scan] {
143            selection.valid = true;
144            selection.cluster = scan;
145            selection.next_cursor = if scan == b2(2) { b2(0) } else { scan + b2(1) };
146        }
147        scan = if scan == b2(2) { b2(0) } else { scan + b2(1) };
148    }
149    selection
150}
151
152/// Input to the registered, frame-locked data merge.
153#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
154pub struct GlobalFrameMergeInput {
155    /// Registered beat heads from clusters zero through two.
156    pub clusters: [GlobalFrameSource; SIGNER_CLUSTER_COUNT],
157    /// Downstream acceptance for the global output register.
158    pub output_ready: bool,
159}
160
161/// Data-merge controls, registered output, and audit pulses.
162#[allow(clippy::struct_excessive_bools)] // Independent handshake and audit wires.
163#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
164pub struct GlobalFrameMergeOutput {
165    /// Ready is asserted only to the locked cluster when a canonical beat fits.
166    pub cluster_ready: [bool; SIGNER_CLUSTER_COUNT],
167    /// A global registered output beat is available.
168    pub output_valid: bool,
169    /// Registered beat and source identity, stable under downstream stalls.
170    pub output: SignerOutputBeat,
171    /// One frame currently owns the cluster-side merge.
172    pub frame_active: bool,
173    /// Locked cluster, meaningful while `frame_active`.
174    pub frame_cluster: b2,
175    /// Next cluster-side beat index expected from the owner.
176    pub expected_beat: b6,
177    /// A cluster beat entered the global register this cycle.
178    pub captured: bool,
179    /// Captured beat was beat 34 and returned local cluster credit.
180    pub captured_final: bool,
181    /// Registered final beat transferred to the external consumer this cycle.
182    pub delivered_final: bool,
183    /// Sticky canonical-control, job-identity, or ownership fault.
184    pub fault: bool,
185}
186
187/// Narrow resettable state for [`GlobalFrameMerge`].
188#[allow(clippy::struct_excessive_bools)] // Each bit is an independent protocol owner.
189#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
190pub struct GlobalFrameMergeControl {
191    /// Rotating frame-grant cursor.
192    pub cursor: b2,
193    /// One cluster owns the cluster-side frame merge.
194    pub frame_active: bool,
195    /// Current frame owner.
196    pub frame_cluster: b2,
197    /// Next expected beat index.
198    pub expected_beat: b6,
199    /// One global output beat is retained.
200    pub output_valid: bool,
201    /// Source cluster stored alongside the output beat.
202    pub output_cluster: b2,
203    /// Sticky structural fault.
204    pub fault: bool,
205}
206
207/// Registered global frame merge with one elastic output slot.
208#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
209pub struct GlobalFrameMerge {
210    control: DFF<GlobalFrameMergeControl>,
211    frame_job_id: NoResetDff<b32>,
212    output: NoResetDff<LaneLocalBeat>,
213}
214
215impl Default for GlobalFrameMerge {
216    fn default() -> Self {
217        Self {
218            control: DFF::new(GlobalFrameMergeControl::default()),
219            frame_job_id: NoResetDff::new(),
220            output: NoResetDff::new(),
221        }
222    }
223}
224
225impl SynchronousIO for GlobalFrameMerge {
226    type I = GlobalFrameMergeInput;
227    type O = GlobalFrameMergeOutput;
228    type Kernel = global_frame_merge_kernel;
229}
230
231#[kernel]
232fn select_frame_source_kernel(
233    sources: [GlobalFrameSource; SIGNER_CLUSTER_COUNT],
234    cluster: b2,
235) -> GlobalFrameSource {
236    match cluster {
237        Bits::<2>(0) => sources[0],
238        Bits::<2>(1) => sources[1],
239        _ => sources[2],
240    }
241}
242
243/// State transition for [`GlobalFrameMerge`].
244#[kernel]
245pub fn global_frame_merge_kernel(
246    clock_reset: ClockReset,
247    input: GlobalFrameMergeInput,
248    q: GlobalFrameMergeQ,
249) -> (GlobalFrameMergeOutput, GlobalFrameMergeD) {
250    let resetting = clock_reset.reset.any();
251    let mut control = q.control;
252    let mut frame_job_id = q.frame_job_id;
253    let mut output_beat = q.output;
254    let mut cluster_ready = [false; SIGNER_CLUSTER_COUNT];
255    let mut captured = false;
256    let mut captured_final = false;
257
258    let ownership_fault = q.control.cursor >= b2(3)
259        || (q.control.frame_active && q.control.frame_cluster >= b2(3))
260        || (q.control.frame_active && q.control.expected_beat >= b6(35))
261        || (q.control.output_valid && q.control.output_cluster >= b2(3));
262    let output_valid = !resetting && q.control.output_valid && !q.control.fault && !ownership_fault;
263    let output_handshake = output_valid && input.output_ready;
264    let delivered_final = output_handshake && q.output.last;
265    let output_capacity = !q.control.output_valid || output_handshake;
266    if output_handshake {
267        control.output_valid = false;
268    }
269
270    if !resetting && ownership_fault {
271        control.fault = true;
272        control.frame_active = false;
273    }
274
275    if !resetting && q.control.frame_active && !q.control.fault && !ownership_fault {
276        let source = select_frame_source_kernel(input.clusters, q.control.frame_cluster);
277        if source.valid {
278            let canonical = global_beat_is_canonical_kernel(GlobalBeatValidationInput {
279                beat: source.beat,
280                expected_beat: q.control.expected_beat,
281                expected_job_id: q.frame_job_id,
282                check_job_id: q.control.expected_beat != b6(0),
283            });
284            if !canonical {
285                control.fault = true;
286                control.frame_active = false;
287            } else if output_capacity {
288                cluster_ready[q.control.frame_cluster] = true;
289                output_beat = source.beat;
290                control.output_cluster = q.control.frame_cluster;
291                control.output_valid = true;
292                captured = true;
293                if q.control.expected_beat == b6(0) {
294                    frame_job_id = source.beat.job_id;
295                }
296                if q.control.expected_beat == b6(34) {
297                    captured_final = true;
298                    control.frame_active = false;
299                    control.expected_beat = b6(0);
300
301                    // Register a different already-primed owner while the
302                    // final beat moves into the global output register. That
303                    // owner can refill on the final beat's later handshake.
304                    let eligible = [
305                        input.clusters[0].valid && q.control.frame_cluster != b2(0),
306                        input.clusters[1].valid && q.control.frame_cluster != b2(1),
307                        input.clusters[2].valid && q.control.frame_cluster != b2(2),
308                    ];
309                    let selection = select_global_lane_kernel(eligible, q.control.cursor);
310                    if selection.valid {
311                        control.frame_active = true;
312                        control.frame_cluster = selection.cluster;
313                        control.cursor = selection.next_cursor;
314                    }
315                } else {
316                    control.expected_beat = q.control.expected_beat + b6(1);
317                }
318            }
319        }
320    } else if !resetting && !q.control.fault && !ownership_fault {
321        let eligible = [
322            input.clusters[0].valid,
323            input.clusters[1].valid,
324            input.clusters[2].valid,
325        ];
326        let selection = select_global_lane_kernel(eligible, q.control.cursor);
327        if selection.valid {
328            control.frame_active = true;
329            control.frame_cluster = selection.cluster;
330            control.expected_beat = b6(0);
331            control.cursor = selection.next_cursor;
332        }
333    }
334
335    if resetting {
336        control = GlobalFrameMergeControl::default();
337        cluster_ready = [false; SIGNER_CLUSTER_COUNT];
338        captured = false;
339        captured_final = false;
340    }
341
342    let output = GlobalFrameMergeOutput {
343        cluster_ready,
344        output_valid,
345        output: SignerOutputBeat {
346            beat: q.output,
347            cluster: q.control.output_cluster,
348        },
349        frame_active: !resetting && q.control.frame_active,
350        frame_cluster: q.control.frame_cluster,
351        expected_beat: q.control.expected_beat,
352        captured,
353        captured_final,
354        delivered_final,
355        fault: !resetting && q.control.fault,
356    };
357    let d = GlobalFrameMergeD {
358        control,
359        frame_job_id,
360        output: output_beat,
361    };
362    (output, d)
363}
364
365/// One held lane-local error completion offered to the global merge.
366#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
367pub struct GlobalErrorSource {
368    /// A lane-local error completion is held.
369    pub valid: bool,
370    /// Opaque job identity retained by the lane cluster.
371    pub job_id: b32,
372    /// Local context `0..=3` that owned the failed job.
373    pub context: b2,
374}
375
376/// Error completion returned to the host with global cluster identity.
377#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
378pub struct SignerErrorCompletion {
379    /// Opaque failed-job identity.
380    pub job_id: b32,
381    /// Source cluster `0..=2`.
382    pub cluster: b2,
383    /// Source-local context `0..=3`.
384    pub context: b2,
385}
386
387/// Input to the independent global error merge.
388#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
389pub struct GlobalErrorMergeInput {
390    /// Held lane-local error completions.
391    pub clusters: [GlobalErrorSource; SIGNER_CLUSTER_COUNT],
392    /// Host acceptance for the global held error register.
393    pub error_ready: bool,
394}
395
396/// Registered error output and per-cluster capture handshakes.
397#[allow(clippy::struct_excessive_bools)] // Independent handshake and audit wires.
398#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
399pub struct GlobalErrorMergeOutput {
400    /// Ready is asserted only to the selected lane-local error source.
401    pub cluster_ready: [bool; SIGNER_CLUSTER_COUNT],
402    /// A registered error completion is held for the host.
403    pub error_valid: bool,
404    /// Registered error metadata, stable under backpressure.
405    pub error: SignerErrorCompletion,
406    /// A cluster error entered the global register this cycle.
407    pub captured: bool,
408    /// A registered error transferred to the host this cycle.
409    pub delivered: bool,
410    /// Sticky cursor or ownership fault.
411    pub fault: bool,
412}
413
414/// Resettable control for [`GlobalErrorMerge`].
415#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
416pub struct GlobalErrorMergeControl {
417    /// Rotating capture cursor.
418    pub cursor: b2,
419    /// One global error completion is retained.
420    pub output_valid: bool,
421    /// Source cluster stored with the output metadata.
422    pub output_cluster: b2,
423    /// Source-local context stored with the output metadata.
424    pub output_context: b2,
425    /// Sticky structural fault.
426    pub fault: bool,
427}
428
429/// Independent one-entry elastic error-completion merge.
430#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
431pub struct GlobalErrorMerge {
432    control: DFF<GlobalErrorMergeControl>,
433    job_id: NoResetDff<b32>,
434}
435
436impl Default for GlobalErrorMerge {
437    fn default() -> Self {
438        Self {
439            control: DFF::new(GlobalErrorMergeControl::default()),
440            job_id: NoResetDff::new(),
441        }
442    }
443}
444
445impl SynchronousIO for GlobalErrorMerge {
446    type I = GlobalErrorMergeInput;
447    type O = GlobalErrorMergeOutput;
448    type Kernel = global_error_merge_kernel;
449}
450
451/// State transition for [`GlobalErrorMerge`].
452#[kernel]
453pub fn global_error_merge_kernel(
454    clock_reset: ClockReset,
455    input: GlobalErrorMergeInput,
456    q: GlobalErrorMergeQ,
457) -> (GlobalErrorMergeOutput, GlobalErrorMergeD) {
458    let resetting = clock_reset.reset.any();
459    let mut control = q.control;
460    let mut job_id = q.job_id;
461    let mut cluster_ready = [false; SIGNER_CLUSTER_COUNT];
462    let mut captured = false;
463    let ownership_fault =
464        q.control.cursor >= b2(3) || (q.control.output_valid && q.control.output_cluster >= b2(3));
465    let error_valid = !resetting && q.control.output_valid && !q.control.fault && !ownership_fault;
466    let delivered = error_valid && input.error_ready;
467    let capacity = !q.control.output_valid || delivered;
468    if delivered {
469        control.output_valid = false;
470    }
471
472    if !resetting && ownership_fault {
473        control.fault = true;
474        control.output_valid = false;
475    }
476
477    if !resetting && !q.control.fault && !ownership_fault && capacity {
478        let eligible = [
479            input.clusters[0].valid,
480            input.clusters[1].valid,
481            input.clusters[2].valid,
482        ];
483        let selection = select_global_lane_kernel(eligible, q.control.cursor);
484        if selection.valid {
485            cluster_ready[selection.cluster] = true;
486            control.cursor = selection.next_cursor;
487            control.output_valid = true;
488            control.output_cluster = selection.cluster;
489            let selected = match selection.cluster {
490                Bits::<2>(0) => input.clusters[0],
491                Bits::<2>(1) => input.clusters[1],
492                _ => input.clusters[2],
493            };
494            control.output_context = selected.context;
495            job_id = selected.job_id;
496            captured = true;
497        }
498    }
499
500    if resetting {
501        control = GlobalErrorMergeControl::default();
502        cluster_ready = [false; SIGNER_CLUSTER_COUNT];
503        captured = false;
504    }
505
506    let output = GlobalErrorMergeOutput {
507        cluster_ready,
508        error_valid,
509        error: SignerErrorCompletion {
510            job_id: q.job_id,
511            cluster: q.control.output_cluster,
512            context: q.control.output_context,
513        },
514        captured,
515        delivered,
516        fault: !resetting && q.control.fault,
517    };
518    let d = GlobalErrorMergeD { control, job_id };
519    (output, d)
520}
521
522/// External streaming input to the three-cluster signer top.
523#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
524pub struct SignerTopInput {
525    /// Offer one private-seed fused signing operation.
526    pub start_valid: bool,
527    /// Private seed consumed by the operation.
528    pub private_seed: HashBytes,
529    /// Already-hashed message selecting WOTS positions.
530    pub message: MessageBytes,
531    /// Opaque identity returned with data or error completion.
532    pub job_id: b32,
533    /// Downstream acceptance for the registered data output.
534    pub output_ready: bool,
535    /// Host acceptance for the independent registered error output.
536    pub error_ready: bool,
537}
538
539/// Global signer status, data output, and independent error output.
540///
541/// `output` is meaningful only while `output_valid` is true, and `error` is
542/// meaningful only while `error_valid` is true. Each registered payload remains
543/// stable while its valid signal is asserted and the corresponding ready input
544/// is false. When a valid signal is false, its resetless payload bits may be
545/// stale or unspecified and must not be consumed.
546#[allow(clippy::struct_excessive_bools)] // Independent host and audit handshakes.
547#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
548pub struct SignerTopOutput {
549    /// At least one cluster can accept the offered job.
550    pub start_ready: bool,
551    /// The offered job transferred this cycle.
552    pub start_accepted: bool,
553    /// Cluster selected for the offered job.
554    pub start_cluster: b2,
555    /// Per-cluster admission readiness.
556    pub cluster_start_ready: [bool; SIGNER_CLUSTER_COUNT],
557    /// Per-cluster active-context status.
558    pub cluster_context_active: [[bool; 4]; SIGNER_CLUSTER_COUNT],
559    /// Per-cluster sticky fault status.
560    pub cluster_fault: [bool; SIGNER_CLUSTER_COUNT],
561    /// Per-cluster successful final-capture credit releases.
562    pub cluster_credit_release: [bool; SIGNER_CLUSTER_COUNT],
563    /// A registered global data beat is available and `output` is meaningful.
564    pub output_valid: bool,
565    /// Registered data beat and source cluster.
566    ///
567    /// Meaningful only when `output_valid`; stable while `output_valid` is true
568    /// and the input `output_ready` is false.
569    pub output: SignerOutputBeat,
570    /// External consumer accepted a final data beat this cycle.
571    pub delivered_final: bool,
572    /// A registered global error completion is available and `error` is
573    /// meaningful.
574    pub error_valid: bool,
575    /// Registered error metadata.
576    ///
577    /// Meaningful only when `error_valid`; stable while `error_valid` is true
578    /// and the input `error_ready` is false.
579    pub error: SignerErrorCompletion,
580    /// Host accepted one error completion this cycle.
581    pub delivered_error: bool,
582    /// Sticky cluster or global structural fault.
583    pub fault: bool,
584}
585
586/// Narrow resettable global admission/fault state.
587#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
588pub struct SignerTopControl {
589    /// Rotating one-job admission cursor.
590    pub admission_cursor: b2,
591    /// Sticky union of cluster and merge faults.
592    pub fault: bool,
593}
594
595/// Exactly three identical four-context lane clusters and narrow global merges.
596#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
597pub struct SignerTop<
598    const ROUNDS: usize,
599    const ADDRESS_BITS: usize,
600    L = InlineCompressionLane<ROUNDS, ADDRESS_BITS>,
601> where
602    rhdl::bits::W<ROUNDS>: BitWidth,
603    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
604    L: CompressionLaneComponent,
605{
606    clusters:
607        ReplicatedSynchronous<LaneLocalCluster<ROUNDS, ADDRESS_BITS, L>, SIGNER_CLUSTER_COUNT>,
608    frames: GlobalFrameMerge,
609    errors: GlobalErrorMerge,
610    control: DFF<SignerTopControl>,
611}
612
613impl<const ROUNDS: usize, const ADDRESS_BITS: usize, L> Default
614    for SignerTop<ROUNDS, ADDRESS_BITS, L>
615where
616    rhdl::bits::W<ROUNDS>: BitWidth,
617    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
618    L: CompressionLaneComponent + Default,
619{
620    fn default() -> Self {
621        Self {
622            clusters: ReplicatedSynchronous::new(LaneLocalCluster::default()),
623            frames: GlobalFrameMerge::default(),
624            errors: GlobalErrorMerge::default(),
625            control: DFF::new(SignerTopControl::default()),
626        }
627    }
628}
629
630impl<const ROUNDS: usize, const ADDRESS_BITS: usize, L> SynchronousIO
631    for SignerTop<ROUNDS, ADDRESS_BITS, L>
632where
633    rhdl::bits::W<ROUNDS>: BitWidth,
634    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
635    L: CompressionLaneComponent,
636{
637    type I = SignerTopInput;
638    type O = SignerTopOutput;
639    type Kernel = signer_top_with_lane_kernel<ROUNDS, ADDRESS_BITS, L>;
640}
641
642/// State transition and narrow global connectivity for an injected lane.
643#[kernel]
644#[allow(clippy::needless_range_loop)] // Iterators are not lowered by pinned RHDL kernels.
645pub fn signer_top_with_lane_kernel<
646    const ROUNDS: usize,
647    const ADDRESS_BITS: usize,
648    L: CompressionLaneComponent,
649>(
650    clock_reset: ClockReset,
651    input: SignerTopInput,
652    q: SignerTopQ<ROUNDS, ADDRESS_BITS, L>,
653) -> (SignerTopOutput, SignerTopD<ROUNDS, ADDRESS_BITS, L>)
654where
655    rhdl::bits::W<ROUNDS>: BitWidth,
656    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
657{
658    let resetting = clock_reset.reset.any();
659    let cluster_start_ready = [
660        q.clusters[0].start_ready,
661        q.clusters[1].start_ready,
662        q.clusters[2].start_ready,
663    ];
664    let cluster_fault = [
665        q.clusters[0].fault,
666        q.clusters[1].fault,
667        q.clusters[2].fault,
668    ];
669    let observed_fault = cluster_fault[0]
670        || cluster_fault[1]
671        || cluster_fault[2]
672        || q.frames.fault
673        || q.errors.fault
674        || q.control.admission_cursor >= b2(3);
675    let fault = q.control.fault || observed_fault;
676    let admission = select_global_lane_kernel(cluster_start_ready, q.control.admission_cursor);
677    let start_ready = !resetting && !fault && admission.valid;
678    let start_accepted = start_ready && input.start_valid;
679
680    let mut control = q.control;
681    if start_accepted {
682        control.admission_cursor = admission.next_cursor;
683    }
684    if !resetting && observed_fault {
685        control.fault = true;
686    }
687    if resetting {
688        control = SignerTopControl::default();
689    }
690
691    let mut cluster_inputs = [LaneLocalClusterInput::default(); SIGNER_CLUSTER_COUNT];
692    for cluster in 0..SIGNER_CLUSTER_COUNT {
693        cluster_inputs[cluster].output_ready = !resetting && q.frames.cluster_ready[cluster];
694        cluster_inputs[cluster].error_ready = !resetting && q.errors.cluster_ready[cluster];
695    }
696    if start_accepted {
697        cluster_inputs[admission.cluster].start_valid = true;
698        cluster_inputs[admission.cluster].private_seed = input.private_seed;
699        cluster_inputs[admission.cluster].message = input.message;
700        cluster_inputs[admission.cluster].job_id = input.job_id;
701    }
702
703    let frame_sources = [
704        GlobalFrameSource {
705            valid: q.clusters[0].output_valid,
706            beat: q.clusters[0].output,
707        },
708        GlobalFrameSource {
709            valid: q.clusters[1].output_valid,
710            beat: q.clusters[1].output,
711        },
712        GlobalFrameSource {
713            valid: q.clusters[2].output_valid,
714            beat: q.clusters[2].output,
715        },
716    ];
717    let error_sources = [
718        GlobalErrorSource {
719            valid: q.clusters[0].error_valid,
720            job_id: q.clusters[0].error_job_id,
721            context: q.clusters[0].error_context,
722        },
723        GlobalErrorSource {
724            valid: q.clusters[1].error_valid,
725            job_id: q.clusters[1].error_job_id,
726            context: q.clusters[1].error_context,
727        },
728        GlobalErrorSource {
729            valid: q.clusters[2].error_valid,
730            job_id: q.clusters[2].error_job_id,
731            context: q.clusters[2].error_context,
732        },
733    ];
734
735    let output = SignerTopOutput {
736        start_ready,
737        start_accepted,
738        start_cluster: admission.cluster,
739        cluster_start_ready: if resetting || fault {
740            [false; SIGNER_CLUSTER_COUNT]
741        } else {
742            cluster_start_ready
743        },
744        cluster_context_active: [
745            q.clusters[0].context_active,
746            q.clusters[1].context_active,
747            q.clusters[2].context_active,
748        ],
749        cluster_fault: if resetting {
750            [false; SIGNER_CLUSTER_COUNT]
751        } else {
752            cluster_fault
753        },
754        cluster_credit_release: [
755            !resetting && q.clusters[0].output_credit_release,
756            !resetting && q.clusters[1].output_credit_release,
757            !resetting && q.clusters[2].output_credit_release,
758        ],
759        output_valid: !resetting && q.frames.output_valid,
760        output: q.frames.output,
761        delivered_final: !resetting && q.frames.delivered_final,
762        error_valid: !resetting && q.errors.error_valid,
763        error: q.errors.error,
764        delivered_error: !resetting && q.errors.delivered,
765        fault: !resetting && fault,
766    };
767    let d = SignerTopD::<ROUNDS, ADDRESS_BITS, L> {
768        clusters: cluster_inputs,
769        frames: GlobalFrameMergeInput {
770            clusters: frame_sources,
771            output_ready: input.output_ready,
772        },
773        errors: GlobalErrorMergeInput {
774            clusters: error_sources,
775            error_ready: input.error_ready,
776        },
777        control,
778    };
779    (output, d)
780}
781
782/// Native-lane transition retained for the original two-const API.
783///
784/// Generic parent lowering uses [`signer_top_with_lane_kernel`]. This wrapper
785/// keeps callers of `signer_top_kernel::<ROUNDS, ADDRESS_BITS>`
786/// source-compatible while selecting the checked inline lane.
787#[kernel]
788pub fn signer_top_kernel<const ROUNDS: usize, const ADDRESS_BITS: usize>(
789    clock_reset: ClockReset,
790    input: SignerTopInput,
791    q: SignerTopQ<ROUNDS, ADDRESS_BITS>,
792) -> (SignerTopOutput, SignerTopD<ROUNDS, ADDRESS_BITS>)
793where
794    rhdl::bits::W<ROUNDS>: BitWidth,
795    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
796{
797    signer_top_with_lane_kernel::<ROUNDS, ADDRESS_BITS, InlineCompressionLane<ROUNDS, ADDRESS_BITS>>(
798        clock_reset,
799        input,
800        q,
801    )
802}
803
804/// Production three-cluster, 64-round SHA-profile signer top.
805pub type Sha256SignerTop = SignerTop<64, 6>;
806
807/// Production signer skeleton linked with a separately generated full SHA lane.
808///
809/// Behavioral simulation is identical to [`Sha256SignerTop`]. Its emitted HDL
810/// intentionally leaves `sha256_compression_lane` unresolved until the exact
811/// RHDL-generated lane artifact is supplied to an external parser.
812pub type ModularSha256SignerTop = SignerTop<64, 6, ExternalFullCompressionLane>;
813
814impl<const ROUNDS: usize, const ADDRESS_BITS: usize, L> SignerTop<ROUNDS, ADDRESS_BITS, L>
815where
816    rhdl::bits::W<ROUNDS>: BitWidth,
817    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
818    L: CompressionLaneComponent,
819{
820    /// Reads per-context compression-acceptance pulses from software simulation state.
821    ///
822    /// This hidden host helper is not part of [`SignerTopInput`],
823    /// [`SignerTopOutput`], or the generated hardware interface. It exists so
824    /// source-simulation gates can audit the exact amount of accepted SHA work
825    /// without widening the production datapath or reproducing scheduler state
826    /// in a test-only model.
827    #[doc(hidden)]
828    #[must_use]
829    pub fn simulation_request_accepted(
830        state: &<Self as Synchronous>::S,
831    ) -> [[bool; 4]; SIGNER_CLUSTER_COUNT] {
832        [
833            state.0.clusters[0].request_accepted,
834            state.0.clusters[1].request_accepted,
835            state.0.clusters[2].request_accepted,
836        ]
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use crate::cluster::LaneLocalClusterOutput;
844
845    fn ready_cluster() -> LaneLocalClusterOutput {
846        LaneLocalClusterOutput {
847            start_ready: true,
848            ..LaneLocalClusterOutput::default()
849        }
850    }
851
852    fn ready_top_q() -> SignerTopQ<4, 2> {
853        SignerTopQ::<4, 2> {
854            clusters: [ready_cluster(); SIGNER_CLUSTER_COUNT],
855            frames: GlobalFrameMergeOutput::default(),
856            errors: GlobalErrorMergeOutput::default(),
857            control: SignerTopControl::default(),
858        }
859    }
860
861    #[test]
862    fn admissions_fail_closed_on_every_fault_source_until_shared_reset() {
863        let active_clock = clock_reset(clock(false), reset(false));
864        let reset_clock = clock_reset(clock(false), reset(true));
865        let offered = SignerTopInput {
866            start_valid: true,
867            ..SignerTopInput::default()
868        };
869
870        let (healthy, _) = signer_top_kernel::<4, 2>(active_clock, offered, ready_top_q());
871        assert!(healthy.start_ready);
872        assert!(healthy.start_accepted);
873
874        for fault_source in 0..6 {
875            let mut q = ready_top_q();
876            match fault_source {
877                0 => q.clusters[0].fault = true,
878                1 => q.clusters[1].fault = true,
879                2 => q.clusters[2].fault = true,
880                3 => q.frames.fault = true,
881                4 => q.errors.fault = true,
882                _ => q.control.admission_cursor = b2(3),
883            }
884            let (faulted, d) = signer_top_kernel::<4, 2>(active_clock, offered, q);
885            assert!(faulted.fault);
886            assert!(!faulted.start_ready);
887            assert!(!faulted.start_accepted);
888            assert_eq!(faulted.cluster_start_ready, [false; SIGNER_CLUSTER_COUNT]);
889            assert!(d.control.fault);
890        }
891
892        let mut sticky_q = ready_top_q();
893        sticky_q.control.fault = true;
894        let (sticky, sticky_d) = signer_top_kernel::<4, 2>(active_clock, offered, sticky_q);
895        assert!(sticky.fault);
896        assert!(!sticky.start_ready);
897        assert!(!sticky.start_accepted);
898        assert!(sticky_d.control.fault);
899
900        let (during_reset, reset_d) = signer_top_kernel::<4, 2>(reset_clock, offered, sticky_q);
901        assert!(!during_reset.fault);
902        assert!(!during_reset.start_ready);
903        assert!(!during_reset.start_accepted);
904        assert_eq!(reset_d.control, SignerTopControl::default());
905
906        let (recovered, _) = signer_top_kernel::<4, 2>(active_clock, offered, ready_top_q());
907        assert!(recovered.start_ready);
908        assert!(recovered.start_accepted);
909        assert!(!recovered.fault);
910    }
911
912    #[test]
913    fn error_and_data_ownership_are_independent_at_the_top_boundary() {
914        let mut q = ready_top_q();
915        let data = LaneLocalBeat {
916            data: [b8(0x5a); 64],
917            keep: b64(0xffff_ffff_ffff_ffff),
918            last: false,
919            job_id: b32(0x1234_5678),
920        };
921        q.frames.output_valid = true;
922        q.frames.output = SignerOutputBeat {
923            beat: data,
924            cluster: b2(1),
925        };
926        q.frames.frame_active = true;
927        q.frames.frame_cluster = b2(1);
928        q.frames.cluster_ready = [false, true, false];
929        q.errors.error_valid = true;
930        q.errors.error = SignerErrorCompletion {
931            job_id: b32(0xdead_beef),
932            cluster: b2(0),
933            context: b2(3),
934        };
935        q.errors.cluster_ready = [true, false, false];
936        q.errors.delivered = true;
937
938        let input = SignerTopInput {
939            output_ready: false,
940            error_ready: true,
941            ..SignerTopInput::default()
942        };
943        let (output, d) =
944            signer_top_kernel::<4, 2>(clock_reset(clock(false), reset(false)), input, q);
945        assert!(output.output_valid);
946        assert_eq!(output.output.beat, data);
947        assert_eq!(output.output.cluster, b2(1));
948        assert!(output.error_valid);
949        assert_eq!(output.error.job_id, b32(0xdead_beef));
950        assert_eq!(output.error.cluster, b2(0));
951        assert_eq!(output.error.context, b2(3));
952        assert!(output.delivered_error);
953
954        // Each merge drives only its corresponding cluster handshake and host
955        // ready input; neither channel can consume or rewrite the other.
956        assert!(d.clusters[0].error_ready);
957        assert!(!d.clusters[0].output_ready);
958        assert!(d.clusters[1].output_ready);
959        assert!(!d.clusters[1].error_ready);
960        assert!(!d.clusters[2].output_ready);
961        assert!(!d.clusters[2].error_ready);
962        assert!(!d.frames.output_ready);
963        assert!(d.errors.error_ready);
964    }
965
966    #[test]
967    fn invalid_internal_merge_ownership_never_grants_or_delivers() {
968        let active_clock = clock_reset(clock(false), reset(false));
969        let frame_source = GlobalFrameSource {
970            valid: true,
971            beat: LaneLocalBeat {
972                data: [b8(0x33); 64],
973                keep: b64(0xffff_ffff_ffff_ffff),
974                last: false,
975                job_id: b32(0x1111_2222),
976            },
977        };
978        let frame_input = GlobalFrameMergeInput {
979            clusters: [frame_source; SIGNER_CLUSTER_COUNT],
980            output_ready: true,
981        };
982        for corruption in 0..4 {
983            let mut control = GlobalFrameMergeControl {
984                frame_active: true,
985                output_valid: true,
986                ..GlobalFrameMergeControl::default()
987            };
988            match corruption {
989                0 => control.cursor = b2(3),
990                1 => control.frame_cluster = b2(3),
991                2 => control.expected_beat = b6(35),
992                _ => control.output_cluster = b2(3),
993            }
994            let q = GlobalFrameMergeQ {
995                control,
996                frame_job_id: b32(0x1111_2222),
997                output: frame_source.beat,
998            };
999            let (output, d) = global_frame_merge_kernel(active_clock, frame_input, q);
1000            assert!(!output.output_valid);
1001            assert!(!output.delivered_final);
1002            assert!(!output.captured);
1003            assert_eq!(output.cluster_ready, [false; SIGNER_CLUSTER_COUNT]);
1004            assert!(d.control.fault);
1005            assert!(!d.control.frame_active);
1006        }
1007
1008        let error_input = GlobalErrorMergeInput {
1009            clusters: [GlobalErrorSource {
1010                valid: true,
1011                job_id: b32(0xaaaa_5555),
1012                context: b2(2),
1013            }; SIGNER_CLUSTER_COUNT],
1014            error_ready: true,
1015        };
1016        for invalid_output_owner in [false, true] {
1017            let q = GlobalErrorMergeQ {
1018                control: GlobalErrorMergeControl {
1019                    cursor: if invalid_output_owner { b2(0) } else { b2(3) },
1020                    output_valid: invalid_output_owner,
1021                    output_cluster: if invalid_output_owner { b2(3) } else { b2(0) },
1022                    ..GlobalErrorMergeControl::default()
1023                },
1024                job_id: b32(0xaaaa_5555),
1025            };
1026            let (output, d) = global_error_merge_kernel(active_clock, error_input, q);
1027            assert!(!output.error_valid);
1028            assert!(!output.delivered);
1029            assert!(!output.captured);
1030            assert_eq!(output.cluster_ready, [false; SIGNER_CLUSTER_COUNT]);
1031            assert!(d.control.fault);
1032            assert!(!d.control.output_valid);
1033        }
1034    }
1035}