Skip to main content

wots_rhdl/verify/
top.rs

1//! Three-cluster SHA-256 verifier top with global fail-closed drainage.
2//!
3//! One input stream is locked to a selected cluster for exactly 35 accepted
4//! beats. Beat zero strictly targets the registered three-way rotating cursor;
5//! a busy target causes bounded head-of-line stall even when another cluster is
6//! ready. The cursor advances only after accepted beat zero. The fixed
7//! accepted-beat count, never untrusted `last`, releases ownership. The selected
8//! cluster, physical context, and generation form the complete internal job
9//! identity.
10//! The selected child sees a standard ready/valid offer independent of its
11//! ready response; top and child frame state advance only on their identical
12//! accepted event. Field-level stream muxes keep unrelated cluster status out
13//! of that handshake's structural dependency graph. Each cluster input is also
14//! a direct literal with independent one-hot stream-valid and result-ready
15//! fields; no dynamic aggregate mutation joins those protocols during lowering.
16//!
17//! Results cross a one-entry registered fair merge. The retained payload is
18//! stable under stalls except that the first global fatal edge converts a clean
19//! completion to an identified transport error and suppresses its transfer.
20//! Any local cluster fault latches global poison. Peer abort is driven only from
21//! that registered poison bit, so a child output can never feed a combinational
22//! fault-to-abort-to-fault loop.
23//!
24//! A current cluster fault is a containment-edge exception to ordinary
25//! ready/fault intuition. It may depend on a child response observed on this
26//! edge, so top admission cannot retract already-advertised credit through that
27//! path. One beat may therefore transfer while `fault` rises. The selected
28//! cluster receives exactly the reported accepted beat and retains its job
29//! identity; registered poison removes credit on the next cycle. Result
30//! transfer and capture remain suppressed on the original fault edge.
31//!
32//! The first broadcast-abort edge is transition-only inside each still-clean
33//! peer: its output remains independent of `external_abort`, while poison,
34//! frame-clear, and diagnostic D state are recorded. The poisoned top presents
35//! neither a stream beat nor a child-result grant on that edge. At most one lane
36//! launch, one routed return, and one internal result capture belonging to
37//! already-accepted work may advance per peer. A separate registered
38//! `drain_armed` phase waits for peer poison to contain those effects before the
39//! top selects any poisoned child. Once armed, selection depends only on a
40//! child's registered `result_valid`, never on payload data that could feed its
41//! release wire. Capture defensively converts any unexpected clean payload to
42//! an identified transport error and preserves every existing nonzero error.
43//! Every captured child is released on the same edge, and the top's registered
44//! slot may replace a transferring completion.
45//!
46//! This module provides source/RHDL behavioral structure only. It makes no
47//! emitted-RTL, synthesis, resource, timing, route, throughput, or card claim.
48
49use rhdl::{core::ReplicatedSynchronous, prelude::*};
50use rhdl_fpga::core::dff::DFF;
51use rhdl_primitives::NoResetDff;
52use sha256_rhdl::lane::{ExternalFullCompressionLane, InlineCompressionLane};
53
54use crate::{
55    cluster::CompressionLaneComponent,
56    verify::{
57        cluster::{VerifierCluster, VerifyClusterInput, VerifyResult},
58        framing::VerifyStreamBeat,
59        sha::{VERIFY_ERROR_NONE, VERIFY_ERROR_TRANSPORT},
60    },
61};
62
63/// Number of independent lane-local verifier clusters.
64pub const VERIFY_TOP_CLUSTERS: usize = 3;
65/// Number of physical verifier contexts across all clusters.
66pub const VERIFY_TOP_CONTEXTS: usize = VERIFY_TOP_CLUSTERS * 4;
67
68/// Cluster zero caused the first global poison edge.
69pub const VERIFY_TOP_DIAGNOSTIC_CLUSTER_0: u128 = 1 << 0;
70/// Cluster one caused the first global poison edge.
71pub const VERIFY_TOP_DIAGNOSTIC_CLUSTER_1: u128 = 1 << 1;
72/// Cluster two caused the first global poison edge.
73pub const VERIFY_TOP_DIAGNOSTIC_CLUSTER_2: u128 = 1 << 2;
74/// Registered top/child frame ownership disagreed.
75pub const VERIFY_TOP_DIAGNOSTIC_OWNERSHIP: u128 = 1 << 3;
76/// Admission cursor was outside `0..=2`.
77pub const VERIFY_TOP_DIAGNOSTIC_ADMISSION_CURSOR: u128 = 1 << 4;
78/// Result cursor was outside `0..=2`.
79pub const VERIFY_TOP_DIAGNOSTIC_RESULT_CURSOR: u128 = 1 << 5;
80
81const _: () = {
82    assert!(VERIFY_TOP_CLUSTERS == 3);
83    assert!(VERIFY_TOP_CONTEXTS == 12);
84};
85
86/// External verifier input and result-consumer handshake.
87#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
88pub struct VerifierTopInput {
89    /// One 512-bit verifier input beat is present.
90    pub stream_valid: bool,
91    /// Signature, public-key material, message, and sideband.
92    pub stream: VerifyStreamBeat,
93    /// Consumer accepts the registered top result.
94    pub result_ready: bool,
95}
96
97/// Three-way rotating selection result.
98#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
99pub struct VerifierTopSelection {
100    /// At least one eligible cluster exists.
101    pub valid: bool,
102    /// Selected cluster `0..=2`.
103    pub cluster: b2,
104    /// Cursor following the selected cluster.
105    pub next_cursor: b2,
106}
107
108/// Narrow resettable frame, merge, and poison state.
109#[allow(clippy::struct_excessive_bools)] // Independent protocol phases.
110#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
111pub struct VerifierTopControl {
112    /// Rotating beat-zero admission cursor.
113    pub admission_cursor: b2,
114    /// One cluster owns the current 35-beat input frame.
115    pub frame_active: bool,
116    /// Cluster owning the current frame.
117    pub frame_cluster: b2,
118    /// Physical context assigned on accepted beat zero.
119    pub frame_context: b2,
120    /// Generation assigned on accepted beat zero.
121    pub frame_generation: b8,
122    /// Next fixed beat index expected by the owner.
123    pub frame_beat: b6,
124    /// Rotating cluster-result cursor.
125    pub result_cursor: b2,
126    /// One registered result occupies the top slot.
127    pub result_valid: bool,
128    /// Source cluster of the retained result.
129    pub result_cluster: b2,
130    /// Sticky global poison.
131    pub poison: bool,
132    /// A complete registered peer-abort edge has elapsed.
133    pub drain_armed: bool,
134    /// Sticky first-cause top diagnostics.
135    pub diagnostics: b8,
136    /// Cluster-fault mask sampled before peer abort changes every child.
137    pub origin_cluster_fault: b3,
138    /// Per-cluster diagnostic snapshots from the first fatal edge.
139    pub origin_cluster_diagnostics: [b16; VERIFY_TOP_CLUSTERS],
140}
141
142/// Global verifier status, accepted identity, and registered result.
143///
144/// `result` and `result_cluster` are meaningful only while `result_valid` is
145/// true. They remain stable while the result is stalled, except that the first
146/// fatal edge may fail-closed convert a retained clean result into an identified
147/// transport error while suppressing transfer. When `result_valid` is false,
148/// the stored payload fields may be stale or unspecified and must not be
149/// consumed.
150#[allow(clippy::struct_excessive_bools)] // Independent host and audit wires.
151#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
152pub struct VerifierTopOutput {
153    /// The presented input beat may transfer.
154    pub stream_ready: bool,
155    /// One input beat transferred this cycle.
156    pub stream_accepted: bool,
157    /// Cluster owning or selected for the transfer.
158    pub stream_cluster: b2,
159    /// Physical context owning or selected for the transfer.
160    pub stream_context: b2,
161    /// Generation owning or selected for the transfer.
162    pub stream_generation: b8,
163    /// A fixed-count global input frame is active.
164    pub frame_active: bool,
165    /// Current global frame owner.
166    pub frame_cluster: b2,
167    /// Next fixed beat index for the current frame.
168    pub frame_beat: b6,
169    /// Beat-zero job identity retained for ownership diagnostics.
170    pub frame_job_id: b32,
171    /// Per-cluster input readiness before global selection.
172    pub cluster_stream_ready: [bool; VERIFY_TOP_CLUSTERS],
173    /// Per-cluster frame ownership state.
174    pub cluster_frame_active: [bool; VERIFY_TOP_CLUSTERS],
175    /// Per-cluster physical loading state.
176    pub cluster_context_loading: [[bool; 4]; VERIFY_TOP_CLUSTERS],
177    /// Per-cluster physical task-graph activity.
178    pub cluster_context_active: [[bool; 4]; VERIFY_TOP_CLUSTERS],
179    /// Per-cluster retained child results.
180    pub cluster_context_result_valid: [[bool; 4]; VERIFY_TOP_CLUSTERS],
181    /// Immediate or sticky per-cluster faults.
182    pub cluster_fault: [bool; VERIFY_TOP_CLUSTERS],
183    /// Per-cluster sticky diagnostics.
184    pub cluster_diagnostics: [b16; VERIFY_TOP_CLUSTERS],
185    /// One registered top result is available and its payload fields are
186    /// meaningful.
187    pub result_valid: bool,
188    /// Registered completion.
189    ///
190    /// Meaningful only when `result_valid`. Stable while stalled except for the
191    /// documented fail-closed conversion on the first fatal edge.
192    pub result: VerifyResult,
193    /// Source cluster retained beside `result`, meaningful only when
194    /// `result_valid`.
195    pub result_cluster: b2,
196    /// The registered top result transferred this cycle.
197    pub result_transfer: bool,
198    /// Immediate or sticky global poison.
199    pub fault: bool,
200    /// A complete registered peer-abort edge has elapsed.
201    pub drain_armed: bool,
202    /// Sticky top-level cause mask.
203    pub diagnostics: b8,
204    /// First-edge cluster-fault snapshot.
205    pub origin_cluster_fault: b3,
206    /// First-edge per-cluster diagnostic snapshot.
207    pub origin_cluster_diagnostics: [b16; VERIFY_TOP_CLUSTERS],
208}
209
210/// Select one of three eligible clusters with rotating fairness.
211#[kernel]
212#[allow(clippy::assign_op_pattern)] // Compound assignment is not lowered by pinned RHDL.
213pub fn select_verifier_top_cluster_kernel(
214    eligible: [bool; VERIFY_TOP_CLUSTERS],
215    cursor: b2,
216) -> VerifierTopSelection {
217    let normalized_cursor = if cursor < b2(3) { cursor } else { b2(0) };
218    let mut selection = VerifierTopSelection {
219        next_cursor: normalized_cursor,
220        ..VerifierTopSelection::default()
221    };
222    let mut scan = normalized_cursor;
223    for _offset in 0..VERIFY_TOP_CLUSTERS {
224        if !selection.valid && eligible[scan] {
225            selection.valid = true;
226            selection.cluster = scan;
227            selection.next_cursor = if scan == b2(2) { b2(0) } else { scan + b2(1) };
228        }
229        scan = if scan == b2(2) { b2(0) } else { scan + b2(1) };
230    }
231    selection
232}
233
234/// Three replicated four-context verifier clusters and narrow global control.
235#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
236pub struct VerifierTop<L = InlineCompressionLane<64, 6>>
237where
238    L: CompressionLaneComponent,
239{
240    clusters: ReplicatedSynchronous<VerifierCluster<L>, VERIFY_TOP_CLUSTERS>,
241    control: DFF<VerifierTopControl>,
242    frame_job_id: NoResetDff<b32>,
243    result: NoResetDff<VerifyResult>,
244}
245
246impl<L> Default for VerifierTop<L>
247where
248    L: CompressionLaneComponent + Default,
249{
250    fn default() -> Self {
251        Self {
252            clusters: ReplicatedSynchronous::new(VerifierCluster::default()),
253            control: DFF::new(VerifierTopControl::default()),
254            frame_job_id: NoResetDff::new(),
255            result: NoResetDff::new(),
256        }
257    }
258}
259
260impl<L> SynchronousIO for VerifierTop<L>
261where
262    L: CompressionLaneComponent,
263{
264    type I = VerifierTopInput;
265    type O = VerifierTopOutput;
266    type Kernel = verifier_top_with_lane_kernel<L>;
267}
268
269/// Global frame ownership, result merge, and registered peer-abort broadcast.
270#[kernel]
271#[allow(clippy::assign_op_pattern, clippy::needless_range_loop)] // Pinned RHDL lowering subset.
272pub fn verifier_top_with_lane_kernel<L: CompressionLaneComponent>(
273    clock_reset: ClockReset,
274    input: VerifierTopInput,
275    q: VerifierTopQ<L>,
276) -> (VerifierTopOutput, VerifierTopD<L>) {
277    let resetting = clock_reset.reset.any();
278    let cluster_ready = [
279        q.clusters[0].stream_ready,
280        q.clusters[1].stream_ready,
281        q.clusters[2].stream_ready,
282    ];
283    let cluster_frames = [
284        q.clusters[0].frame_active,
285        q.clusters[1].frame_active,
286        q.clusters[2].frame_active,
287    ];
288    let cluster_fault = [
289        q.clusters[0].fault,
290        q.clusters[1].fault,
291        q.clusters[2].fault,
292    ];
293    let cluster_diagnostics = [
294        q.clusters[0].diagnostics,
295        q.clusters[1].diagnostics,
296        q.clusters[2].diagnostics,
297    ];
298
299    let owner_consistent = match q.control.frame_cluster {
300        Bits::<2>(0) => {
301            cluster_frames[0]
302                && !cluster_frames[1]
303                && !cluster_frames[2]
304                && q.clusters[0].stream_context == q.control.frame_context
305                && q.clusters[0].stream_generation == q.control.frame_generation
306                && q.clusters[0].frame_beat == q.control.frame_beat
307                && q.clusters[0].frame_job_id == q.frame_job_id
308        }
309        Bits::<2>(1) => {
310            !cluster_frames[0]
311                && cluster_frames[1]
312                && !cluster_frames[2]
313                && q.clusters[1].stream_context == q.control.frame_context
314                && q.clusters[1].stream_generation == q.control.frame_generation
315                && q.clusters[1].frame_beat == q.control.frame_beat
316                && q.clusters[1].frame_job_id == q.frame_job_id
317        }
318        Bits::<2>(2) => {
319            !cluster_frames[0]
320                && !cluster_frames[1]
321                && cluster_frames[2]
322                && q.clusters[2].stream_context == q.control.frame_context
323                && q.clusters[2].stream_generation == q.control.frame_generation
324                && q.clusters[2].frame_beat == q.control.frame_beat
325                && q.clusters[2].frame_job_id == q.frame_job_id
326        }
327        _ => false,
328    };
329    let any_cluster_frame = cluster_frames[0] || cluster_frames[1] || cluster_frames[2];
330    let ownership_fault = !q.control.poison
331        && ((q.control.frame_active && !owner_consistent)
332            || (!q.control.frame_active && any_cluster_frame));
333    let admission_cursor_fault = q.control.admission_cursor >= b2(3);
334    let result_cursor_fault = q.control.result_cursor >= b2(3);
335    let observed_cluster_fault = cluster_fault[0] || cluster_fault[1] || cluster_fault[2];
336
337    let mut diagnostic_now = b8(0);
338    let mut origin_cluster_fault = b3(0);
339    if cluster_fault[0] {
340        diagnostic_now = diagnostic_now | b8(VERIFY_TOP_DIAGNOSTIC_CLUSTER_0);
341        origin_cluster_fault = origin_cluster_fault | b3(1);
342    }
343    if cluster_fault[1] {
344        diagnostic_now = diagnostic_now | b8(VERIFY_TOP_DIAGNOSTIC_CLUSTER_1);
345        origin_cluster_fault = origin_cluster_fault | b3(2);
346    }
347    if cluster_fault[2] {
348        diagnostic_now = diagnostic_now | b8(VERIFY_TOP_DIAGNOSTIC_CLUSTER_2);
349        origin_cluster_fault = origin_cluster_fault | b3(4);
350    }
351    if ownership_fault {
352        diagnostic_now = diagnostic_now | b8(VERIFY_TOP_DIAGNOSTIC_OWNERSHIP);
353    }
354    if admission_cursor_fault {
355        diagnostic_now = diagnostic_now | b8(VERIFY_TOP_DIAGNOSTIC_ADMISSION_CURSOR);
356    }
357    if result_cursor_fault {
358        diagnostic_now = diagnostic_now | b8(VERIFY_TOP_DIAGNOSTIC_RESULT_CURSOR);
359    }
360    let fatal_now = !resetting
361        && !q.control.poison
362        && (observed_cluster_fault
363            || ownership_fault
364            || admission_cursor_fault
365            || result_cursor_fault);
366    let fault = q.control.poison || fatal_now;
367    let admission_fatal_now = !resetting
368        && !q.control.poison
369        && (ownership_fault || admission_cursor_fault || result_cursor_fault);
370
371    // Beat-zero targeting is strict round-robin from registered state. Child
372    // ready affects only acceptance, never which child sees valid. This permits
373    // bounded head-of-line stalls but removes every ready -> selector -> valid
374    // dependency from the hierarchy.
375    let admission_target = if q.control.admission_cursor < b2(3) {
376        q.control.admission_cursor
377    } else {
378        b2(0)
379    };
380    let selected_cluster = if q.control.frame_active {
381        q.control.frame_cluster
382    } else {
383        admission_target
384    };
385    // Mux only the scalar stream fields consumed below. Selecting the complete
386    // cluster aggregate would make unrelated fault/result/response fields
387    // structural dependencies of stream credit during RHDL lowering.
388    let selected_stream_ready = match selected_cluster {
389        Bits::<2>(0) => q.clusters[0].stream_ready,
390        Bits::<2>(1) => q.clusters[1].stream_ready,
391        _ => q.clusters[2].stream_ready,
392    };
393    let selected_stream_context = match selected_cluster {
394        Bits::<2>(0) => q.clusters[0].stream_context,
395        Bits::<2>(1) => q.clusters[1].stream_context,
396        _ => q.clusters[2].stream_context,
397    };
398    let selected_stream_generation = match selected_cluster {
399        Bits::<2>(0) => q.clusters[0].stream_generation,
400        Bits::<2>(1) => q.clusters[1].stream_generation,
401        _ => q.clusters[2].stream_generation,
402    };
403    // A current cluster fault may depend on the response that this hierarchy is
404    // simultaneously presenting. Honor the credit already visible on this
405    // edge; top-local structural faults remain safe immediate admission gates.
406    let stream_ready =
407        !resetting && !q.control.poison && !admission_fatal_now && selected_stream_ready;
408    // Present valid independently of the selected child's ready/accepted
409    // response. This is the ordinary ready/valid contract and cuts the
410    // child-ready -> top-accepted -> child-valid hierarchy fixed point. Beat
411    // zero uses only the registered strict-round-robin target; an active frame
412    // uses its registered owner. Both therefore hold their offer through child
413    // stalls without observing any ready signal.
414    let stream_offer =
415        !resetting && !q.control.poison && !admission_fatal_now && input.stream_valid;
416    let stream_accepted = stream_offer && selected_stream_ready;
417
418    let result_transfer = !resetting && !fatal_now && q.control.result_valid && input.result_ready;
419    let result_slot_available = !q.control.result_valid || result_transfer;
420    let poisoned_result_eligible = q.control.poison && q.control.drain_armed;
421    let result_eligible = [
422        q.clusters[0].result_valid && (!q.control.poison || poisoned_result_eligible),
423        q.clusters[1].result_valid && (!q.control.poison || poisoned_result_eligible),
424        q.clusters[2].result_valid && (!q.control.poison || poisoned_result_eligible),
425    ];
426    let result_selection =
427        select_verifier_top_cluster_kernel(result_eligible, q.control.result_cursor);
428    let capture_result =
429        !resetting && !fatal_now && result_slot_available && result_selection.valid;
430
431    let mut control = q.control;
432    let mut frame_job_id = q.frame_job_id;
433    let mut result = q.result;
434    let mut visible_result = q.result;
435    let abort_clean_result = !resetting
436        && (fatal_now || q.control.poison)
437        && q.control.result_valid
438        && q.result.error == b3(VERIFY_ERROR_NONE);
439    if abort_clean_result {
440        visible_result = VerifyResult {
441            job_id: q.result.job_id,
442            verified: false,
443            error: b3(VERIFY_ERROR_TRANSPORT),
444        };
445        result = visible_result;
446    }
447
448    if result_transfer {
449        control.result_valid = false;
450    }
451    if capture_result {
452        let child = match result_selection.cluster {
453            Bits::<2>(0) => q.clusters[0].result,
454            Bits::<2>(1) => q.clusters[1].result,
455            _ => q.clusters[2].result,
456        };
457        let captured_error = if q.control.poison && child.error == b3(VERIFY_ERROR_NONE) {
458            b3(VERIFY_ERROR_TRANSPORT)
459        } else {
460            child.error
461        };
462        result = VerifyResult {
463            job_id: child.job_id,
464            verified: captured_error == b3(VERIFY_ERROR_NONE) && child.verified,
465            error: captured_error,
466        };
467        control.result_valid = true;
468        control.result_cluster = result_selection.cluster;
469        control.result_cursor = result_selection.next_cursor;
470    }
471
472    if stream_accepted {
473        if q.control.frame_active {
474            if q.control.frame_beat == b6(34) {
475                control.frame_active = false;
476                control.frame_beat = b6(0);
477            } else {
478                control.frame_beat = q.control.frame_beat + b6(1);
479            }
480        } else {
481            control.frame_active = true;
482            control.frame_cluster = admission_target;
483            control.frame_context = selected_stream_context;
484            control.frame_generation = selected_stream_generation;
485            control.frame_beat = b6(1);
486            control.admission_cursor = if admission_target == b2(2) {
487                b2(0)
488            } else {
489                admission_target + b2(1)
490            };
491            frame_job_id = input.stream.job_id;
492        }
493    }
494
495    if q.control.poison && !q.control.drain_armed {
496        control.drain_armed = true;
497    }
498    if fatal_now {
499        control.poison = true;
500        control.drain_armed = false;
501        control.frame_active = false;
502        control.frame_beat = b6(0);
503        control.diagnostics = q.control.diagnostics | diagnostic_now;
504        control.origin_cluster_fault = origin_cluster_fault;
505        control.origin_cluster_diagnostics = cluster_diagnostics;
506    }
507    if resetting {
508        control = VerifierTopControl::default();
509    }
510
511    // Construct every child field directly. Dynamic mutation of one field in a
512    // shared aggregate makes RHDL conservatively couple sibling fields; that
513    // previously let fault-dependent result grant logic contaminate stream
514    // valid. These scalar one-hot expressions keep the independent protocols
515    // structurally separate as well as behaviorally separate.
516    let cluster_external_abort = !resetting && q.control.poison;
517    let cluster_0_stream_valid = stream_offer && selected_cluster == b2(0);
518    let cluster_1_stream_valid = stream_offer && selected_cluster == b2(1);
519    let cluster_2_stream_valid = stream_offer && selected_cluster == b2(2);
520    let cluster_0_result_ready = capture_result && result_selection.cluster == b2(0);
521    let cluster_1_result_ready = capture_result && result_selection.cluster == b2(1);
522    let cluster_2_result_ready = capture_result && result_selection.cluster == b2(2);
523    let cluster_inputs = [
524        VerifyClusterInput {
525            stream_valid: cluster_0_stream_valid,
526            stream: input.stream,
527            result_ready: cluster_0_result_ready,
528            external_abort: cluster_external_abort,
529        },
530        VerifyClusterInput {
531            stream_valid: cluster_1_stream_valid,
532            stream: input.stream,
533            result_ready: cluster_1_result_ready,
534            external_abort: cluster_external_abort,
535        },
536        VerifyClusterInput {
537            stream_valid: cluster_2_stream_valid,
538            stream: input.stream,
539            result_ready: cluster_2_result_ready,
540            external_abort: cluster_external_abort,
541        },
542    ];
543
544    let output = VerifierTopOutput {
545        stream_ready,
546        stream_accepted,
547        stream_cluster: selected_cluster,
548        stream_context: selected_stream_context,
549        stream_generation: selected_stream_generation,
550        frame_active: !resetting && q.control.frame_active,
551        frame_cluster: q.control.frame_cluster,
552        frame_beat: q.control.frame_beat,
553        frame_job_id: q.frame_job_id,
554        cluster_stream_ready: if resetting || q.control.poison || admission_fatal_now {
555            [false; VERIFY_TOP_CLUSTERS]
556        } else {
557            cluster_ready
558        },
559        cluster_frame_active: if resetting {
560            [false; VERIFY_TOP_CLUSTERS]
561        } else {
562            cluster_frames
563        },
564        cluster_context_loading: if resetting {
565            [[false; 4]; VERIFY_TOP_CLUSTERS]
566        } else {
567            [
568                q.clusters[0].context_loading,
569                q.clusters[1].context_loading,
570                q.clusters[2].context_loading,
571            ]
572        },
573        cluster_context_active: if resetting {
574            [[false; 4]; VERIFY_TOP_CLUSTERS]
575        } else {
576            [
577                q.clusters[0].context_active,
578                q.clusters[1].context_active,
579                q.clusters[2].context_active,
580            ]
581        },
582        cluster_context_result_valid: if resetting {
583            [[false; 4]; VERIFY_TOP_CLUSTERS]
584        } else {
585            [
586                q.clusters[0].context_result_valid,
587                q.clusters[1].context_result_valid,
588                q.clusters[2].context_result_valid,
589            ]
590        },
591        cluster_fault: if resetting {
592            [false; VERIFY_TOP_CLUSTERS]
593        } else {
594            cluster_fault
595        },
596        cluster_diagnostics: if resetting {
597            [b16(0); VERIFY_TOP_CLUSTERS]
598        } else {
599            cluster_diagnostics
600        },
601        result_valid: !resetting && q.control.result_valid,
602        result: visible_result,
603        result_cluster: q.control.result_cluster,
604        result_transfer,
605        fault: !resetting && fault,
606        drain_armed: !resetting && q.control.drain_armed,
607        diagnostics: if resetting {
608            b8(0)
609        } else if fatal_now {
610            q.control.diagnostics | diagnostic_now
611        } else {
612            q.control.diagnostics
613        },
614        origin_cluster_fault: if resetting {
615            b3(0)
616        } else if fatal_now {
617            origin_cluster_fault
618        } else {
619            q.control.origin_cluster_fault
620        },
621        origin_cluster_diagnostics: if resetting {
622            [b16(0); VERIFY_TOP_CLUSTERS]
623        } else if fatal_now {
624            cluster_diagnostics
625        } else {
626            q.control.origin_cluster_diagnostics
627        },
628    };
629    let d = VerifierTopD::<L> {
630        clusters: cluster_inputs,
631        control,
632        frame_job_id,
633        result,
634    };
635    (output, d)
636}
637
638/// Production three-cluster verifier with three inline full SHA-256 lanes.
639pub type Sha256VerifierTop = VerifierTop<InlineCompressionLane<64, 6>>;
640
641/// Production verifier skeleton linked with one separately generated lane type.
642///
643/// Only the complete ordinary [`VerifierCluster`] is replicated. Its direct
644/// external lane child remains unresolved until the separately generated RHDL
645/// lane artifact is supplied to an external parser.
646pub type ModularSha256VerifierTop = VerifierTop<ExternalFullCompressionLane>;
647
648/// Native-lane wrapper retained as a stable free-kernel entry point.
649#[kernel]
650pub fn sha256_verifier_top_kernel(
651    clock_reset: ClockReset,
652    input: VerifierTopInput,
653    q: VerifierTopQ<InlineCompressionLane<64, 6>>,
654) -> (
655    VerifierTopOutput,
656    VerifierTopD<InlineCompressionLane<64, 6>>,
657) {
658    verifier_top_with_lane_kernel::<InlineCompressionLane<64, 6>>(clock_reset, input, q)
659}
660
661impl<L> VerifierTop<L>
662where
663    L: CompressionLaneComponent,
664{
665    /// Reads per-context launch handshakes from software-simulation state.
666    ///
667    /// This host-only helper is not part of [`VerifierTopInput`],
668    /// [`VerifierTopOutput`], or the generated hardware interface.
669    #[doc(hidden)]
670    #[must_use]
671    pub fn simulation_request_accepted(
672        state: &<Self as Synchronous>::S,
673    ) -> [[bool; 4]; VERIFY_TOP_CLUSTERS] {
674        [
675            state.0.clusters[0].request_accepted,
676            state.0.clusters[1].request_accepted,
677            state.0.clusters[2].request_accepted,
678        ]
679    }
680
681    /// Reads per-context accepted-return pulses from software-simulation state.
682    ///
683    /// This host-only helper exists for exact finite source-equivalence gates;
684    /// it adds no production datapath or hardware port.
685    #[doc(hidden)]
686    #[must_use]
687    pub fn simulation_response_routed(
688        state: &<Self as Synchronous>::S,
689    ) -> [[bool; 4]; VERIFY_TOP_CLUSTERS] {
690        [
691            state.0.clusters[0].response_routed,
692            state.0.clusters[1].response_routed,
693            state.0.clusters[2].response_routed,
694        ]
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701    use crate::verify::{cluster::VerifyClusterOutput, sha::VERIFY_ERROR_FRAME};
702
703    type NativeLane = InlineCompressionLane<64, 6>;
704
705    fn fatal_collision_q(result: VerifyResult) -> VerifierTopQ<NativeLane> {
706        let mut clusters = [VerifyClusterOutput::default(); VERIFY_TOP_CLUSTERS];
707        clusters[0].fault = true;
708        clusters[0].diagnostics = b16(0x0004);
709        clusters[1].result_valid = true;
710        clusters[1].result = VerifyResult {
711            job_id: b32(0x2222_0002),
712            verified: true,
713            error: b3(VERIFY_ERROR_NONE),
714        };
715        VerifierTopQ::<NativeLane> {
716            clusters,
717            control: VerifierTopControl {
718                result_valid: true,
719                result_cluster: b2(2),
720                ..VerifierTopControl::default()
721            },
722            frame_job_id: b32(0),
723            result,
724        }
725    }
726
727    #[test]
728    fn fatal_edge_converts_clean_top_slot_and_suppresses_transfer_and_child_capture() {
729        let retained = VerifyResult {
730            job_id: b32(0x1111_0001),
731            verified: true,
732            error: b3(VERIFY_ERROR_NONE),
733        };
734        let (output, d) = verifier_top_with_lane_kernel::<NativeLane>(
735            clock_reset(clock(false), reset(false)),
736            VerifierTopInput {
737                result_ready: true,
738                ..VerifierTopInput::default()
739            },
740            fatal_collision_q(retained),
741        );
742
743        assert!(output.fault);
744        assert!(output.result_valid);
745        assert!(!output.result_transfer);
746        assert_eq!(output.result.job_id, retained.job_id);
747        assert!(!output.result.verified);
748        assert_eq!(output.result.error, b3(VERIFY_ERROR_TRANSPORT));
749        assert_eq!(output.result_cluster, b2(2));
750        assert!(d.control.result_valid);
751        assert_eq!(d.result, output.result);
752        assert!(!d.clusters[0].result_ready);
753        assert!(!d.clusters[1].result_ready);
754        assert!(!d.clusters[2].result_ready);
755    }
756
757    #[test]
758    fn fatal_edge_preserves_retained_nonzero_error() {
759        let retained = VerifyResult {
760            job_id: b32(0x3333_0003),
761            verified: false,
762            error: b3(VERIFY_ERROR_FRAME),
763        };
764        let (output, d) = verifier_top_with_lane_kernel::<NativeLane>(
765            clock_reset(clock(false), reset(false)),
766            VerifierTopInput {
767                result_ready: true,
768                ..VerifierTopInput::default()
769            },
770            fatal_collision_q(retained),
771        );
772
773        assert!(output.fault);
774        assert!(output.result_valid);
775        assert!(!output.result_transfer);
776        assert_eq!(output.result, retained);
777        assert_eq!(d.result, retained);
778        assert!(d.control.result_valid);
779        assert!(!d.clusters[1].result_ready);
780    }
781
782    #[test]
783    fn beat_zero_offer_targets_registered_cursor_even_when_another_cluster_is_ready() {
784        let stream = VerifyStreamBeat {
785            job_id: b32(0x3030_0003),
786            ..VerifyStreamBeat::default()
787        };
788        let mut clusters = [VerifyClusterOutput::default(); VERIFY_TOP_CLUSTERS];
789        clusters[0].stream_ready = true;
790        clusters[1].stream_context = b2(3);
791        clusters[1].stream_generation = b8(9);
792        clusters[2].stream_ready = true;
793        let q = VerifierTopQ::<NativeLane> {
794            clusters,
795            control: VerifierTopControl {
796                admission_cursor: b2(1),
797                ..VerifierTopControl::default()
798            },
799            frame_job_id: b32(0),
800            result: VerifyResult::default(),
801        };
802        let input = VerifierTopInput {
803            stream_valid: true,
804            stream,
805            result_ready: false,
806        };
807
808        let (stalled, stalled_d) = verifier_top_with_lane_kernel::<NativeLane>(
809            clock_reset(clock(false), reset(false)),
810            input,
811            q,
812        );
813        assert_eq!(stalled.stream_cluster, b2(1));
814        assert!(!stalled.stream_ready);
815        assert!(!stalled.stream_accepted);
816        assert!(stalled_d.clusters[1].stream_valid);
817        assert_eq!(stalled_d.clusters[1].stream, stream);
818        assert!(!stalled_d.clusters[0].stream_valid);
819        assert!(!stalled_d.clusters[2].stream_valid);
820        assert!(!stalled_d.control.frame_active);
821        assert_eq!(stalled_d.control.admission_cursor, b2(1));
822
823        let mut ready_q = q;
824        ready_q.clusters[1].stream_ready = true;
825        let (accepted, accepted_d) = verifier_top_with_lane_kernel::<NativeLane>(
826            clock_reset(clock(false), reset(false)),
827            input,
828            ready_q,
829        );
830        assert_eq!(accepted.stream_cluster, b2(1));
831        assert!(accepted.stream_ready);
832        assert!(accepted.stream_accepted);
833        assert!(accepted_d.clusters[1].stream_valid);
834        assert_eq!(accepted_d.clusters[1].stream, stream);
835        assert!(accepted_d.control.frame_active);
836        assert_eq!(accepted_d.control.frame_cluster, b2(1));
837        assert_eq!(accepted_d.control.frame_context, b2(3));
838        assert_eq!(accepted_d.control.frame_generation, b8(9));
839        assert_eq!(accepted_d.control.frame_beat, b6(1));
840        assert_eq!(accepted_d.control.admission_cursor, b2(2));
841        assert_eq!(accepted_d.frame_job_id, stream.job_id);
842    }
843
844    #[test]
845    fn selected_child_offer_is_ready_independent_and_acceptance_matches() {
846        let stream = VerifyStreamBeat {
847            job_id: b32(0x4040_0004),
848            ..VerifyStreamBeat::default()
849        };
850        let mut clusters = [VerifyClusterOutput::default(); VERIFY_TOP_CLUSTERS];
851        clusters[1].frame_active = true;
852        clusters[1].frame_beat = b6(5);
853        clusters[1].frame_job_id = stream.job_id;
854        clusters[1].stream_context = b2(2);
855        clusters[1].stream_generation = b8(7);
856        let q = VerifierTopQ::<NativeLane> {
857            clusters,
858            control: VerifierTopControl {
859                frame_active: true,
860                frame_cluster: b2(1),
861                frame_context: b2(2),
862                frame_generation: b8(7),
863                frame_beat: b6(5),
864                ..VerifierTopControl::default()
865            },
866            frame_job_id: stream.job_id,
867            result: VerifyResult::default(),
868        };
869        let input = VerifierTopInput {
870            stream_valid: true,
871            stream,
872            result_ready: false,
873        };
874
875        let (stalled, stalled_d) = verifier_top_with_lane_kernel::<NativeLane>(
876            clock_reset(clock(false), reset(false)),
877            input,
878            q,
879        );
880        assert!(!stalled.stream_ready);
881        assert!(!stalled.stream_accepted);
882        assert_eq!(stalled.stream_cluster, b2(1));
883        assert_eq!(stalled.stream_context, b2(2));
884        assert_eq!(stalled.stream_generation, b8(7));
885        assert!(stalled_d.clusters[1].stream_valid);
886        assert_eq!(stalled_d.clusters[1].stream, stream);
887        assert!(!stalled_d.clusters[0].stream_valid);
888        assert!(!stalled_d.clusters[2].stream_valid);
889        assert_eq!(
890            stalled.stream_accepted,
891            q.clusters[1].stream_ready && stalled_d.clusters[1].stream_valid
892        );
893        assert_eq!(stalled_d.control.frame_beat, b6(5));
894
895        let mut ready_q = q;
896        ready_q.clusters[1].stream_ready = true;
897        let (accepted, accepted_d) = verifier_top_with_lane_kernel::<NativeLane>(
898            clock_reset(clock(false), reset(false)),
899            input,
900            ready_q,
901        );
902        assert!(accepted.stream_ready);
903        assert!(accepted.stream_accepted);
904        assert!(accepted_d.clusters[1].stream_valid);
905        assert_eq!(accepted_d.clusters[1].stream, stream);
906        assert_eq!(
907            accepted.stream_accepted,
908            ready_q.clusters[1].stream_ready && accepted_d.clusters[1].stream_valid
909        );
910        assert_eq!(accepted_d.control.frame_beat, b6(6));
911    }
912
913    #[test]
914    fn stream_offer_and_result_capture_coexist_on_independent_one_hot_fields() {
915        let stream = VerifyStreamBeat {
916            job_id: b32(0x4141_0004),
917            ..VerifyStreamBeat::default()
918        };
919        let captured = VerifyResult {
920            job_id: b32(0x4242_0004),
921            verified: true,
922            error: b3(VERIFY_ERROR_NONE),
923        };
924        let mut clusters = [VerifyClusterOutput::default(); VERIFY_TOP_CLUSTERS];
925        clusters[0].stream_ready = true;
926        clusters[0].stream_context = b2(1);
927        clusters[0].stream_generation = b8(5);
928        clusters[2].result_valid = true;
929        clusters[2].result = captured;
930        let (output, d) = verifier_top_with_lane_kernel::<NativeLane>(
931            clock_reset(clock(false), reset(false)),
932            VerifierTopInput {
933                stream_valid: true,
934                stream,
935                result_ready: false,
936            },
937            VerifierTopQ::<NativeLane> {
938                clusters,
939                control: VerifierTopControl {
940                    admission_cursor: b2(0),
941                    result_cursor: b2(2),
942                    ..VerifierTopControl::default()
943                },
944                frame_job_id: b32(0),
945                result: VerifyResult::default(),
946            },
947        );
948
949        assert!(output.stream_accepted);
950        assert!(d.clusters[0].stream_valid);
951        assert!(!d.clusters[1].stream_valid);
952        assert!(!d.clusters[2].stream_valid);
953        assert!(!d.clusters[0].result_ready);
954        assert!(!d.clusters[1].result_ready);
955        assert!(d.clusters[2].result_ready);
956        for cluster in d.clusters {
957            assert_eq!(cluster.stream, stream);
958        }
959        assert!(d.control.result_valid);
960        assert_eq!(d.control.result_cluster, b2(2));
961        assert_eq!(d.result, captured);
962    }
963
964    #[test]
965    #[allow(clippy::too_many_lines)] // Clean/fault/registered containment timeline.
966    fn cluster_fault_containment_edge_accepts_beat_zero_exactly_once_then_broadcasts_abort() {
967        let stream = VerifyStreamBeat {
968            job_id: b32(0x4444_0004),
969            ..VerifyStreamBeat::default()
970        };
971        let retained = VerifyResult {
972            job_id: b32(0x5555_0005),
973            verified: true,
974            error: b3(VERIFY_ERROR_NONE),
975        };
976        let mut clusters = [VerifyClusterOutput::default(); VERIFY_TOP_CLUSTERS];
977        clusters[0].fault = true;
978        clusters[0].diagnostics = b16(0x0020);
979        clusters[1].stream_ready = true;
980        clusters[1].stream_context = b2(2);
981        clusters[1].stream_generation = b8(7);
982        clusters[2].result_valid = true;
983        clusters[2].result = VerifyResult {
984            job_id: b32(0x5656_0005),
985            verified: true,
986            error: b3(VERIFY_ERROR_NONE),
987        };
988        let q = VerifierTopQ::<NativeLane> {
989            clusters,
990            control: VerifierTopControl {
991                admission_cursor: b2(1),
992                result_cursor: b2(2),
993                result_valid: true,
994                result_cluster: b2(2),
995                ..VerifierTopControl::default()
996            },
997            frame_job_id: b32(0),
998            result: retained,
999        };
1000        let input = VerifierTopInput {
1001            stream_valid: true,
1002            stream,
1003            result_ready: true,
1004        };
1005        let mut clean_q = q;
1006        clean_q.clusters[0].fault = false;
1007        clean_q.clusters[0].diagnostics = b16(0);
1008        let (_, clean_d) = verifier_top_with_lane_kernel::<NativeLane>(
1009            clock_reset(clock(false), reset(false)),
1010            input,
1011            clean_q,
1012        );
1013        let (containment, d) = verifier_top_with_lane_kernel::<NativeLane>(
1014            clock_reset(clock(false), reset(false)),
1015            input,
1016            q,
1017        );
1018
1019        assert!(containment.fault);
1020        assert!(containment.stream_ready);
1021        assert!(containment.stream_accepted);
1022        assert_eq!(containment.stream_cluster, b2(1));
1023        assert_eq!(containment.stream_context, b2(2));
1024        assert_eq!(containment.stream_generation, b8(7));
1025        assert!(!containment.result_transfer);
1026        assert_eq!(containment.result.job_id, retained.job_id);
1027        assert_eq!(containment.result.error, b3(VERIFY_ERROR_TRANSPORT));
1028        assert!(d.clusters[1].stream_valid);
1029        assert_eq!(d.clusters[1].stream, stream);
1030        assert!(!d.clusters[0].stream_valid);
1031        assert!(!d.clusters[2].stream_valid);
1032        for (cluster, (faulted, clean)) in
1033            d.clusters.iter().zip(clean_d.clusters.iter()).enumerate()
1034        {
1035            assert_eq!(
1036                faulted.stream_valid, clean.stream_valid,
1037                "current fault contaminated cluster {cluster} stream offer"
1038            );
1039            assert_eq!(faulted.stream, stream);
1040        }
1041        assert!(clean_d.clusters[2].result_ready);
1042        for faulted in d.clusters {
1043            assert!(!faulted.result_ready);
1044        }
1045        assert!(d.control.poison);
1046        assert!(!d.control.frame_active);
1047        assert_eq!(d.frame_job_id, stream.job_id);
1048
1049        let mut next_clusters = [VerifyClusterOutput::default(); VERIFY_TOP_CLUSTERS];
1050        for cluster in &mut next_clusters {
1051            cluster.stream_ready = true;
1052        }
1053        let (poisoned, poisoned_d) = verifier_top_with_lane_kernel::<NativeLane>(
1054            clock_reset(clock(false), reset(false)),
1055            VerifierTopInput {
1056                stream_valid: true,
1057                stream,
1058                result_ready: false,
1059            },
1060            VerifierTopQ::<NativeLane> {
1061                clusters: next_clusters,
1062                control: d.control,
1063                frame_job_id: d.frame_job_id,
1064                result: d.result,
1065            },
1066        );
1067        assert!(poisoned.fault);
1068        assert!(!poisoned.stream_ready);
1069        assert!(!poisoned.stream_accepted);
1070        for cluster in 0..VERIFY_TOP_CLUSTERS {
1071            assert!(poisoned_d.clusters[cluster].external_abort);
1072            assert!(!poisoned_d.clusters[cluster].stream_valid);
1073            assert!(!poisoned_d.clusters[cluster].result_ready);
1074        }
1075    }
1076
1077    fn poisoned_drain_capture(
1078        error: b3,
1079        verified: bool,
1080    ) -> (VerifyResult, VerifierTopD<NativeLane>) {
1081        let mut clusters = [VerifyClusterOutput::default(); VERIFY_TOP_CLUSTERS];
1082        clusters[1].result_valid = true;
1083        clusters[1].result = VerifyResult {
1084            job_id: b32(0x6666_0006),
1085            verified,
1086            error,
1087        };
1088        let (output, d) = verifier_top_with_lane_kernel::<NativeLane>(
1089            clock_reset(clock(false), reset(false)),
1090            VerifierTopInput::default(),
1091            VerifierTopQ::<NativeLane> {
1092                clusters,
1093                control: VerifierTopControl {
1094                    result_cursor: b2(1),
1095                    poison: true,
1096                    drain_armed: true,
1097                    ..VerifierTopControl::default()
1098                },
1099                frame_job_id: b32(0),
1100                result: VerifyResult::default(),
1101            },
1102        );
1103        assert!(output.fault);
1104        assert!(!output.result_valid);
1105        assert!(d.control.result_valid);
1106        assert_eq!(d.control.result_cluster, b2(1));
1107        assert!(d.clusters[1].result_ready);
1108        assert!(!d.clusters[0].result_ready);
1109        assert!(!d.clusters[2].result_ready);
1110        (d.result, d)
1111    }
1112
1113    #[test]
1114    fn poisoned_drain_grant_is_payload_independent_and_capture_fails_closed() {
1115        let (clean, _) = poisoned_drain_capture(b3(VERIFY_ERROR_NONE), true);
1116        assert_eq!(clean.job_id, b32(0x6666_0006));
1117        assert!(!clean.verified);
1118        assert_eq!(clean.error, b3(VERIFY_ERROR_TRANSPORT));
1119
1120        let (frame, _) = poisoned_drain_capture(b3(VERIFY_ERROR_FRAME), false);
1121        assert_eq!(frame.job_id, b32(0x6666_0006));
1122        assert!(!frame.verified);
1123        assert_eq!(frame.error, b3(VERIFY_ERROR_FRAME));
1124    }
1125}