Skip to main content

wots_rhdl/
benchmark.rs

1//! On-chip U280 benchmark control for the SHA-256 WOTS signer.
2//!
3//! This module keeps the benchmark workload, one-time seed allocation, signer
4//! handshakes, frame validation, counters, checksum, and AXI4-Lite control in
5//! Rust/RHDL.  The generated Vitis-facing Verilog wrapper is wiring only: it
6//! maps named ports to the packed [`AxiLiteBenchmarkInput`] and
7//! [`AxiLiteBenchmarkOutput`] fields using RHDL `Digital` metadata.
8//!
9//! # One-time-key boundary
10//!
11//! A launch snapshots a 192-bit test nonce and a 64-bit base index.  Job `j`
12//! receives the private seed `nonce || big_endian(base + j)`.  A nonzero batch
13//! is accepted only when `base + batch` fits in 64 bits, so the half-open range
14//! `[base, base + batch)` is injective and a representable next index remains.
15//! The continuously loaded kernel advances its configured base atomically when
16//! the launch is accepted.  That is not durable allocation: software must
17//! reserve and persist the nonce/range before every launch, including warmups
18//! and retries, and must restore a never-before-used range after reset or
19//! reprogramming.
20//!
21//! This is an on-chip compute benchmark.  It does not include `PCIe`, HBM, DDR,
22//! or result-DMA traffic, and its 512-bit XOR fold is an observability checksum,
23//! not a cryptographic authenticator.
24
25use rhdl::{core::circuit::descriptor::SyncKind, prelude::*};
26use rhdl_fpga::core::dff::DFF;
27use sha256_rhdl::lane::{ExternalFullCompressionLane, InlineCompressionLane};
28
29use crate::{
30    CompressionLaneComponent, HashBytes, LANE_LOCAL_CONTEXTS, MessageBytes, SIGNER_CLUSTER_COUNT,
31    SIGNER_FRAME_BEATS, SignerOutputBeat, SignerTop, SignerTopInput,
32    global_beat_is_canonical_kernel,
33};
34
35/// Number of 32-bit words in the test-only 192-bit nonce prefix.
36pub const BENCHMARK_NONCE_WORDS: usize = 6;
37/// Number of 32-bit words in the observable 512-bit checksum.
38pub const BENCHMARK_CHECKSUM_WORDS: usize = 16;
39/// Exact number of live signer contexts represented by the ownership scoreboard.
40pub const BENCHMARK_LIVE_JOB_SLOTS: usize = SIGNER_CLUSTER_COUNT * LANE_LOCAL_CONTEXTS;
41/// AXI4-Lite address width used by the named Vitis wrapper.
42pub const BENCHMARK_AXI_ADDR_BITS: usize = 12;
43/// AXI4-Lite register aperture in bytes.
44pub const BENCHMARK_AXI_RANGE_BYTES: u16 = 4096;
45/// Stable register-map version: major 1, minor 0.
46pub const BENCHMARK_ABI_VERSION: u32 = 0x0001_0000;
47
48/// Canonical separately generated production signer module used by Phase A.
49///
50/// The modular benchmark exposes this name as an unresolved HDL child. The
51/// Phase-A generator must supply the exact RHDL-generated signer skeleton under
52/// the same name before the benchmark bundle is complete.
53pub const BENCHMARK_SIGNER_TOP_MODULE: &str = "wots_sha256_signer_top";
54
55/// Exact synchronous signer interface accepted by the benchmark engine.
56///
57/// Implementations are copyable type-level adapters. Their simulation state is
58/// carried by [`Synchronous::S`], while the adapter value selects whether HDL
59/// lowering expands the signer inline or leaves the separately generated
60/// production signer unresolved.
61pub trait BenchmarkSignerComponent:
62    Synchronous<I = SignerTopInput, O = crate::SignerTopOutput>
63    + Clone
64    + Copy
65    + Default
66    + PartialEq
67    + core::fmt::Debug
68{
69}
70
71impl<T> BenchmarkSignerComponent for T where
72    T: Synchronous<I = SignerTopInput, O = crate::SignerTopOutput>
73        + Clone
74        + Copy
75        + Default
76        + PartialEq
77        + core::fmt::Debug
78{
79}
80
81/// Copyable adapter that expands an ordinary [`SignerTop`] into its parent.
82///
83/// This preserves the historical const-generic benchmark API while keeping
84/// the injected component copyable for the pinned RHDL derive macros.
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub struct InlineBenchmarkSigner<
87    const ROUNDS: usize,
88    const ADDRESS_BITS: usize,
89    L = InlineCompressionLane<ROUNDS, ADDRESS_BITS>,
90>(core::marker::PhantomData<L>)
91where
92    rhdl::bits::W<ROUNDS>: BitWidth,
93    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
94    L: CompressionLaneComponent + Default;
95
96impl<const ROUNDS: usize, const ADDRESS_BITS: usize, L> Default
97    for InlineBenchmarkSigner<ROUNDS, ADDRESS_BITS, L>
98where
99    rhdl::bits::W<ROUNDS>: BitWidth,
100    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
101    L: CompressionLaneComponent + Default,
102{
103    fn default() -> Self {
104        Self(core::marker::PhantomData)
105    }
106}
107
108impl<const ROUNDS: usize, const ADDRESS_BITS: usize, L> SynchronousDQ
109    for InlineBenchmarkSigner<ROUNDS, ADDRESS_BITS, L>
110where
111    rhdl::bits::W<ROUNDS>: BitWidth,
112    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
113    L: CompressionLaneComponent + Default,
114{
115    type D = <SignerTop<ROUNDS, ADDRESS_BITS, L> as SynchronousDQ>::D;
116    type Q = <SignerTop<ROUNDS, ADDRESS_BITS, L> as SynchronousDQ>::Q;
117}
118
119impl<const ROUNDS: usize, const ADDRESS_BITS: usize, L> SynchronousIO
120    for InlineBenchmarkSigner<ROUNDS, ADDRESS_BITS, L>
121where
122    rhdl::bits::W<ROUNDS>: BitWidth,
123    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
124    L: CompressionLaneComponent + Default,
125{
126    type I = SignerTopInput;
127    type O = crate::SignerTopOutput;
128    type Kernel = <SignerTop<ROUNDS, ADDRESS_BITS, L> as SynchronousIO>::Kernel;
129}
130
131impl<const ROUNDS: usize, const ADDRESS_BITS: usize, L> Synchronous
132    for InlineBenchmarkSigner<ROUNDS, ADDRESS_BITS, L>
133where
134    rhdl::bits::W<ROUNDS>: BitWidth,
135    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
136    L: CompressionLaneComponent + Default,
137{
138    type S = <SignerTop<ROUNDS, ADDRESS_BITS, L> as Synchronous>::S;
139
140    fn init(&self) -> Self::S {
141        SignerTop::<ROUNDS, ADDRESS_BITS, L>::default().init()
142    }
143
144    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
145        SignerTop::<ROUNDS, ADDRESS_BITS, L>::default().sim(clock_reset, input, state)
146    }
147
148    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
149        SignerTop::<ROUNDS, ADDRESS_BITS, L>::default().descriptor(scoped_name)
150    }
151}
152
153/// Behavioral production signer with an unresolved external HDL definition.
154///
155/// Simulation delegates to the exact 64-round modular signer, including its
156/// full state and external-lane behavioral model. HDL lowering emits only a
157/// typed reference to [`BENCHMARK_SIGNER_TOP_MODULE`]; the Phase-A generator
158/// separately lowers and checksum-binds that production signer skeleton.
159#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
160pub struct ExternalModularSha256SignerTop;
161
162impl SynchronousDQ for ExternalModularSha256SignerTop {
163    type D = ();
164    type Q = ();
165}
166
167impl SynchronousIO for ExternalModularSha256SignerTop {
168    type I = SignerTopInput;
169    type O = crate::SignerTopOutput;
170    type Kernel = NoSynchronousKernel<ClockReset, SignerTopInput, (), (crate::SignerTopOutput, ())>;
171}
172
173impl Synchronous for ExternalModularSha256SignerTop {
174    type S = <SignerTop<64, 6, ExternalFullCompressionLane> as Synchronous>::S;
175
176    fn init(&self) -> Self::S {
177        SignerTop::<64, 6, ExternalFullCompressionLane>::default().init()
178    }
179
180    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
181        SignerTop::<64, 6, ExternalFullCompressionLane>::default().sim(clock_reset, input, state)
182    }
183
184    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
185        Descriptor::<SyncKind> {
186            name: scoped_name,
187            input_kind: SignerTopInput::static_kind(),
188            output_kind: crate::SignerTopOutput::static_kind(),
189            d_kind: Kind::Empty,
190            q_kind: Kind::Empty,
191            kernel: None,
192            netlist: None,
193            hdl: Some(HDLDescriptor {
194                name: BENCHMARK_SIGNER_TOP_MODULE.into(),
195                modules: vlog::ModuleList { modules: vec![] },
196            }),
197            _phantom: core::marker::PhantomData,
198        }
199        .with_external_netlist_black_box()
200    }
201}
202
203/// AXI4-Lite `OKAY` response.
204pub const AXI_RESP_OKAY: u8 = 0;
205/// AXI4-Lite `SLVERR` response.
206pub const AXI_RESP_SLVERR: u8 = 2;
207
208/// No launch rejection.
209pub const BENCHMARK_REJECT_NONE: u8 = 0;
210/// A zero-length batch was rejected.
211pub const BENCHMARK_REJECT_ZERO_BATCH: u8 = 1;
212/// `base + batch` required a 65th bit and was rejected.
213pub const BENCHMARK_REJECT_INDEX_OVERFLOW: u8 = 2;
214
215/// No structural fault.
216pub const BENCHMARK_FAULT_NONE: u8 = 0;
217/// The wrapped signer reported a sticky structural fault.
218pub const BENCHMARK_FAULT_SIGNER: u8 = 0x10;
219/// A signer admission pulse violated the benchmark ownership contract.
220pub const BENCHMARK_FAULT_ADMISSION: u8 = 0x11;
221/// A transferred output beat violated canonical control or frame identity.
222pub const BENCHMARK_FAULT_FRAME: u8 = 0x12;
223/// A transferred error completion had invalid identity or handshake metadata.
224pub const BENCHMARK_FAULT_ERROR: u8 = 0x13;
225/// A completion counter exceeded the accepted batch contract.
226pub const BENCHMARK_FAULT_COUNTER: u8 = 0x14;
227
228/// Standard Vitis control register.
229pub const REG_AP_CTRL: u16 = 0x000;
230/// Global interrupt enable.
231pub const REG_GIE: u16 = 0x004;
232/// Per-event interrupt enable (`done`, `fault`).
233pub const REG_IER: u16 = 0x008;
234/// Toggle-on-write interrupt status (`done`, `fault`).
235pub const REG_ISR: u16 = 0x00c;
236/// Requested batch size.
237pub const REG_BATCH: u16 = 0x010;
238/// Read-only ABI version.
239pub const REG_ABI_VERSION: u16 = 0x014;
240/// First nonce-prefix word; words are consecutive through `0x02c`.
241pub const REG_NONCE_0: u16 = 0x018;
242/// Current base/next-index low word.
243pub const REG_BASE_LO: u16 = 0x030;
244/// Current base/next-index high word.
245pub const REG_BASE_HI: u16 = 0x034;
246/// Base index latched by the most recently accepted launch, low word.
247pub const REG_RUN_BASE_LO: u16 = 0x038;
248/// Base index latched by the most recently accepted launch, high word.
249pub const REG_RUN_BASE_HI: u16 = 0x03c;
250/// Packed busy/fault/rejection status.
251pub const REG_STATUS: u16 = 0x040;
252/// Low byte is structural fault code; next byte is last rejection code.
253pub const REG_CODES: u16 = 0x044;
254/// Accepted-job counter, low word; high word follows.
255pub const REG_ACCEPTED_LO: u16 = 0x048;
256/// Successful-frame completion counter, low word; high word follows.
257pub const REG_COMPLETED_LO: u16 = 0x050;
258/// Transferred-beat counter, low word; high word follows.
259pub const REG_BEATS_LO: u16 = 0x058;
260/// Identified error-completion counter, low word; high word follows.
261pub const REG_ERRORS_LO: u16 = 0x060;
262/// Active run cycles, low word; high word follows.
263pub const REG_ELAPSED_LO: u16 = 0x068;
264/// Zero-based cycle of the first successful final beat, low word.
265pub const REG_FIRST_COMPLETION_LO: u16 = 0x070;
266/// Zero-based cycle of the last successful final beat, low word.
267pub const REG_LAST_COMPLETION_LO: u16 = 0x078;
268/// Last-minus-first successful completion span, low word.
269pub const REG_COMPLETION_SPAN_LO: u16 = 0x080;
270/// Inclusive first-to-last transferred-payload span, low word.
271pub const REG_PAYLOAD_CYCLES_LO: u16 = 0x088;
272/// First checksum word; sixteen consecutive words end at `0x0cc`.
273pub const REG_CHECKSUM_0: u16 = 0x090;
274
275const _: () = {
276    assert!(SIGNER_FRAME_BEATS == 35);
277    assert!(BENCHMARK_NONCE_WORDS * 32 == 192);
278    assert!(BENCHMARK_CHECKSUM_WORDS * 32 == 512);
279    assert!(SIGNER_CLUSTER_COUNT == 3);
280    assert!(LANE_LOCAL_CONTEXTS == 4);
281    assert!(BENCHMARK_LIVE_JOB_SLOTS == 12);
282    assert!(BENCHMARK_AXI_RANGE_BYTES == 1 << BENCHMARK_AXI_ADDR_BITS);
283    assert!(REG_CHECKSUM_0 + 64 <= BENCHMARK_AXI_RANGE_BYTES);
284};
285
286/// Configuration offered to the on-chip benchmark engine.
287#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
288pub struct BenchmarkEngineInput {
289    /// Hold high until either `launch_accepted` or `launch_rejected` pulses.
290    pub launch_valid: bool,
291    /// Number of one-time signatures requested; zero is rejected.
292    pub batch: b32,
293    /// Six little-endian register words forming 24 seed bytes in word order.
294    pub nonce_prefix: [b32; BENCHMARK_NONCE_WORDS],
295    /// First one-time index in the requested half-open range.
296    pub base_index: b64,
297}
298
299/// Complete benchmark status retained for AXI4-Lite reads.
300#[allow(clippy::struct_excessive_bools)] // Independent protocol and audit indications.
301#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
302pub struct BenchmarkEngineOutput {
303    /// The engine can inspect a launch offer.
304    pub launch_ready: bool,
305    /// A valid launch reserved its complete one-time range this cycle.
306    pub launch_accepted: bool,
307    /// An invalid launch was rejected without consuming its range this cycle.
308    pub launch_rejected: bool,
309    /// Reason for `launch_rejected`.
310    pub reject_code: b8,
311    /// One benchmark batch is active.
312    pub busy: bool,
313    /// A valid batch completed, a launch was rejected, or a fault terminated work.
314    pub done: bool,
315    /// Sticky structural fault; shared reset is required.
316    pub fault: bool,
317    /// First retained structural fault code.
318    pub fault_code: b8,
319    /// Base latched by the latest accepted launch.
320    pub run_base_index: b64,
321    /// First unused index after the accepted batch.
322    pub next_base_index: b64,
323    /// Number of jobs admitted by the signer.
324    pub accepted: b64,
325    /// Number of canonical successful frames completed.
326    pub completed: b64,
327    /// Number of signer output beats transferred into the checker.
328    pub beats: b64,
329    /// Number of identified signer error completions accepted.
330    pub errors: b64,
331    /// Run cycles including fill, payload, and drain.
332    pub elapsed_cycles: b64,
333    /// Zero-based active cycle containing the first successful final beat.
334    pub first_completion_cycle: b64,
335    /// Zero-based active cycle containing the latest successful final beat.
336    pub last_completion_cycle: b64,
337    /// `last_completion_cycle - first_completion_cycle`, or zero for fewer than two.
338    pub completion_span_cycles: b64,
339    /// Inclusive span from the first through latest transferred data beat.
340    pub payload_cycles: b64,
341    /// Wide deterministic fold over every transferred payload and frame identity.
342    pub checksum: [b32; BENCHMARK_CHECKSUM_WORDS],
343}
344
345/// Launch-range validation input.
346#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
347pub struct BenchmarkLaunchValidationInput {
348    /// Requested batch.
349    pub batch: b32,
350    /// Requested first index.
351    pub base_index: b64,
352}
353
354/// Launch-range validation result.
355#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
356pub struct BenchmarkLaunchValidationOutput {
357    /// The nonempty half-open range is representable.
358    pub valid: bool,
359    /// First unused index, meaningful only when `valid`.
360    pub next_base_index: b64,
361    /// Rejection reason, zero only when `valid`.
362    pub reject_code: b8,
363}
364
365/// Reject zero batches and any reservation whose exclusive end needs 65 bits.
366#[kernel]
367pub fn benchmark_launch_validation_kernel(
368    input: BenchmarkLaunchValidationInput,
369) -> BenchmarkLaunchValidationOutput {
370    let wide_base: b65 = input.base_index.resize();
371    let wide_batch: b65 = input.batch.resize();
372    let wide_next = wide_base + wide_batch;
373    let overflow = (wide_next >> 64) != b65(0);
374    let zero = input.batch == b32(0);
375    let valid = !zero && !overflow;
376    BenchmarkLaunchValidationOutput {
377        valid,
378        next_base_index: wide_next.resize(),
379        reject_code: if zero {
380            b8(1)
381        } else if overflow {
382            b8(2)
383        } else {
384            b8(0)
385        },
386    }
387}
388
389/// Deterministic private seed and already-hashed benchmark message.
390#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
391pub struct BenchmarkJobMaterial {
392    /// Injective one-time seed `nonce || big_endian(index)`.
393    pub private_seed: HashBytes,
394    /// Distinct deterministic test-message domain.
395    pub message: MessageBytes,
396}
397
398/// Construct one deterministic benchmark job.
399///
400/// Nonce register word zero contributes bytes zero through three, least
401/// significant byte first.  The final eight seed bytes are the index in network
402/// (big-endian) order.  The message is an injective bytewise domain transform
403/// of the seed; it is test data, not an application prehash construction.
404#[kernel]
405#[allow(clippy::needless_range_loop)] // Constant loops are directly lowered by pinned RHDL.
406pub fn benchmark_job_material_kernel(
407    nonce_prefix: [b32; BENCHMARK_NONCE_WORDS],
408    index: b64,
409) -> BenchmarkJobMaterial {
410    let mut private_seed = [b8(0); 32];
411    for word in 0..BENCHMARK_NONCE_WORDS {
412        private_seed[word * 4] = nonce_prefix[word].resize();
413        private_seed[word * 4 + 1] = (nonce_prefix[word] >> 8).resize();
414        private_seed[word * 4 + 2] = (nonce_prefix[word] >> 16).resize();
415        private_seed[word * 4 + 3] = (nonce_prefix[word] >> 24).resize();
416    }
417    private_seed[24] = (index >> 56).resize();
418    private_seed[25] = (index >> 48).resize();
419    private_seed[26] = (index >> 40).resize();
420    private_seed[27] = (index >> 32).resize();
421    private_seed[28] = (index >> 24).resize();
422    private_seed[29] = (index >> 16).resize();
423    private_seed[30] = (index >> 8).resize();
424    private_seed[31] = index.resize();
425
426    let mut message = [b8(0); 32];
427    for lane in 0..32 {
428        message[lane] = private_seed[lane] ^ b8(0xa5);
429    }
430    BenchmarkJobMaterial {
431        private_seed,
432        message,
433    }
434}
435
436/// Input to the benchmark's independent frame checker.
437#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
438pub struct BenchmarkFrameValidationInput {
439    /// Candidate signer output.
440    pub output: SignerOutputBeat,
441    /// Whether a frame is already active.
442    pub frame_active: bool,
443    /// Next expected beat number.
444    pub expected_beat: b6,
445    /// Job identity captured from beat zero.
446    pub frame_job_id: b32,
447    /// Cluster identity captured from beat zero.
448    pub frame_cluster: b2,
449    /// Latched batch bound for job identities.
450    pub batch: b32,
451}
452
453/// Independently validate canonical framing and batch identity.
454#[kernel]
455pub fn benchmark_frame_is_canonical_kernel(input: BenchmarkFrameValidationInput) -> bool {
456    let continuity = !input.frame_active
457        || (input.output.beat.job_id == input.frame_job_id
458            && input.output.cluster == input.frame_cluster);
459    input.output.beat.job_id < input.batch
460        && input.output.cluster < b2(3)
461        && continuity
462        && global_beat_is_canonical_kernel(crate::GlobalBeatValidationInput {
463            beat: input.output.beat,
464            expected_beat: input.expected_beat,
465            expected_job_id: input.frame_job_id,
466            check_job_id: input.frame_active,
467        })
468}
469
470/// Input to the 512-bit observability fold.
471#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
472pub struct BenchmarkChecksumInput {
473    /// Previous fold.
474    pub checksum: [b32; BENCHMARK_CHECKSUM_WORDS],
475    /// Transferred signer output.
476    pub output: SignerOutputBeat,
477    /// Independently tracked frame beat number.
478    pub beat_index: b6,
479}
480
481/// XOR-fold one transferred 512-bit beat plus its job/cluster/beat identity.
482///
483/// The fold is deliberately cheap and completely observable.  It is not
484/// collision resistant and must never be used to authenticate a signature.
485#[kernel]
486#[allow(clippy::needless_range_loop)] // Constant word packing is lowered directly.
487pub fn benchmark_checksum_kernel(input: BenchmarkChecksumInput) -> [b32; BENCHMARK_CHECKSUM_WORDS] {
488    let mut checksum = input.checksum;
489    let identity = input.output.beat.job_id
490        ^ input.beat_index.resize::<32>()
491        ^ input.output.cluster.resize::<32>();
492    for word in 0..BENCHMARK_CHECKSUM_WORDS {
493        let byte_zero: b32 = input.output.beat.data[word * 4].resize();
494        let byte_one: b32 = input.output.beat.data[word * 4 + 1].resize();
495        let byte_two: b32 = input.output.beat.data[word * 4 + 2].resize();
496        let byte_three: b32 = input.output.beat.data[word * 4 + 3].resize();
497        let packed = byte_zero | (byte_one << 8) | (byte_two << 16) | (byte_three << 24);
498        checksum[word] = input.checksum[word] ^ packed ^ identity;
499    }
500    checksum
501}
502
503/// One exact live-job ownership record in the three-cluster/four-context signer.
504///
505/// Slots `0..=3`, `4..=7`, and `8..=11` are reserved for clusters zero, one,
506/// and two respectively.  The physical context number is deliberately not
507/// trusted as returned metadata on successful frames, so ownership is matched
508/// by the unique `(job_id, cluster)` pair retained at admission.
509#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
510pub struct BenchmarkLiveJob {
511    /// This signer-context capacity slot owns a job.
512    pub live: bool,
513    /// Unique job identity offered on the accepted start.
514    pub job_id: b32,
515    /// Admitting cluster, redundantly checked against the slot partition.
516    pub cluster: b2,
517    /// Beat zero of this job's successful frame has transferred.
518    pub frame_started: bool,
519}
520
521/// Input to the live-job scoreboard integrity audit.
522#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
523pub struct BenchmarkScoreboardAuditInput {
524    /// Exact twelve-entry ownership scoreboard.
525    pub live_jobs: [BenchmarkLiveJob; BENCHMARK_LIVE_JOB_SLOTS],
526}
527
528/// Integrity and population summary for the live-job scoreboard.
529#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
530pub struct BenchmarkScoreboardAuditOutput {
531    /// Every live entry occupies its statically assigned cluster partition.
532    pub valid: bool,
533    /// Number of live owners, in `0..=12`.
534    pub live_count: b4,
535    /// No signer context remains owned by the benchmark.
536    pub empty: bool,
537}
538
539/// Map one of the exact twelve capacity slots to its owning cluster partition.
540#[kernel]
541fn benchmark_scoreboard_slot_cluster_kernel(slot: b4) -> b2 {
542    if slot < b4(4) {
543        b2(0)
544    } else if slot < b4(8) {
545        b2(1)
546    } else {
547        b2(2)
548    }
549}
550
551/// Audit cluster partitioning and count live ownership in one linear pass.
552#[kernel]
553#[allow(clippy::assign_op_pattern, clippy::needless_range_loop)] // Preserve reviewed explicit accumulation and fixed-loop source shape.
554pub fn benchmark_scoreboard_audit_kernel(
555    input: BenchmarkScoreboardAuditInput,
556) -> BenchmarkScoreboardAuditOutput {
557    let mut valid = true;
558    let mut live_count = b4(0);
559    for slot in 0..BENCHMARK_LIVE_JOB_SLOTS {
560        if input.live_jobs[slot].live {
561            live_count = live_count + b4(1);
562            let expected_cluster = benchmark_scoreboard_slot_cluster_kernel(b4(slot as u128));
563            if input.live_jobs[slot].cluster != expected_cluster {
564                valid = false;
565            }
566        }
567    }
568    BenchmarkScoreboardAuditOutput {
569        valid,
570        live_count,
571        empty: live_count == b4(0),
572    }
573}
574
575/// Candidate signer admission offered to the exact ownership scoreboard.
576#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
577pub struct BenchmarkScoreboardAdmissionInput {
578    /// Current scoreboard before the admission pulse.
579    pub live_jobs: [BenchmarkLiveJob; BENCHMARK_LIVE_JOB_SLOTS],
580    /// Job identity accepted by the signer.
581    pub job_id: b32,
582    /// Cluster reported by the signer admission handshake.
583    pub cluster: b2,
584}
585
586/// Allocation decision for one accepted signer start.
587#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
588pub struct BenchmarkScoreboardAdmissionOutput {
589    /// A unique free slot exists in the reported cluster partition.
590    pub valid: bool,
591    /// Exact scoreboard slot to allocate when `valid` is true.
592    pub slot: b4,
593}
594
595/// Allocate the first free capacity slot in the admitted cluster.
596#[kernel]
597#[allow(clippy::needless_range_loop)] // Fixed scoreboard loops are lowered directly.
598pub fn benchmark_scoreboard_admission_kernel(
599    input: BenchmarkScoreboardAdmissionInput,
600) -> BenchmarkScoreboardAdmissionOutput {
601    let mut partition_valid = true;
602    let mut duplicate = false;
603    let mut free_found = false;
604    let mut slot = b4(0);
605    for candidate in 0..BENCHMARK_LIVE_JOB_SLOTS {
606        let candidate_cluster = benchmark_scoreboard_slot_cluster_kernel(b4(candidate as u128));
607        if input.live_jobs[candidate].live
608            && input.live_jobs[candidate].cluster != candidate_cluster
609        {
610            partition_valid = false;
611        }
612        if input.live_jobs[candidate].live && input.live_jobs[candidate].job_id == input.job_id {
613            duplicate = true;
614        }
615        if !free_found && candidate_cluster == input.cluster && !input.live_jobs[candidate].live {
616            free_found = true;
617            slot = b4(candidate as u128);
618        }
619    }
620    BenchmarkScoreboardAdmissionOutput {
621        valid: partition_valid && input.cluster < b2(3) && !duplicate && free_found,
622        slot,
623    }
624}
625
626/// Lookup request for a data or error completion owner.
627#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
628pub struct BenchmarkScoreboardLookupInput {
629    /// Current scoreboard before this cycle's events.
630    pub live_jobs: [BenchmarkLiveJob; BENCHMARK_LIVE_JOB_SLOTS],
631    /// Returned job identity.
632    pub job_id: b32,
633    /// Returned source cluster.
634    pub cluster: b2,
635}
636
637/// Exact live-owner lookup result.
638#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
639pub struct BenchmarkScoreboardLookupOutput {
640    /// Exactly one well-formed live entry owns the returned identity.
641    pub valid: bool,
642    /// Matching scoreboard slot when `valid` is true.
643    pub slot: b4,
644    /// Whether the matching job has already started a successful frame.
645    pub frame_started: bool,
646    /// Raw number of matching live owners, retained for audit tests.
647    pub match_count: b4,
648}
649
650/// Match a returned `(job_id, cluster)` against exactly one live owner.
651#[kernel]
652#[allow(clippy::assign_op_pattern, clippy::needless_range_loop)] // Preserve reviewed explicit accumulation and fixed-loop source shape.
653pub fn benchmark_scoreboard_lookup_kernel(
654    input: BenchmarkScoreboardLookupInput,
655) -> BenchmarkScoreboardLookupOutput {
656    let mut slot = b4(0);
657    let mut frame_started = false;
658    let mut match_count = b4(0);
659    let mut exact_count = b4(0);
660    for candidate in 0..BENCHMARK_LIVE_JOB_SLOTS {
661        if input.live_jobs[candidate].live && input.live_jobs[candidate].job_id == input.job_id {
662            match_count = match_count + b4(1);
663            let candidate_cluster = benchmark_scoreboard_slot_cluster_kernel(b4(candidate as u128));
664            if input.live_jobs[candidate].cluster == input.cluster
665                && input.live_jobs[candidate].cluster == candidate_cluster
666            {
667                slot = b4(candidate as u128);
668                frame_started = input.live_jobs[candidate].frame_started;
669                exact_count = exact_count + b4(1);
670            }
671        }
672    }
673    BenchmarkScoreboardLookupOutput {
674        valid: input.cluster < b2(3) && match_count == b4(1) && exact_count == b4(1),
675        slot,
676        frame_started,
677        match_count,
678    }
679}
680
681/// Counter/ownership audit after all current-cycle admissions and terminals.
682#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
683pub struct BenchmarkCounterAuditInput {
684    /// Updated exact live-job scoreboard.
685    pub live_jobs: [BenchmarkLiveJob; BENCHMARK_LIVE_JOB_SLOTS],
686    /// Updated admitted-job count.
687    pub accepted: b64,
688    /// Updated successful-frame count.
689    pub completed: b64,
690    /// Updated error-terminal count.
691    pub errors: b64,
692    /// Updated transferred-data-beat count.
693    pub beats: b64,
694    /// Latched batch size.
695    pub batch: b32,
696    /// Whether a successful frame remains partially transferred.
697    pub frame_active: bool,
698}
699
700/// Counter/ownership invariants and exact healthy terminal predicate.
701#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
702pub struct BenchmarkCounterAuditOutput {
703    /// Running counters and ownership agree exactly.
704    pub valid: bool,
705    /// All batch jobs terminated with exact beat accounting and no live owner.
706    pub terminal_exact: bool,
707    /// Number of live scoreboard owners.
708    pub live_count: b4,
709}
710
711/// Cross-check counters against the exact live-job scoreboard.
712#[kernel]
713pub fn benchmark_counter_audit_kernel(
714    input: BenchmarkCounterAuditInput,
715) -> BenchmarkCounterAuditOutput {
716    let scoreboard = benchmark_scoreboard_audit_kernel(BenchmarkScoreboardAuditInput {
717        live_jobs: input.live_jobs,
718    });
719    let terminal_total = input.completed + input.errors;
720    let live_count: b64 = scoreboard.live_count.resize();
721    let batch: b64 = input.batch.resize();
722    let expected_beats = (input.completed << 5) + (input.completed << 1) + input.completed;
723    let valid = scoreboard.valid
724        && input.accepted <= batch
725        && terminal_total <= input.accepted
726        && terminal_total + live_count == input.accepted;
727    BenchmarkCounterAuditOutput {
728        valid,
729        terminal_exact: valid
730            && terminal_total == batch
731            && input.accepted == batch
732            && scoreboard.empty
733            && !input.frame_active
734            && input.beats == expected_beats,
735        live_count: scoreboard.live_count,
736    }
737}
738
739/// Resettable benchmark ownership and counter state.
740#[allow(clippy::struct_excessive_bools)] // Independent ownership markers are intentional.
741#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
742pub struct BenchmarkEngineControl {
743    /// One batch owns the engine.
744    pub busy: bool,
745    /// Sticky structural failure.
746    pub fault: bool,
747    /// First structural fault code.
748    pub fault_code: b8,
749    /// Number of signer admissions.
750    pub accepted: b64,
751    /// Number of canonical successful final transfers.
752    pub completed: b64,
753    /// Number of data-beat transfers.
754    pub beats: b64,
755    /// Number of error-completion transfers.
756    pub errors: b64,
757    /// Exact live ownership of all three-by-four signer contexts.
758    pub live_jobs: [BenchmarkLiveJob; BENCHMARK_LIVE_JOB_SLOTS],
759    /// Zero-based active cycle counter while busy; total cycles after done.
760    pub elapsed_cycles: b64,
761    /// A payload beat has transferred.
762    pub payload_started: bool,
763    /// Cycle of the first payload beat.
764    pub first_payload_cycle: b64,
765    /// Inclusive first-through-latest payload span.
766    pub payload_cycles: b64,
767    /// At least one successful final beat transferred.
768    pub completion_started: bool,
769    /// Cycle of first successful final beat.
770    pub first_completion_cycle: b64,
771    /// Cycle of latest successful final beat.
772    pub last_completion_cycle: b64,
773    /// Last-minus-first successful completion span.
774    pub completion_span_cycles: b64,
775    /// An output frame is being checked.
776    pub frame_active: bool,
777    /// Next expected frame beat.
778    pub expected_beat: b6,
779    /// Job captured at frame beat zero.
780    pub frame_job_id: b32,
781    /// Cluster captured at frame beat zero.
782    pub frame_cluster: b2,
783}
784
785/// Resettable run data and retained results.
786#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
787pub struct BenchmarkEngineData {
788    /// Latched batch size.
789    pub batch: b32,
790    /// Latched nonce.
791    pub nonce_prefix: [b32; BENCHMARK_NONCE_WORDS],
792    /// Latched first index.
793    pub run_base_index: b64,
794    /// First unused index after the batch.
795    pub next_base_index: b64,
796    /// Observable checksum.
797    pub checksum: [b32; BENCHMARK_CHECKSUM_WORDS],
798}
799
800/// On-chip workload generator, injected signer driver, checker, and counter bank.
801#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
802pub struct BenchmarkEngineWithSigner<S = InlineBenchmarkSigner<64, 6>>
803where
804    S: BenchmarkSignerComponent,
805{
806    signer: S,
807    control: DFF<BenchmarkEngineControl>,
808    data: DFF<BenchmarkEngineData>,
809}
810
811impl<S> Default for BenchmarkEngineWithSigner<S>
812where
813    S: BenchmarkSignerComponent,
814{
815    fn default() -> Self {
816        Self {
817            signer: S::default(),
818            control: DFF::new(BenchmarkEngineControl::default()),
819            data: DFF::new(BenchmarkEngineData::default()),
820        }
821    }
822}
823
824impl<S> SynchronousIO for BenchmarkEngineWithSigner<S>
825where
826    S: BenchmarkSignerComponent,
827{
828    type I = BenchmarkEngineInput;
829    type O = BenchmarkEngineOutput;
830    type Kernel = benchmark_engine_kernel<S>;
831}
832
833/// State transition for [`BenchmarkEngineWithSigner`].
834#[kernel]
835#[allow(clippy::if_not_else, clippy::needless_range_loop)] // Preserve first-event branch order and constant-loop lowering.
836pub fn benchmark_engine_kernel<S: BenchmarkSignerComponent>(
837    clock_reset: ClockReset,
838    input: BenchmarkEngineInput,
839    q: BenchmarkEngineWithSignerQ<S>,
840) -> (BenchmarkEngineOutput, BenchmarkEngineWithSignerD<S>) {
841    let resetting = clock_reset.reset.any();
842    let launch = benchmark_launch_validation_kernel(BenchmarkLaunchValidationInput {
843        batch: input.batch,
844        base_index: input.base_index,
845    });
846    let launch_ready = !resetting && !q.control.busy && !q.control.fault && !q.signer.fault;
847    let launch_offered = launch_ready && input.launch_valid;
848    let launch_accepted = launch_offered && launch.valid;
849    let launch_rejected = launch_offered && !launch.valid;
850
851    let mut control = q.control;
852    let mut data = q.data;
853    let mut done = false;
854
855    if launch_accepted {
856        control = BenchmarkEngineControl {
857            busy: true,
858            ..BenchmarkEngineControl::default()
859        };
860        data = BenchmarkEngineData {
861            batch: input.batch,
862            nonce_prefix: input.nonce_prefix,
863            run_base_index: input.base_index,
864            next_base_index: launch.next_base_index,
865            checksum: [b32(0); BENCHMARK_CHECKSUM_WORDS],
866        };
867    }
868    if launch_rejected {
869        done = true;
870    }
871
872    let active = q.control.busy && !q.control.fault && !q.signer.fault;
873    let active_cycle = q.control.elapsed_cycles;
874    if active {
875        control.elapsed_cycles = q.control.elapsed_cycles + b64(1);
876    }
877
878    let current_index = q.data.run_base_index + q.control.accepted;
879    let material = benchmark_job_material_kernel(q.data.nonce_prefix, current_index);
880    let offer_job = active && q.control.accepted < q.data.batch.resize::<64>();
881    let signer_input = SignerTopInput {
882        start_valid: offer_job,
883        private_seed: material.private_seed,
884        message: material.message,
885        job_id: q.control.accepted.resize(),
886        output_ready: active,
887        error_ready: active,
888    };
889
890    let admitted_cluster_ready = match q.signer.start_cluster {
891        Bits::<2>(0) => q.signer.cluster_start_ready[0],
892        Bits::<2>(1) => q.signer.cluster_start_ready[1],
893        Bits::<2>(2) => q.signer.cluster_start_ready[2],
894        _ => false,
895    };
896    let admission = benchmark_scoreboard_admission_kernel(BenchmarkScoreboardAdmissionInput {
897        live_jobs: q.control.live_jobs,
898        job_id: q.control.accepted.resize(),
899        cluster: q.signer.start_cluster,
900    });
901    let admission_fault = q.signer.start_accepted
902        && (!active
903            || !offer_job
904            || !q.signer.start_ready
905            || !admitted_cluster_ready
906            || !admission.valid);
907    let admission_event_valid = q.signer.start_accepted && !admission_fault;
908    if admission_event_valid {
909        control.accepted = q.control.accepted + b64(1);
910    }
911
912    let beat_transfer = active && q.signer.output_valid;
913    let error_transfer = active && q.signer.error_valid;
914    let same_terminal_identity = beat_transfer
915        && error_transfer
916        && q.signer.output.beat.job_id == q.signer.error.job_id
917        && q.signer.output.cluster == q.signer.error.cluster;
918    let mut frame_fault = false;
919    let mut error_fault = false;
920    let frame_owner = benchmark_scoreboard_lookup_kernel(BenchmarkScoreboardLookupInput {
921        live_jobs: q.control.live_jobs,
922        job_id: q.signer.output.beat.job_id,
923        cluster: q.signer.output.cluster,
924    });
925    let error_owner = benchmark_scoreboard_lookup_kernel(BenchmarkScoreboardLookupInput {
926        live_jobs: q.control.live_jobs,
927        job_id: q.signer.error.job_id,
928        cluster: q.signer.error.cluster,
929    });
930    let final_beat = q.control.expected_beat == b6(34);
931    let frame_owner_phase = if q.control.frame_active {
932        frame_owner.frame_started
933    } else {
934        !frame_owner.frame_started && q.control.expected_beat == b6(0)
935    };
936    let mut frame_event_valid = false;
937    let mut error_event_valid = false;
938
939    if beat_transfer {
940        control.beats = q.control.beats + b64(1);
941        data.checksum = benchmark_checksum_kernel(BenchmarkChecksumInput {
942            checksum: q.data.checksum,
943            output: q.signer.output,
944            beat_index: q.control.expected_beat,
945        });
946        if !q.control.payload_started {
947            control.payload_started = true;
948            control.first_payload_cycle = active_cycle;
949            control.payload_cycles = b64(1);
950        } else {
951            control.payload_cycles = active_cycle - q.control.first_payload_cycle + b64(1);
952        }
953
954        let canonical = benchmark_frame_is_canonical_kernel(BenchmarkFrameValidationInput {
955            output: q.signer.output,
956            frame_active: q.control.frame_active,
957            expected_beat: q.control.expected_beat,
958            frame_job_id: q.control.frame_job_id,
959            frame_cluster: q.control.frame_cluster,
960            batch: q.data.batch,
961        });
962        frame_event_valid = canonical
963            && frame_owner.valid
964            && frame_owner_phase
965            && q.signer.delivered_final == final_beat;
966        frame_fault = !frame_event_valid;
967        if frame_event_valid {
968            if !q.control.frame_active {
969                control.frame_active = true;
970                control.frame_job_id = q.signer.output.beat.job_id;
971                control.frame_cluster = q.signer.output.cluster;
972            }
973            if final_beat {
974                control.frame_active = false;
975                control.expected_beat = b6(0);
976                control.completed = q.control.completed + b64(1);
977                if !q.control.completion_started {
978                    control.completion_started = true;
979                    control.first_completion_cycle = active_cycle;
980                    control.last_completion_cycle = active_cycle;
981                    control.completion_span_cycles = b64(0);
982                } else {
983                    control.last_completion_cycle = active_cycle;
984                    control.completion_span_cycles =
985                        active_cycle - q.control.first_completion_cycle;
986                }
987            } else {
988                control.expected_beat = q.control.expected_beat + b6(1);
989            }
990        }
991    } else if q.signer.delivered_final {
992        frame_fault = true;
993    }
994
995    if error_transfer {
996        error_event_valid = q.signer.delivered_error
997            && error_owner.valid
998            && !error_owner.frame_started
999            && !same_terminal_identity
1000            && q.signer.error.job_id < q.data.batch
1001            && q.signer.error.cluster < b2(3);
1002        error_fault = !error_event_valid;
1003        if error_event_valid {
1004            control.errors = q.control.errors + b64(1);
1005        }
1006    } else if q.signer.delivered_error {
1007        error_fault = true;
1008    }
1009
1010    let mut live_jobs = q.control.live_jobs;
1011    if frame_event_valid {
1012        for slot in 0..BENCHMARK_LIVE_JOB_SLOTS {
1013            if b4(slot as u128) == frame_owner.slot {
1014                if final_beat {
1015                    live_jobs[slot] = BenchmarkLiveJob::default();
1016                } else if !q.control.frame_active {
1017                    live_jobs[slot].frame_started = true;
1018                }
1019            }
1020        }
1021    }
1022    if error_event_valid {
1023        for slot in 0..BENCHMARK_LIVE_JOB_SLOTS {
1024            if b4(slot as u128) == error_owner.slot {
1025                live_jobs[slot] = BenchmarkLiveJob::default();
1026            }
1027        }
1028    }
1029    if admission_event_valid {
1030        for slot in 0..BENCHMARK_LIVE_JOB_SLOTS {
1031            if b4(slot as u128) == admission.slot {
1032                live_jobs[slot] = BenchmarkLiveJob {
1033                    live: true,
1034                    job_id: q.control.accepted.resize(),
1035                    cluster: q.signer.start_cluster,
1036                    frame_started: false,
1037                };
1038            }
1039        }
1040    }
1041    control.live_jobs = live_jobs;
1042
1043    let counter_audit = benchmark_counter_audit_kernel(BenchmarkCounterAuditInput {
1044        live_jobs,
1045        accepted: control.accepted,
1046        completed: control.completed,
1047        errors: control.errors,
1048        beats: control.beats,
1049        batch: q.data.batch,
1050        frame_active: control.frame_active,
1051    });
1052    let terminal_total = control.completed + control.errors;
1053    let batch_total: b64 = q.data.batch.resize();
1054    let protocol_fault = q.signer.fault || admission_fault || frame_fault || error_fault;
1055    let counter_fault = active
1056        && !protocol_fault
1057        && (!counter_audit.valid
1058            || (terminal_total == batch_total && !counter_audit.terminal_exact));
1059    let observed_fault = protocol_fault || counter_fault;
1060    if active && !observed_fault && counter_audit.terminal_exact {
1061        control.busy = false;
1062        done = true;
1063    }
1064    if q.control.busy && observed_fault {
1065        control.fault = true;
1066        control.fault_code = if q.signer.fault {
1067            b8(0x10)
1068        } else if admission_fault {
1069            b8(0x11)
1070        } else if frame_fault {
1071            b8(0x12)
1072        } else if error_fault {
1073            b8(0x13)
1074        } else {
1075            b8(0x14)
1076        };
1077        control.busy = false;
1078        done = true;
1079    }
1080
1081    if resetting {
1082        control = BenchmarkEngineControl::default();
1083        data = BenchmarkEngineData::default();
1084        done = false;
1085    }
1086
1087    let output = BenchmarkEngineOutput {
1088        launch_ready,
1089        launch_accepted: !resetting && launch_accepted,
1090        launch_rejected: !resetting && launch_rejected,
1091        reject_code: if launch_rejected {
1092            launch.reject_code
1093        } else {
1094            b8(0)
1095        },
1096        busy: !resetting && control.busy,
1097        done: !resetting && done,
1098        fault: !resetting && control.fault,
1099        fault_code: control.fault_code,
1100        run_base_index: data.run_base_index,
1101        next_base_index: data.next_base_index,
1102        accepted: control.accepted,
1103        completed: control.completed,
1104        beats: control.beats,
1105        errors: control.errors,
1106        elapsed_cycles: control.elapsed_cycles,
1107        first_completion_cycle: control.first_completion_cycle,
1108        last_completion_cycle: control.last_completion_cycle,
1109        completion_span_cycles: control.completion_span_cycles,
1110        payload_cycles: control.payload_cycles,
1111        checksum: data.checksum,
1112    };
1113    let d = BenchmarkEngineWithSignerD::<S> {
1114        signer: signer_input,
1115        control,
1116        data,
1117    };
1118    (output, d)
1119}
1120
1121/// Const-generic inline-signer benchmark engine retained for source compatibility.
1122#[allow(type_alias_bounds)] // Bounds make the adapter well formed; the target repeats them.
1123pub type BenchmarkEngine<
1124    const ROUNDS: usize,
1125    const ADDRESS_BITS: usize,
1126    L: CompressionLaneComponent + Default = InlineCompressionLane<ROUNDS, ADDRESS_BITS>,
1127> where
1128    rhdl::bits::W<ROUNDS>: BitWidth,
1129    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
1130= BenchmarkEngineWithSigner<InlineBenchmarkSigner<ROUNDS, ADDRESS_BITS, L>>;
1131
1132/// Production inline-lane benchmark engine.
1133pub type WotsSha256BenchmarkEngine = BenchmarkEngine<64, 6>;
1134/// Production benchmark engine linked to the separately generated signer top.
1135pub type ModularWotsSha256BenchmarkEngine =
1136    BenchmarkEngineWithSigner<ExternalModularSha256SignerTop>;
1137
1138/// Packed AXI4-Lite slave inputs consumed by the RHDL kernel adapter.
1139#[allow(clippy::struct_excessive_bools)] // Independent AXI channel handshakes define this ABI.
1140#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
1141pub struct AxiLiteBenchmarkInput {
1142    /// Write-address channel valid.
1143    pub aw_valid: bool,
1144    /// Byte address within the 4 KiB register aperture.
1145    pub aw_addr: b12,
1146    /// Write-data channel valid.
1147    pub w_valid: bool,
1148    /// Write data.
1149    pub w_data: b32,
1150    /// Per-byte write strobes.
1151    pub w_strb: b4,
1152    /// Host accepts the registered write response.
1153    pub b_ready: bool,
1154    /// Read-address channel valid.
1155    pub ar_valid: bool,
1156    /// Byte address within the 4 KiB register aperture.
1157    pub ar_addr: b12,
1158    /// Host accepts the registered read response.
1159    pub r_ready: bool,
1160}
1161
1162/// Packed AXI4-Lite slave outputs produced by the RHDL kernel adapter.
1163#[allow(clippy::struct_excessive_bools)] // Independent AXI channel handshakes.
1164#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
1165pub struct AxiLiteBenchmarkOutput {
1166    /// Write-address channel ready.
1167    pub aw_ready: bool,
1168    /// Write-data channel ready.
1169    pub w_ready: bool,
1170    /// Registered write response valid.
1171    pub b_valid: bool,
1172    /// Write response (`OKAY` or `SLVERR`).
1173    pub b_resp: b2,
1174    /// Read-address channel ready.
1175    pub ar_ready: bool,
1176    /// Registered read response valid.
1177    pub r_valid: bool,
1178    /// Registered read data, stable under backpressure.
1179    pub r_data: b32,
1180    /// Read response (`OKAY` or `SLVERR`).
1181    pub r_resp: b2,
1182    /// Level interrupt from enabled sticky event bits.
1183    pub interrupt: bool,
1184}
1185
1186/// Resettable AXI channel ownership and interrupt state.
1187#[allow(clippy::struct_excessive_bools)] // Each bit is an independent protocol owner.
1188#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
1189pub struct AxiLiteBenchmarkControl {
1190    /// One write address is buffered.
1191    pub aw_pending: bool,
1192    /// Buffered write address.
1193    pub aw_addr: b12,
1194    /// Engine/configuration lock state sampled when AW transferred.
1195    pub aw_locked_at_accept: bool,
1196    /// One write data beat is buffered.
1197    pub w_pending: bool,
1198    /// Buffered write data.
1199    pub w_data: b32,
1200    /// Buffered byte strobes.
1201    pub w_strb: b4,
1202    /// Engine/configuration lock state sampled when W transferred.
1203    pub w_locked_at_accept: bool,
1204    /// Registered write response is owned by the B channel.
1205    pub b_valid: bool,
1206    /// Registered write response.
1207    pub b_resp: b2,
1208    /// Registered read response is owned by the R channel.
1209    pub r_valid: bool,
1210    /// Registered read data.
1211    pub r_data: b32,
1212    /// Registered read response.
1213    pub r_resp: b2,
1214    /// `AP_START` is waiting for engine acceptance/rejection.
1215    pub start_pending: bool,
1216    /// Sticky `AP_DONE`, clear on `AP_CTRL` read.
1217    pub done: bool,
1218    /// Global interrupt enable.
1219    pub gie: bool,
1220    /// Done/fault interrupt enables.
1221    pub ier: b2,
1222    /// Done/fault sticky interrupt status.
1223    pub isr: b2,
1224    /// Prevent repeated fault-event assertion after software clears ISR.
1225    pub fault_seen: bool,
1226    /// Most recent invalid-launch reason.
1227    pub last_reject_code: b8,
1228}
1229
1230/// Mutable launch configuration.
1231#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
1232pub struct AxiLiteBenchmarkConfig {
1233    /// Requested batch.
1234    pub batch: b32,
1235    /// 192-bit nonce as six register words.
1236    pub nonce_prefix: [b32; BENCHMARK_NONCE_WORDS],
1237    /// Current base/next index; auto-advanced on accepted launch.
1238    pub base_index: b64,
1239}
1240
1241/// Merge one AXI write word according to byte strobes.
1242#[kernel]
1243#[allow(clippy::assign_op_pattern)] // Preserve the reviewed explicit bit-mask expression.
1244pub fn benchmark_merge_write_kernel(old: b32, new: b32, strb: b4) -> b32 {
1245    let mut mask = b32(0);
1246    if strb & b4(1) != b4(0) {
1247        mask = mask | b32(0x0000_00ff);
1248    }
1249    if strb & b4(2) != b4(0) {
1250        mask = mask | b32(0x0000_ff00);
1251    }
1252    if strb & b4(4) != b4(0) {
1253        mask = mask | b32(0x00ff_0000);
1254    }
1255    if strb & b4(8) != b4(0) {
1256        mask = mask | b32(0xff00_0000);
1257    }
1258    (old & !mask) | (new & mask)
1259}
1260
1261/// Select one half of a 64-bit counter.
1262#[kernel]
1263fn benchmark_counter_word_kernel(value: b64, high: bool) -> b32 {
1264    if high {
1265        (value >> 32).resize()
1266    } else {
1267        value.resize()
1268    }
1269}
1270
1271/// Convert one control predicate to a register bit without a Rust integer cast.
1272#[kernel]
1273fn benchmark_bool_word_kernel(value: bool) -> b32 {
1274    if value { b32(1) } else { b32(0) }
1275}
1276
1277/// Read-decode input assembled entirely from RHDL state.
1278#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
1279pub struct BenchmarkReadDecodeInput {
1280    /// Requested byte address.
1281    pub address: b12,
1282    /// Current `AP_START` state.
1283    pub start_pending: bool,
1284    /// Sticky `AP_DONE` state.
1285    pub done: bool,
1286    /// Global interrupt enable.
1287    pub gie: bool,
1288    /// Interrupt enables.
1289    pub ier: b2,
1290    /// Interrupt status.
1291    pub isr: b2,
1292    /// Mutable configuration.
1293    pub config: AxiLiteBenchmarkConfig,
1294    /// Most recent rejection code.
1295    pub last_reject_code: b8,
1296    /// Engine status/results.
1297    pub engine: BenchmarkEngineOutput,
1298}
1299
1300/// Read-decode result.
1301#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
1302pub struct BenchmarkReadDecodeOutput {
1303    /// Register exists and is word aligned.
1304    pub valid: bool,
1305    /// Selected read data.
1306    pub data: b32,
1307}
1308
1309/// Decode the stable benchmark register map.
1310#[kernel]
1311#[allow(clippy::too_many_lines)] // Explicit addresses are the auditable hardware ABI.
1312pub fn benchmark_read_decode_kernel(input: BenchmarkReadDecodeInput) -> BenchmarkReadDecodeOutput {
1313    let mut output = BenchmarkReadDecodeOutput {
1314        valid: true,
1315        data: b32(0),
1316    };
1317    match input.address {
1318        Bits::<12>(0x000) => {
1319            output.data = benchmark_bool_word_kernel(input.start_pending)
1320                | (benchmark_bool_word_kernel(input.done) << 1)
1321                | (benchmark_bool_word_kernel(!input.engine.busy && !input.start_pending) << 2)
1322                | (benchmark_bool_word_kernel(input.engine.launch_ready && !input.start_pending)
1323                    << 3);
1324        }
1325        Bits::<12>(0x004) => output.data = benchmark_bool_word_kernel(input.gie),
1326        Bits::<12>(0x008) => output.data = input.ier.resize(),
1327        Bits::<12>(0x00c) => output.data = input.isr.resize(),
1328        Bits::<12>(0x010) => output.data = input.config.batch,
1329        Bits::<12>(0x014) => output.data = b32(0x0001_0000),
1330        Bits::<12>(0x018) => output.data = input.config.nonce_prefix[0],
1331        Bits::<12>(0x01c) => output.data = input.config.nonce_prefix[1],
1332        Bits::<12>(0x020) => output.data = input.config.nonce_prefix[2],
1333        Bits::<12>(0x024) => output.data = input.config.nonce_prefix[3],
1334        Bits::<12>(0x028) => output.data = input.config.nonce_prefix[4],
1335        Bits::<12>(0x02c) => output.data = input.config.nonce_prefix[5],
1336        Bits::<12>(0x030) => {
1337            output.data = benchmark_counter_word_kernel(input.config.base_index, false);
1338        }
1339        Bits::<12>(0x034) => {
1340            output.data = benchmark_counter_word_kernel(input.config.base_index, true);
1341        }
1342        Bits::<12>(0x038) => {
1343            output.data = benchmark_counter_word_kernel(input.engine.run_base_index, false);
1344        }
1345        Bits::<12>(0x03c) => {
1346            output.data = benchmark_counter_word_kernel(input.engine.run_base_index, true);
1347        }
1348        Bits::<12>(0x040) => {
1349            output.data = benchmark_bool_word_kernel(input.engine.busy)
1350                | (benchmark_bool_word_kernel(input.engine.fault) << 1)
1351                | (benchmark_bool_word_kernel(input.last_reject_code != b8(0)) << 2)
1352                | (benchmark_bool_word_kernel(input.engine.fault_code == b8(0x10)) << 3)
1353                | (benchmark_bool_word_kernel(
1354                    input.engine.fault_code != b8(0) && input.engine.fault_code != b8(0x10),
1355                ) << 4);
1356        }
1357        Bits::<12>(0x044) => {
1358            output.data = input.engine.fault_code.resize::<32>()
1359                | (input.last_reject_code.resize::<32>() << 8);
1360        }
1361        Bits::<12>(0x048) => {
1362            output.data = benchmark_counter_word_kernel(input.engine.accepted, false);
1363        }
1364        Bits::<12>(0x04c) => {
1365            output.data = benchmark_counter_word_kernel(input.engine.accepted, true);
1366        }
1367        Bits::<12>(0x050) => {
1368            output.data = benchmark_counter_word_kernel(input.engine.completed, false);
1369        }
1370        Bits::<12>(0x054) => {
1371            output.data = benchmark_counter_word_kernel(input.engine.completed, true);
1372        }
1373        Bits::<12>(0x058) => {
1374            output.data = benchmark_counter_word_kernel(input.engine.beats, false);
1375        }
1376        Bits::<12>(0x05c) => {
1377            output.data = benchmark_counter_word_kernel(input.engine.beats, true);
1378        }
1379        Bits::<12>(0x060) => {
1380            output.data = benchmark_counter_word_kernel(input.engine.errors, false);
1381        }
1382        Bits::<12>(0x064) => {
1383            output.data = benchmark_counter_word_kernel(input.engine.errors, true);
1384        }
1385        Bits::<12>(0x068) => {
1386            output.data = benchmark_counter_word_kernel(input.engine.elapsed_cycles, false);
1387        }
1388        Bits::<12>(0x06c) => {
1389            output.data = benchmark_counter_word_kernel(input.engine.elapsed_cycles, true);
1390        }
1391        Bits::<12>(0x070) => {
1392            output.data = benchmark_counter_word_kernel(input.engine.first_completion_cycle, false);
1393        }
1394        Bits::<12>(0x074) => {
1395            output.data = benchmark_counter_word_kernel(input.engine.first_completion_cycle, true);
1396        }
1397        Bits::<12>(0x078) => {
1398            output.data = benchmark_counter_word_kernel(input.engine.last_completion_cycle, false);
1399        }
1400        Bits::<12>(0x07c) => {
1401            output.data = benchmark_counter_word_kernel(input.engine.last_completion_cycle, true);
1402        }
1403        Bits::<12>(0x080) => {
1404            output.data = benchmark_counter_word_kernel(input.engine.completion_span_cycles, false);
1405        }
1406        Bits::<12>(0x084) => {
1407            output.data = benchmark_counter_word_kernel(input.engine.completion_span_cycles, true);
1408        }
1409        Bits::<12>(0x088) => {
1410            output.data = benchmark_counter_word_kernel(input.engine.payload_cycles, false);
1411        }
1412        Bits::<12>(0x08c) => {
1413            output.data = benchmark_counter_word_kernel(input.engine.payload_cycles, true);
1414        }
1415        Bits::<12>(0x090) => output.data = input.engine.checksum[0],
1416        Bits::<12>(0x094) => output.data = input.engine.checksum[1],
1417        Bits::<12>(0x098) => output.data = input.engine.checksum[2],
1418        Bits::<12>(0x09c) => output.data = input.engine.checksum[3],
1419        Bits::<12>(0x0a0) => output.data = input.engine.checksum[4],
1420        Bits::<12>(0x0a4) => output.data = input.engine.checksum[5],
1421        Bits::<12>(0x0a8) => output.data = input.engine.checksum[6],
1422        Bits::<12>(0x0ac) => output.data = input.engine.checksum[7],
1423        Bits::<12>(0x0b0) => output.data = input.engine.checksum[8],
1424        Bits::<12>(0x0b4) => output.data = input.engine.checksum[9],
1425        Bits::<12>(0x0b8) => output.data = input.engine.checksum[10],
1426        Bits::<12>(0x0bc) => output.data = input.engine.checksum[11],
1427        Bits::<12>(0x0c0) => output.data = input.engine.checksum[12],
1428        Bits::<12>(0x0c4) => output.data = input.engine.checksum[13],
1429        Bits::<12>(0x0c8) => output.data = input.engine.checksum[14],
1430        Bits::<12>(0x0cc) => output.data = input.engine.checksum[15],
1431        _ => output.valid = false,
1432    }
1433    output
1434}
1435
1436/// AXI4-Lite-only benchmark kernel over an injected signer component.
1437#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
1438pub struct AxiLiteBenchmarkKernelWithSigner<S = InlineBenchmarkSigner<64, 6>>
1439where
1440    S: BenchmarkSignerComponent,
1441{
1442    engine: BenchmarkEngineWithSigner<S>,
1443    control: DFF<AxiLiteBenchmarkControl>,
1444    config: DFF<AxiLiteBenchmarkConfig>,
1445}
1446
1447impl<S> Default for AxiLiteBenchmarkKernelWithSigner<S>
1448where
1449    S: BenchmarkSignerComponent,
1450{
1451    fn default() -> Self {
1452        Self {
1453            engine: BenchmarkEngineWithSigner::default(),
1454            control: DFF::new(AxiLiteBenchmarkControl::default()),
1455            config: DFF::new(AxiLiteBenchmarkConfig::default()),
1456        }
1457    }
1458}
1459
1460impl<S> SynchronousIO for AxiLiteBenchmarkKernelWithSigner<S>
1461where
1462    S: BenchmarkSignerComponent,
1463{
1464    type I = AxiLiteBenchmarkInput;
1465    type O = AxiLiteBenchmarkOutput;
1466    type Kernel = axi_lite_benchmark_kernel<S>;
1467}
1468
1469/// State transition for [`AxiLiteBenchmarkKernelWithSigner`].
1470#[kernel]
1471#[allow(clippy::assign_op_pattern, clippy::too_many_lines)] // Preserve the reviewed explicit AXI state-update source shape.
1472pub fn axi_lite_benchmark_kernel<S: BenchmarkSignerComponent>(
1473    clock_reset: ClockReset,
1474    input: AxiLiteBenchmarkInput,
1475    q: AxiLiteBenchmarkKernelWithSignerQ<S>,
1476) -> (AxiLiteBenchmarkOutput, AxiLiteBenchmarkKernelWithSignerD<S>) {
1477    let resetting = clock_reset.reset.any();
1478    let aw_ready = !resetting && !q.control.aw_pending && !q.control.b_valid;
1479    let w_ready = !resetting && !q.control.w_pending && !q.control.b_valid;
1480    let ar_ready = !resetting && !q.control.r_valid;
1481    let mut control = q.control;
1482    let mut config = q.config;
1483
1484    if q.control.b_valid && input.b_ready {
1485        control.b_valid = false;
1486    }
1487    if q.control.r_valid && input.r_ready {
1488        control.r_valid = false;
1489    }
1490    if input.aw_valid && aw_ready {
1491        control.aw_pending = true;
1492        control.aw_addr = input.aw_addr;
1493        control.aw_locked_at_accept = q.engine.busy || q.control.start_pending;
1494    }
1495    if input.w_valid && w_ready {
1496        control.w_pending = true;
1497        control.w_data = input.w_data;
1498        control.w_strb = input.w_strb;
1499        control.w_locked_at_accept = q.engine.busy || q.control.start_pending;
1500    }
1501
1502    if q.control.aw_pending && q.control.w_pending && !q.control.b_valid {
1503        let locked = q.control.aw_locked_at_accept
1504            || q.control.w_locked_at_accept
1505            || q.engine.busy
1506            || q.control.start_pending;
1507        let mut write_valid = true;
1508        match q.control.aw_addr {
1509            Bits::<12>(0x000) => {
1510                if q.control.w_strb & b4(1) != b4(0) && q.control.w_data & b32(1) != b32(0) {
1511                    if locked || !q.engine.launch_ready {
1512                        write_valid = false;
1513                    } else {
1514                        control.start_pending = true;
1515                    }
1516                }
1517            }
1518            Bits::<12>(0x004) => {
1519                if q.control.w_strb & b4(1) != b4(0) {
1520                    control.gie = q.control.w_data & b32(1) != b32(0);
1521                }
1522            }
1523            Bits::<12>(0x008) => {
1524                if q.control.w_strb & b4(1) != b4(0) {
1525                    control.ier = q.control.w_data.resize();
1526                }
1527            }
1528            Bits::<12>(0x00c) => {
1529                if q.control.w_strb & b4(1) != b4(0) {
1530                    control.isr = q.control.isr ^ q.control.w_data.resize::<2>();
1531                }
1532            }
1533            Bits::<12>(0x010) => {
1534                if locked {
1535                    write_valid = false;
1536                } else {
1537                    config.batch = benchmark_merge_write_kernel(
1538                        q.config.batch,
1539                        q.control.w_data,
1540                        q.control.w_strb,
1541                    );
1542                }
1543            }
1544            Bits::<12>(0x018) => {
1545                if locked {
1546                    write_valid = false;
1547                } else {
1548                    config.nonce_prefix[0] = benchmark_merge_write_kernel(
1549                        q.config.nonce_prefix[0],
1550                        q.control.w_data,
1551                        q.control.w_strb,
1552                    );
1553                }
1554            }
1555            Bits::<12>(0x01c) => {
1556                if locked {
1557                    write_valid = false;
1558                } else {
1559                    config.nonce_prefix[1] = benchmark_merge_write_kernel(
1560                        q.config.nonce_prefix[1],
1561                        q.control.w_data,
1562                        q.control.w_strb,
1563                    );
1564                }
1565            }
1566            Bits::<12>(0x020) => {
1567                if locked {
1568                    write_valid = false;
1569                } else {
1570                    config.nonce_prefix[2] = benchmark_merge_write_kernel(
1571                        q.config.nonce_prefix[2],
1572                        q.control.w_data,
1573                        q.control.w_strb,
1574                    );
1575                }
1576            }
1577            Bits::<12>(0x024) => {
1578                if locked {
1579                    write_valid = false;
1580                } else {
1581                    config.nonce_prefix[3] = benchmark_merge_write_kernel(
1582                        q.config.nonce_prefix[3],
1583                        q.control.w_data,
1584                        q.control.w_strb,
1585                    );
1586                }
1587            }
1588            Bits::<12>(0x028) => {
1589                if locked {
1590                    write_valid = false;
1591                } else {
1592                    config.nonce_prefix[4] = benchmark_merge_write_kernel(
1593                        q.config.nonce_prefix[4],
1594                        q.control.w_data,
1595                        q.control.w_strb,
1596                    );
1597                }
1598            }
1599            Bits::<12>(0x02c) => {
1600                if locked {
1601                    write_valid = false;
1602                } else {
1603                    config.nonce_prefix[5] = benchmark_merge_write_kernel(
1604                        q.config.nonce_prefix[5],
1605                        q.control.w_data,
1606                        q.control.w_strb,
1607                    );
1608                }
1609            }
1610            Bits::<12>(0x030) => {
1611                if locked {
1612                    write_valid = false;
1613                } else {
1614                    let low = benchmark_merge_write_kernel(
1615                        q.config.base_index.resize(),
1616                        q.control.w_data,
1617                        q.control.w_strb,
1618                    );
1619                    config.base_index =
1620                        (q.config.base_index & b64(0xffff_ffff_0000_0000)) | low.resize::<64>();
1621                }
1622            }
1623            Bits::<12>(0x034) => {
1624                if locked {
1625                    write_valid = false;
1626                } else {
1627                    let old_high: b32 = (q.config.base_index >> 32).resize();
1628                    let high =
1629                        benchmark_merge_write_kernel(old_high, q.control.w_data, q.control.w_strb);
1630                    config.base_index = (q.config.base_index & b64(0x0000_0000_ffff_ffff))
1631                        | (high.resize::<64>() << 32);
1632                }
1633            }
1634            _ => write_valid = false,
1635        }
1636        control.aw_pending = false;
1637        control.aw_locked_at_accept = false;
1638        control.w_pending = false;
1639        control.w_locked_at_accept = false;
1640        control.b_valid = true;
1641        control.b_resp = if write_valid { b2(0) } else { b2(2) };
1642    }
1643
1644    if input.ar_valid && ar_ready {
1645        let decoded = benchmark_read_decode_kernel(BenchmarkReadDecodeInput {
1646            address: input.ar_addr,
1647            start_pending: q.control.start_pending,
1648            done: q.control.done,
1649            gie: q.control.gie,
1650            ier: q.control.ier,
1651            isr: q.control.isr,
1652            config: q.config,
1653            last_reject_code: q.control.last_reject_code,
1654            engine: q.engine,
1655        });
1656        control.r_valid = true;
1657        control.r_data = decoded.data;
1658        control.r_resp = if decoded.valid { b2(0) } else { b2(2) };
1659        if input.ar_addr == b12(0) {
1660            control.done = false;
1661        }
1662    }
1663
1664    if q.engine.launch_accepted {
1665        control.start_pending = false;
1666        control.last_reject_code = b8(0);
1667        config.base_index = q.engine.next_base_index;
1668    }
1669    if q.engine.launch_rejected {
1670        control.start_pending = false;
1671        control.last_reject_code = q.engine.reject_code;
1672    }
1673    if q.engine.done {
1674        control.done = true;
1675        control.isr = control.isr | b2(1);
1676    }
1677    if q.engine.fault && !q.control.fault_seen {
1678        control.fault_seen = true;
1679        control.isr = control.isr | b2(2);
1680    }
1681
1682    let engine_input = BenchmarkEngineInput {
1683        launch_valid: q.control.start_pending,
1684        batch: q.config.batch,
1685        nonce_prefix: q.config.nonce_prefix,
1686        base_index: q.config.base_index,
1687    };
1688
1689    if resetting {
1690        control = AxiLiteBenchmarkControl::default();
1691        config = AxiLiteBenchmarkConfig::default();
1692    }
1693
1694    let output = AxiLiteBenchmarkOutput {
1695        aw_ready,
1696        w_ready,
1697        b_valid: !resetting && q.control.b_valid,
1698        b_resp: q.control.b_resp,
1699        ar_ready,
1700        r_valid: !resetting && q.control.r_valid,
1701        r_data: q.control.r_data,
1702        r_resp: q.control.r_resp,
1703        interrupt: !resetting && q.control.gie && (q.control.isr & q.control.ier) != b2(0),
1704    };
1705    let d = AxiLiteBenchmarkKernelWithSignerD::<S> {
1706        engine: engine_input,
1707        control,
1708        config,
1709    };
1710    (output, d)
1711}
1712
1713/// Const-generic inline-signer AXI kernel retained for source compatibility.
1714#[allow(type_alias_bounds)] // Bounds make the adapter well formed; the target repeats them.
1715pub type AxiLiteBenchmarkKernel<
1716    const ROUNDS: usize,
1717    const ADDRESS_BITS: usize,
1718    L: CompressionLaneComponent + Default = InlineCompressionLane<ROUNDS, ADDRESS_BITS>,
1719> where
1720    rhdl::bits::W<ROUNDS>: BitWidth,
1721    rhdl::bits::W<ADDRESS_BITS>: BitWidth,
1722= AxiLiteBenchmarkKernelWithSigner<InlineBenchmarkSigner<ROUNDS, ADDRESS_BITS, L>>;
1723
1724/// Production inline-lane AXI4-Lite benchmark kernel.
1725pub type WotsSha256AxiLiteBenchmarkKernel = AxiLiteBenchmarkKernel<64, 6>;
1726/// Production AXI4-Lite benchmark linked to the external RHDL signer top.
1727pub type ModularWotsSha256AxiLiteBenchmarkKernel =
1728    AxiLiteBenchmarkKernelWithSigner<ExternalModularSha256SignerTop>;
1729
1730#[cfg(test)]
1731mod tests {
1732    use super::*;
1733    use crate::LaneLocalBeat;
1734
1735    fn raw8(value: b8) -> u8 {
1736        value.raw().to_le_bytes()[0]
1737    }
1738
1739    fn raw4(value: b4) -> usize {
1740        usize::from(value.raw().to_le_bytes()[0] & 0x0f)
1741    }
1742
1743    fn raw32(value: b32) -> u32 {
1744        let bytes = value.raw().to_le_bytes();
1745        u32::from_le_bytes(bytes[..4].try_into().expect("four bytes"))
1746    }
1747
1748    #[test]
1749    fn external_signer_descriptor_is_typed_and_unresolved() -> Result<(), RHDLError> {
1750        let descriptor =
1751            ExternalModularSha256SignerTop.descriptor("benchmark_external_signer_probe".into())?;
1752        assert_eq!(
1753            descriptor.name.to_string(),
1754            "benchmark_external_signer_probe"
1755        );
1756        assert_eq!(descriptor.input_kind, SignerTopInput::static_kind());
1757        assert_eq!(
1758            descriptor.output_kind,
1759            crate::SignerTopOutput::static_kind()
1760        );
1761        assert!(descriptor.hdl().is_err());
1762        assert!(descriptor.hdl_unchecked()?.modules.modules.is_empty());
1763
1764        let wrapper = descriptor
1765            .netlist()?
1766            .as_vlog("benchmark_external_signer_probe")?
1767            .modules
1768            .pretty();
1769        assert!(wrapper.contains(&format!("{BENCHMARK_SIGNER_TOP_MODULE} bb_0(")));
1770        assert!(!wrapper.contains(&format!("module {BENCHMARK_SIGNER_TOP_MODULE}(")));
1771        Ok(())
1772    }
1773
1774    #[test]
1775    fn external_signer_simulation_delegates_to_the_exact_modular_signer() {
1776        let external = ExternalModularSha256SignerTop;
1777        let native = SignerTop::<64, 6, ExternalFullCompressionLane>::default();
1778        let mut external_state = external.init();
1779        let mut native_state = native.init();
1780        for clock_reset in [
1781            clock_reset(clock(false), reset(true)),
1782            clock_reset(clock(false), reset(false)),
1783        ] {
1784            let input = SignerTopInput::default();
1785            assert_eq!(
1786                external.sim(clock_reset, input, &mut external_state),
1787                native.sim(clock_reset, input, &mut native_state)
1788            );
1789            assert!(external_state == native_state);
1790        }
1791    }
1792
1793    fn live_job(job_id: u32, cluster: u8, frame_started: bool) -> BenchmarkLiveJob {
1794        BenchmarkLiveJob {
1795            live: true,
1796            job_id: b32(job_id as u128),
1797            cluster: b2(cluster as u128),
1798            frame_started,
1799        }
1800    }
1801
1802    fn active_engine_q(batch: u32) -> BenchmarkEngineWithSignerQ<InlineBenchmarkSigner<4, 2>> {
1803        BenchmarkEngineWithSignerQ::<InlineBenchmarkSigner<4, 2>> {
1804            signer: crate::SignerTopOutput::default(),
1805            control: BenchmarkEngineControl {
1806                busy: true,
1807                ..BenchmarkEngineControl::default()
1808            },
1809            data: BenchmarkEngineData {
1810                batch: b32(batch as u128),
1811                ..BenchmarkEngineData::default()
1812            },
1813        }
1814    }
1815
1816    fn engine_transition(
1817        mut q: BenchmarkEngineWithSignerQ<InlineBenchmarkSigner<4, 2>>,
1818        signer: crate::SignerTopOutput,
1819    ) -> (
1820        BenchmarkEngineOutput,
1821        BenchmarkEngineControl,
1822        BenchmarkEngineData,
1823    ) {
1824        q.signer = signer;
1825        let (output, d) = benchmark_engine_kernel::<InlineBenchmarkSigner<4, 2>>(
1826            clock_reset(clock(false), reset(false)),
1827            BenchmarkEngineInput::default(),
1828            q,
1829        );
1830        (output, d.control, d.data)
1831    }
1832
1833    fn axi_q(
1834        engine: BenchmarkEngineOutput,
1835        control: AxiLiteBenchmarkControl,
1836        config: AxiLiteBenchmarkConfig,
1837    ) -> AxiLiteBenchmarkKernelWithSignerQ<InlineBenchmarkSigner<4, 2>> {
1838        AxiLiteBenchmarkKernelWithSignerQ::<InlineBenchmarkSigner<4, 2>> {
1839            engine,
1840            control,
1841            config,
1842        }
1843    }
1844
1845    fn axi_transition(
1846        q: AxiLiteBenchmarkKernelWithSignerQ<InlineBenchmarkSigner<4, 2>>,
1847        input: AxiLiteBenchmarkInput,
1848    ) -> (
1849        AxiLiteBenchmarkOutput,
1850        AxiLiteBenchmarkControl,
1851        AxiLiteBenchmarkConfig,
1852    ) {
1853        let (output, d) = axi_lite_benchmark_kernel::<InlineBenchmarkSigner<4, 2>>(
1854            clock_reset(clock(false), reset(false)),
1855            input,
1856            q,
1857        );
1858        (output, d.control, d.config)
1859    }
1860
1861    #[test]
1862    fn launch_validation_reserves_a_representable_exclusive_end() {
1863        let ordinary = benchmark_launch_validation_kernel(BenchmarkLaunchValidationInput {
1864            batch: b32(17),
1865            base_index: b64(41),
1866        });
1867        assert!(ordinary.valid);
1868        assert_eq!(ordinary.next_base_index, b64(58));
1869        assert_eq!(ordinary.reject_code, b8(BENCHMARK_REJECT_NONE as u128));
1870
1871        let zero = benchmark_launch_validation_kernel(BenchmarkLaunchValidationInput {
1872            batch: b32(0),
1873            base_index: b64(41),
1874        });
1875        assert!(!zero.valid);
1876        assert_eq!(zero.reject_code, b8(BENCHMARK_REJECT_ZERO_BATCH as u128));
1877
1878        let overflow = benchmark_launch_validation_kernel(BenchmarkLaunchValidationInput {
1879            batch: b32(2),
1880            base_index: b64(u64::MAX as u128 - 1),
1881        });
1882        assert!(!overflow.valid);
1883        assert_eq!(
1884            overflow.reject_code,
1885            b8(BENCHMARK_REJECT_INDEX_OVERFLOW as u128)
1886        );
1887    }
1888
1889    #[test]
1890    fn seed_byte_order_is_exact_and_batch_material_is_injective() {
1891        let nonce_prefix = [
1892            b32(0x0302_0100),
1893            b32(0x0706_0504),
1894            b32(0x0b0a_0908),
1895            b32(0x0f0e_0d0c),
1896            b32(0x1312_1110),
1897            b32(0x1716_1514),
1898        ];
1899        let first = benchmark_job_material_kernel(nonce_prefix, b64(0x1829_3a4b_5c6d_7e8f));
1900        let seed = first.private_seed.map(raw8);
1901        assert_eq!(
1902            seed[..24],
1903            core::array::from_fn::<_, 24, _>(|index| index as u8)
1904        );
1905        assert_eq!(&seed[24..], &0x1829_3a4b_5c6d_7e8f_u64.to_be_bytes());
1906        assert_eq!(
1907            first.message.map(raw8),
1908            seed.map(|byte| byte ^ 0xa5),
1909            "message uses the documented test-only domain transform"
1910        );
1911
1912        let mut seeds = std::collections::BTreeSet::new();
1913        for offset in 0..257_u64 {
1914            let material = benchmark_job_material_kernel(nonce_prefix, b64(9_000 + offset as u128));
1915            assert!(seeds.insert(material.private_seed.map(raw8)));
1916        }
1917    }
1918
1919    fn canonical_output(job_id: u32, cluster: u8, beat: usize) -> SignerOutputBeat {
1920        let mut data = [b8(0); 64];
1921        for (lane, byte) in data.iter_mut().enumerate() {
1922            *byte = b8(((job_id as usize + beat * 17 + lane * 29) & 0xff) as u128);
1923        }
1924        let final_beat = beat == 34;
1925        if final_beat {
1926            for byte in &mut data[32..] {
1927                *byte = b8(0);
1928            }
1929        }
1930        SignerOutputBeat {
1931            beat: LaneLocalBeat {
1932                data,
1933                keep: if final_beat {
1934                    b64(0xffff_ffff)
1935                } else {
1936                    b64(u64::MAX as u128)
1937                },
1938                last: final_beat,
1939                job_id: b32(job_id as u128),
1940            },
1941            cluster: b2(cluster as u128),
1942        }
1943    }
1944
1945    #[test]
1946    fn independent_frame_checker_enforces_all_thirty_five_beats() {
1947        let mut active = false;
1948        let mut job_id = b32(0);
1949        let mut cluster = b2(0);
1950        for beat in 0..SIGNER_FRAME_BEATS {
1951            let output = canonical_output(7, 2, beat);
1952            assert!(benchmark_frame_is_canonical_kernel(
1953                BenchmarkFrameValidationInput {
1954                    output,
1955                    frame_active: active,
1956                    expected_beat: b6(beat as u128),
1957                    frame_job_id: job_id,
1958                    frame_cluster: cluster,
1959                    batch: b32(8),
1960                }
1961            ));
1962            if beat == 0 {
1963                active = true;
1964                job_id = output.beat.job_id;
1965                cluster = output.cluster;
1966            }
1967        }
1968
1969        let mut wrong_keep = canonical_output(7, 2, 34);
1970        wrong_keep.beat.keep = b64(u64::MAX as u128);
1971        assert!(!benchmark_frame_is_canonical_kernel(
1972            BenchmarkFrameValidationInput {
1973                output: wrong_keep,
1974                frame_active: true,
1975                expected_beat: b6(34),
1976                frame_job_id: b32(7),
1977                frame_cluster: b2(2),
1978                batch: b32(8),
1979            }
1980        ));
1981
1982        let wrong_job = canonical_output(8, 2, 1);
1983        assert!(!benchmark_frame_is_canonical_kernel(
1984            BenchmarkFrameValidationInput {
1985                output: wrong_job,
1986                frame_active: true,
1987                expected_beat: b6(1),
1988                frame_job_id: b32(7),
1989                frame_cluster: b2(2),
1990                batch: b32(8),
1991            }
1992        ));
1993    }
1994
1995    #[test]
1996    fn checksum_fold_is_wide_deterministic_and_identity_sensitive() {
1997        let initial = [b32(0); BENCHMARK_CHECKSUM_WORDS];
1998        let output = canonical_output(3, 1, 0);
1999        let first = benchmark_checksum_kernel(BenchmarkChecksumInput {
2000            checksum: initial,
2001            output,
2002            beat_index: b6(0),
2003        });
2004        assert!(first.iter().any(|word| *word != b32(0)));
2005        assert_eq!(
2006            first,
2007            benchmark_checksum_kernel(BenchmarkChecksumInput {
2008                checksum: initial,
2009                output,
2010                beat_index: b6(0),
2011            })
2012        );
2013        let different_identity = benchmark_checksum_kernel(BenchmarkChecksumInput {
2014            checksum: initial,
2015            output,
2016            beat_index: b6(1),
2017        });
2018        assert_ne!(first, different_identity);
2019    }
2020
2021    #[test]
2022    fn exact_twelve_entry_scoreboard_rejects_duplicates_and_cluster_overfill() {
2023        let mut live_jobs = [BenchmarkLiveJob::default(); BENCHMARK_LIVE_JOB_SLOTS];
2024        for job_id in 0..12_u32 {
2025            let cluster = (job_id % 3) as u8;
2026            let admission =
2027                benchmark_scoreboard_admission_kernel(BenchmarkScoreboardAdmissionInput {
2028                    live_jobs,
2029                    job_id: b32(job_id as u128),
2030                    cluster: b2(cluster as u128),
2031                });
2032            assert!(
2033                admission.valid,
2034                "job {job_id} must fit its four-slot partition"
2035            );
2036            let slot = raw4(admission.slot);
2037            assert_eq!(slot / LANE_LOCAL_CONTEXTS, usize::from(cluster));
2038            live_jobs[slot] = live_job(job_id, cluster, false);
2039        }
2040        let audit = benchmark_scoreboard_audit_kernel(BenchmarkScoreboardAuditInput { live_jobs });
2041        assert!(audit.valid);
2042        assert_eq!(audit.live_count, b4(12));
2043        assert!(!audit.empty);
2044
2045        let cluster_zero_full =
2046            benchmark_scoreboard_admission_kernel(BenchmarkScoreboardAdmissionInput {
2047                live_jobs,
2048                job_id: b32(12),
2049                cluster: b2(0),
2050            });
2051        assert!(!cluster_zero_full.valid);
2052        let duplicate = benchmark_scoreboard_admission_kernel(BenchmarkScoreboardAdmissionInput {
2053            live_jobs,
2054            job_id: b32(7),
2055            cluster: b2(1),
2056        });
2057        assert!(!duplicate.valid);
2058
2059        let owner = benchmark_scoreboard_lookup_kernel(BenchmarkScoreboardLookupInput {
2060            live_jobs,
2061            job_id: b32(10),
2062            cluster: b2(1),
2063        });
2064        assert!(owner.valid);
2065        assert_eq!(owner.match_count, b4(1));
2066
2067        let mut corrupt = live_jobs;
2068        corrupt[0].job_id = corrupt[1].job_id;
2069        let corrupt_owner = benchmark_scoreboard_lookup_kernel(BenchmarkScoreboardLookupInput {
2070            live_jobs: corrupt,
2071            job_id: corrupt[1].job_id,
2072            cluster: corrupt[1].cluster,
2073        });
2074        assert!(
2075            !corrupt_owner.valid,
2076            "duplicate live identity must fail closed"
2077        );
2078        assert_eq!(corrupt_owner.match_count, b4(2));
2079    }
2080
2081    #[test]
2082    fn injected_admission_frame_error_and_counter_faults_fail_closed() {
2083        let active_clock = clock_reset(clock(false), reset(false));
2084
2085        let mut admission_q = active_engine_q(5);
2086        admission_q.control.accepted = b64(4);
2087        for slot in 0..4 {
2088            admission_q.control.live_jobs[slot] = live_job(slot as u32, 0, false);
2089        }
2090        admission_q.signer.start_ready = true;
2091        admission_q.signer.start_accepted = true;
2092        admission_q.signer.start_cluster = b2(0);
2093        admission_q.signer.cluster_start_ready[0] = true;
2094        let (admission, _) = benchmark_engine_kernel::<InlineBenchmarkSigner<4, 2>>(
2095            active_clock,
2096            BenchmarkEngineInput::default(),
2097            admission_q,
2098        );
2099        assert!(admission.fault);
2100        assert_eq!(admission.fault_code, b8(BENCHMARK_FAULT_ADMISSION as u128));
2101
2102        let mut frame_q = active_engine_q(1);
2103        frame_q.signer.output_valid = true;
2104        frame_q.signer.output = canonical_output(0, 0, 0);
2105        let (frame, _) = benchmark_engine_kernel::<InlineBenchmarkSigner<4, 2>>(
2106            active_clock,
2107            BenchmarkEngineInput::default(),
2108            frame_q,
2109        );
2110        assert!(frame.fault, "premature frame has no live owner");
2111        assert_eq!(frame.fault_code, b8(BENCHMARK_FAULT_FRAME as u128));
2112
2113        let mut error_q = active_engine_q(1);
2114        error_q.signer.error_valid = true;
2115        error_q.signer.delivered_error = true;
2116        error_q.signer.error.job_id = b32(0);
2117        error_q.signer.error.cluster = b2(0);
2118        let (error, _) = benchmark_engine_kernel::<InlineBenchmarkSigner<4, 2>>(
2119            active_clock,
2120            BenchmarkEngineInput::default(),
2121            error_q,
2122        );
2123        assert!(error.fault, "premature error has no live owner");
2124        assert_eq!(error.fault_code, b8(BENCHMARK_FAULT_ERROR as u128));
2125
2126        let mut counter_q = active_engine_q(1);
2127        counter_q.control.accepted = b64(1);
2128        counter_q.control.completed = b64(1);
2129        counter_q.control.beats = b64(35);
2130        counter_q.control.live_jobs[0] = live_job(0, 0, false);
2131        let (counter, _) = benchmark_engine_kernel::<InlineBenchmarkSigner<4, 2>>(
2132            active_clock,
2133            BenchmarkEngineInput::default(),
2134            counter_q,
2135        );
2136        assert!(
2137            counter.fault,
2138            "terminal count with a missing retirement must fail"
2139        );
2140        assert_eq!(counter.fault_code, b8(BENCHMARK_FAULT_COUNTER as u128));
2141    }
2142
2143    #[test]
2144    fn duplicate_and_success_error_double_terminals_cannot_retire_twice() {
2145        let active_clock = clock_reset(clock(false), reset(false));
2146        let mut q = active_engine_q(1);
2147        q.control.accepted = b64(1);
2148        q.control.beats = b64(34);
2149        q.control.frame_active = true;
2150        q.control.expected_beat = b6(34);
2151        q.control.frame_job_id = b32(0);
2152        q.control.frame_cluster = b2(0);
2153        q.control.live_jobs[0] = live_job(0, 0, true);
2154        q.signer.output_valid = true;
2155        q.signer.output = canonical_output(0, 0, 34);
2156        q.signer.delivered_final = true;
2157        q.signer.error_valid = true;
2158        q.signer.error = crate::SignerErrorCompletion {
2159            job_id: b32(0),
2160            cluster: b2(0),
2161            context: b2(0),
2162        };
2163        q.signer.delivered_error = true;
2164        let (double, _) = benchmark_engine_kernel::<InlineBenchmarkSigner<4, 2>>(
2165            active_clock,
2166            BenchmarkEngineInput::default(),
2167            q,
2168        );
2169        assert!(double.fault);
2170        assert_eq!(double.fault_code, b8(BENCHMARK_FAULT_ERROR as u128));
2171        assert_eq!(double.completed, b64(1));
2172        assert_eq!(double.errors, b64(0));
2173
2174        let mut duplicate_q = active_engine_q(2);
2175        duplicate_q.control.accepted = b64(1);
2176        duplicate_q.control.completed = b64(1);
2177        duplicate_q.control.beats = b64(35);
2178        duplicate_q.control.frame_active = true;
2179        duplicate_q.control.expected_beat = b6(34);
2180        duplicate_q.control.frame_job_id = b32(0);
2181        duplicate_q.control.frame_cluster = b2(0);
2182        duplicate_q.signer.output_valid = true;
2183        duplicate_q.signer.output = canonical_output(0, 0, 34);
2184        duplicate_q.signer.delivered_final = true;
2185        let (duplicate, _) = benchmark_engine_kernel::<InlineBenchmarkSigner<4, 2>>(
2186            active_clock,
2187            BenchmarkEngineInput::default(),
2188            duplicate_q,
2189        );
2190        assert!(duplicate.fault, "a retired owner cannot complete again");
2191        assert_eq!(duplicate.fault_code, b8(BENCHMARK_FAULT_FRAME as u128));
2192    }
2193
2194    fn software_checksum_fold(
2195        checksum: &mut [u32; BENCHMARK_CHECKSUM_WORDS],
2196        output: SignerOutputBeat,
2197        beat: usize,
2198    ) {
2199        let cluster: b8 = output.cluster.resize();
2200        let identity = raw32(output.beat.job_id) ^ beat as u32 ^ u32::from(raw8(cluster));
2201        for (word, checksum_word) in checksum.iter_mut().enumerate() {
2202            let bytes = [
2203                raw8(output.beat.data[word * 4]),
2204                raw8(output.beat.data[word * 4 + 1]),
2205                raw8(output.beat.data[word * 4 + 2]),
2206                raw8(output.beat.data[word * 4 + 3]),
2207            ];
2208            *checksum_word ^= u32::from_le_bytes(bytes) ^ identity;
2209        }
2210    }
2211
2212    #[test]
2213    fn thirteen_synthetic_jobs_reuse_capacity_and_finish_with_exact_beat_oracle() {
2214        let mut q = active_engine_q(13);
2215        let mut software_checksum = [0_u32; BENCHMARK_CHECKSUM_WORDS];
2216
2217        for job_id in 0..12_u32 {
2218            let cluster = (job_id % 3) as usize;
2219            let mut signer = crate::SignerTopOutput::default();
2220            signer.start_ready = true;
2221            signer.start_accepted = true;
2222            signer.start_cluster = b2(cluster as u128);
2223            signer.cluster_start_ready[cluster] = true;
2224            let (output, control, data) = engine_transition(q, signer);
2225            assert!(!output.fault, "admitting job {job_id}");
2226            q = BenchmarkEngineWithSignerQ::<InlineBenchmarkSigner<4, 2>> {
2227                signer: crate::SignerTopOutput::default(),
2228                control,
2229                data,
2230            };
2231        }
2232        assert_eq!(q.control.accepted, b64(12));
2233        assert_eq!(
2234            benchmark_scoreboard_audit_kernel(BenchmarkScoreboardAuditInput {
2235                live_jobs: q.control.live_jobs,
2236            })
2237            .live_count,
2238            b4(12)
2239        );
2240
2241        let mut terminal = BenchmarkEngineOutput::default();
2242        for job_id in 0..13_u32 {
2243            if job_id == 1 {
2244                let mut signer = crate::SignerTopOutput::default();
2245                signer.start_ready = true;
2246                signer.start_accepted = true;
2247                signer.start_cluster = b2(0);
2248                signer.cluster_start_ready[0] = true;
2249                let (output, control, data) = engine_transition(q, signer);
2250                assert!(
2251                    !output.fault,
2252                    "thirteenth job reuses the retired cluster-zero slot"
2253                );
2254                q = BenchmarkEngineWithSignerQ::<InlineBenchmarkSigner<4, 2>> {
2255                    signer: crate::SignerTopOutput::default(),
2256                    control,
2257                    data,
2258                };
2259            }
2260            let actual_job = if job_id == 0 { 0 } else { job_id };
2261            let cluster = (actual_job % 3) as u8;
2262            for beat in 0..SIGNER_FRAME_BEATS {
2263                let frame = canonical_output(actual_job, cluster, beat);
2264                software_checksum_fold(&mut software_checksum, frame, beat);
2265                let mut signer = crate::SignerTopOutput::default();
2266                signer.output_valid = true;
2267                signer.output = frame;
2268                signer.delivered_final = beat + 1 == SIGNER_FRAME_BEATS;
2269                let (output, control, data) = engine_transition(q, signer);
2270                assert!(!output.fault, "job {actual_job}, beat {beat}");
2271                terminal = output;
2272                q = BenchmarkEngineWithSignerQ::<InlineBenchmarkSigner<4, 2>> {
2273                    signer: crate::SignerTopOutput::default(),
2274                    control,
2275                    data,
2276                };
2277            }
2278        }
2279
2280        assert!(terminal.done);
2281        assert!(!terminal.busy);
2282        assert_eq!(terminal.accepted, b64(13));
2283        assert_eq!(terminal.completed, b64(13));
2284        assert_eq!(terminal.errors, b64(0));
2285        assert_eq!(terminal.beats, b64((13 * SIGNER_FRAME_BEATS) as u128));
2286        assert!(terminal.completion_span_cycles > b64(0));
2287        assert_eq!(terminal.checksum.map(raw32), software_checksum);
2288        assert!(
2289            benchmark_scoreboard_audit_kernel(BenchmarkScoreboardAuditInput {
2290                live_jobs: q.control.live_jobs,
2291            })
2292            .empty
2293        );
2294    }
2295
2296    #[test]
2297    fn nonce_index_payload_matches_an_independent_software_oracle() {
2298        let nonce_prefix = [
2299            b32(0x0123_4567),
2300            b32(0x89ab_cdef),
2301            b32(0x1020_3040),
2302            b32(0x5060_7080),
2303            b32(0x90a0_b0c0),
2304            b32(0xd0e0_f000),
2305        ];
2306        let software_nonce = nonce_prefix.map(raw32);
2307        for offset in 0..33_u64 {
2308            let index = 0x0102_0304_ffff_ff00_u64 + offset;
2309            let hardware = benchmark_job_material_kernel(nonce_prefix, b64(index as u128));
2310            let mut seed = [0_u8; 32];
2311            for (word, value) in software_nonce.iter().enumerate() {
2312                seed[word * 4..word * 4 + 4].copy_from_slice(&value.to_le_bytes());
2313            }
2314            seed[24..].copy_from_slice(&index.to_be_bytes());
2315            assert_eq!(hardware.private_seed.map(raw8), seed);
2316            assert_eq!(hardware.message.map(raw8), seed.map(|byte| byte ^ 0xa5));
2317        }
2318    }
2319
2320    #[test]
2321    fn axi_w_before_aw_and_simultaneous_aw_w_preserve_acceptance_state() {
2322        let busy_engine = BenchmarkEngineOutput {
2323            busy: true,
2324            ..BenchmarkEngineOutput::default()
2325        };
2326        let initial_config = AxiLiteBenchmarkConfig {
2327            batch: b32(7),
2328            ..AxiLiteBenchmarkConfig::default()
2329        };
2330        let (_output, w_control, w_config) = axi_transition(
2331            axi_q(
2332                busy_engine,
2333                AxiLiteBenchmarkControl::default(),
2334                initial_config,
2335            ),
2336            AxiLiteBenchmarkInput {
2337                w_valid: true,
2338                w_data: b32(99),
2339                w_strb: b4(0xf),
2340                ..AxiLiteBenchmarkInput::default()
2341            },
2342        );
2343        assert!(w_control.w_pending);
2344        assert!(w_control.w_locked_at_accept);
2345
2346        let idle_engine = BenchmarkEngineOutput {
2347            launch_ready: true,
2348            ..BenchmarkEngineOutput::default()
2349        };
2350        let (_output, paired_control, paired_config) = axi_transition(
2351            axi_q(idle_engine, w_control, w_config),
2352            AxiLiteBenchmarkInput {
2353                aw_valid: true,
2354                aw_addr: b12(REG_BATCH as u128),
2355                ..AxiLiteBenchmarkInput::default()
2356            },
2357        );
2358        assert!(paired_control.aw_pending);
2359        assert!(!paired_control.aw_locked_at_accept);
2360        let (_output, rejected_control, rejected_config) = axi_transition(
2361            axi_q(idle_engine, paired_control, paired_config),
2362            AxiLiteBenchmarkInput::default(),
2363        );
2364        assert!(rejected_control.b_valid);
2365        assert_eq!(rejected_control.b_resp, b2(AXI_RESP_SLVERR as u128));
2366        assert_eq!(rejected_config.batch, b32(7));
2367
2368        let (_output, simultaneous_control, simultaneous_config) = axi_transition(
2369            axi_q(
2370                idle_engine,
2371                AxiLiteBenchmarkControl::default(),
2372                initial_config,
2373            ),
2374            AxiLiteBenchmarkInput {
2375                aw_valid: true,
2376                aw_addr: b12(REG_BATCH as u128),
2377                w_valid: true,
2378                w_data: b32(123),
2379                w_strb: b4(0xf),
2380                ..AxiLiteBenchmarkInput::default()
2381            },
2382        );
2383        assert!(simultaneous_control.aw_pending);
2384        assert!(simultaneous_control.w_pending);
2385        let (_output, accepted_control, accepted_config) = axi_transition(
2386            axi_q(idle_engine, simultaneous_control, simultaneous_config),
2387            AxiLiteBenchmarkInput::default(),
2388        );
2389        assert_eq!(accepted_control.b_resp, b2(AXI_RESP_OKAY as u128));
2390        assert_eq!(accepted_config.batch, b32(123));
2391    }
2392
2393    #[test]
2394    fn axi_rejects_misaligned_read_only_and_busy_accepted_writes() {
2395        for address in [REG_BATCH + 1, REG_ABI_VERSION, REG_STATUS] {
2396            let control = AxiLiteBenchmarkControl {
2397                aw_pending: true,
2398                aw_addr: b12(address as u128),
2399                w_pending: true,
2400                w_data: b32(0xfeed_face),
2401                w_strb: b4(0xf),
2402                ..AxiLiteBenchmarkControl::default()
2403            };
2404            let config = AxiLiteBenchmarkConfig {
2405                batch: b32(17),
2406                ..AxiLiteBenchmarkConfig::default()
2407            };
2408            let (_output, next_control, next_config) = axi_transition(
2409                axi_q(BenchmarkEngineOutput::default(), control, config),
2410                AxiLiteBenchmarkInput::default(),
2411            );
2412            assert_eq!(next_control.b_resp, b2(AXI_RESP_SLVERR as u128));
2413            assert_eq!(next_config, config);
2414        }
2415
2416        for address in [REG_AP_CTRL, REG_BATCH, REG_NONCE_0, REG_BASE_LO] {
2417            let control = AxiLiteBenchmarkControl {
2418                aw_pending: true,
2419                aw_addr: b12(address as u128),
2420                aw_locked_at_accept: true,
2421                w_pending: true,
2422                w_data: b32(1),
2423                w_strb: b4(0xf),
2424                ..AxiLiteBenchmarkControl::default()
2425            };
2426            let (_output, next_control, _) = axi_transition(
2427                axi_q(
2428                    BenchmarkEngineOutput::default(),
2429                    control,
2430                    AxiLiteBenchmarkConfig::default(),
2431                ),
2432                AxiLiteBenchmarkInput::default(),
2433            );
2434            assert_eq!(next_control.b_resp, b2(AXI_RESP_SLVERR as u128));
2435            assert!(!next_control.start_pending);
2436        }
2437    }
2438
2439    #[test]
2440    fn terminal_cycle_is_idle_but_an_earlier_busy_acceptance_stays_locked() {
2441        let terminal_engine = BenchmarkEngineOutput {
2442            done: true,
2443            launch_ready: false,
2444            ..BenchmarkEngineOutput::default()
2445        };
2446        let (_output, captured, config) = axi_transition(
2447            axi_q(
2448                terminal_engine,
2449                AxiLiteBenchmarkControl::default(),
2450                AxiLiteBenchmarkConfig::default(),
2451            ),
2452            AxiLiteBenchmarkInput {
2453                aw_valid: true,
2454                aw_addr: b12(REG_BATCH as u128),
2455                w_valid: true,
2456                w_data: b32(5),
2457                w_strb: b4(0xf),
2458                ..AxiLiteBenchmarkInput::default()
2459            },
2460        );
2461        assert!(!captured.aw_locked_at_accept);
2462        assert!(!captured.w_locked_at_accept);
2463        assert!(captured.done);
2464        let idle_engine = BenchmarkEngineOutput {
2465            launch_ready: true,
2466            ..BenchmarkEngineOutput::default()
2467        };
2468        let (_output, decoded, config) = axi_transition(
2469            axi_q(idle_engine, captured, config),
2470            AxiLiteBenchmarkInput::default(),
2471        );
2472        assert_eq!(decoded.b_resp, b2(AXI_RESP_OKAY as u128));
2473        assert_eq!(config.batch, b32(5));
2474
2475        let busy_snapshot = AxiLiteBenchmarkControl {
2476            aw_pending: true,
2477            aw_addr: b12(REG_BATCH as u128),
2478            aw_locked_at_accept: true,
2479            w_pending: true,
2480            w_data: b32(9),
2481            w_strb: b4(0xf),
2482            ..AxiLiteBenchmarkControl::default()
2483        };
2484        let (_output, rejected, unchanged) = axi_transition(
2485            axi_q(idle_engine, busy_snapshot, config),
2486            AxiLiteBenchmarkInput::default(),
2487        );
2488        assert_eq!(rejected.b_resp, b2(AXI_RESP_SLVERR as u128));
2489        assert_eq!(unchanged.batch, b32(5));
2490    }
2491
2492    #[test]
2493    fn overflow_rejection_preserves_base_and_interrupt_events_win_races() {
2494        let base = b64(u64::MAX as u128 - 1);
2495        let config = AxiLiteBenchmarkConfig {
2496            batch: b32(2),
2497            base_index: base,
2498            ..AxiLiteBenchmarkConfig::default()
2499        };
2500        let launch_rejected = BenchmarkEngineOutput {
2501            launch_rejected: true,
2502            reject_code: b8(BENCHMARK_REJECT_INDEX_OVERFLOW as u128),
2503            done: true,
2504            ..BenchmarkEngineOutput::default()
2505        };
2506        let (_output, rejected_control, rejected_config) = axi_transition(
2507            axi_q(
2508                launch_rejected,
2509                AxiLiteBenchmarkControl {
2510                    start_pending: true,
2511                    ..AxiLiteBenchmarkControl::default()
2512                },
2513                config,
2514            ),
2515            AxiLiteBenchmarkInput::default(),
2516        );
2517        assert_eq!(rejected_config.base_index, base);
2518        assert!(!rejected_control.start_pending);
2519        assert_eq!(
2520            rejected_control.last_reject_code,
2521            b8(BENCHMARK_REJECT_INDEX_OVERFLOW as u128)
2522        );
2523
2524        let race_control = AxiLiteBenchmarkControl {
2525            aw_pending: true,
2526            aw_addr: b12(REG_ISR as u128),
2527            w_pending: true,
2528            w_data: b32(3),
2529            w_strb: b4(1),
2530            gie: true,
2531            ier: b2(3),
2532            isr: b2(3),
2533            ..AxiLiteBenchmarkControl::default()
2534        };
2535        let events = BenchmarkEngineOutput {
2536            done: true,
2537            fault: true,
2538            ..BenchmarkEngineOutput::default()
2539        };
2540        let (race_output, raced, _) = axi_transition(
2541            axi_q(events, race_control, config),
2542            AxiLiteBenchmarkInput::default(),
2543        );
2544        assert!(race_output.interrupt);
2545        assert!(raced.done);
2546        assert!(raced.fault_seen);
2547        assert_eq!(raced.isr, b2(3), "new events win a same-cycle ISR toggle");
2548
2549        let clear_done = AxiLiteBenchmarkControl {
2550            done: true,
2551            ..AxiLiteBenchmarkControl::default()
2552        };
2553        let (_output, done_wins, _) = axi_transition(
2554            axi_q(events, clear_done, config),
2555            AxiLiteBenchmarkInput {
2556                ar_valid: true,
2557                ar_addr: b12(REG_AP_CTRL as u128),
2558                ..AxiLiteBenchmarkInput::default()
2559            },
2560        );
2561        assert!(done_wins.done, "new AP_DONE wins clear-on-read");
2562
2563        let seen_fault = AxiLiteBenchmarkControl {
2564            aw_pending: true,
2565            aw_addr: b12(REG_ISR as u128),
2566            w_pending: true,
2567            w_data: b32(2),
2568            w_strb: b4(1),
2569            isr: b2(2),
2570            fault_seen: true,
2571            ..AxiLiteBenchmarkControl::default()
2572        };
2573        let (_output, cleared, _) = axi_transition(
2574            axi_q(events, seen_fault, config),
2575            AxiLiteBenchmarkInput::default(),
2576        );
2577        assert_eq!(
2578            cleared.isr,
2579            b2(1),
2580            "fault does not reassert after fault_seen"
2581        );
2582    }
2583
2584    #[test]
2585    fn gie_and_ier_writes_gate_the_registered_isr_level_without_losing_events() {
2586        let gie_write = AxiLiteBenchmarkControl {
2587            aw_pending: true,
2588            aw_addr: b12(REG_GIE as u128),
2589            w_pending: true,
2590            w_data: b32(1),
2591            w_strb: b4(1),
2592            ier: b2(3),
2593            isr: b2(1),
2594            ..AxiLiteBenchmarkControl::default()
2595        };
2596        let (before_gie, gie_enabled, config) = axi_transition(
2597            axi_q(
2598                BenchmarkEngineOutput::default(),
2599                gie_write,
2600                AxiLiteBenchmarkConfig::default(),
2601            ),
2602            AxiLiteBenchmarkInput::default(),
2603        );
2604        assert!(
2605            !before_gie.interrupt,
2606            "GIE takes effect through registered state"
2607        );
2608        assert!(gie_enabled.gie);
2609
2610        let (after_gie, _, _) = axi_transition(
2611            axi_q(
2612                BenchmarkEngineOutput::default(),
2613                AxiLiteBenchmarkControl {
2614                    b_valid: false,
2615                    ..gie_enabled
2616                },
2617                config,
2618            ),
2619            AxiLiteBenchmarkInput::default(),
2620        );
2621        assert!(after_gie.interrupt);
2622
2623        let ier_disable = AxiLiteBenchmarkControl {
2624            aw_pending: true,
2625            aw_addr: b12(REG_IER as u128),
2626            w_pending: true,
2627            w_data: b32(0),
2628            w_strb: b4(1),
2629            gie: true,
2630            ier: b2(3),
2631            isr: b2(0),
2632            ..AxiLiteBenchmarkControl::default()
2633        };
2634        let done_event = BenchmarkEngineOutput {
2635            done: true,
2636            ..BenchmarkEngineOutput::default()
2637        };
2638        let (_before_disable, disabled, _) = axi_transition(
2639            axi_q(done_event, ier_disable, config),
2640            AxiLiteBenchmarkInput::default(),
2641        );
2642        assert_eq!(disabled.ier, b2(0));
2643        assert_eq!(disabled.isr, b2(1), "done remains sticky while masked");
2644        let (masked, _, _) = axi_transition(
2645            axi_q(
2646                BenchmarkEngineOutput::default(),
2647                AxiLiteBenchmarkControl {
2648                    b_valid: false,
2649                    ..disabled
2650                },
2651                config,
2652            ),
2653            AxiLiteBenchmarkInput::default(),
2654        );
2655        assert!(!masked.interrupt);
2656    }
2657
2658    #[test]
2659    fn byte_strobes_and_read_map_are_exact() {
2660        assert_eq!(
2661            benchmark_merge_write_kernel(b32(0x1122_3344), b32(0xaabb_ccdd), b4(0b0101)),
2662            b32(0x11bb_33dd)
2663        );
2664        let mut engine = BenchmarkEngineOutput::default();
2665        engine.completed = b64(0x0123_4567_89ab_cdef);
2666        engine.checksum[15] = b32(0xfeed_face);
2667        let common = BenchmarkReadDecodeInput {
2668            engine,
2669            ..BenchmarkReadDecodeInput::default()
2670        };
2671        let completed_low = benchmark_read_decode_kernel(BenchmarkReadDecodeInput {
2672            address: b12(0x050),
2673            ..common
2674        });
2675        let completed_high = benchmark_read_decode_kernel(BenchmarkReadDecodeInput {
2676            address: b12(0x054),
2677            ..common
2678        });
2679        let checksum_last = benchmark_read_decode_kernel(BenchmarkReadDecodeInput {
2680            address: b12(0x0cc),
2681            ..common
2682        });
2683        let misaligned = benchmark_read_decode_kernel(BenchmarkReadDecodeInput {
2684            address: b12(0x051),
2685            ..common
2686        });
2687        assert_eq!(raw32(completed_low.data), 0x89ab_cdef);
2688        assert_eq!(raw32(completed_high.data), 0x0123_4567);
2689        assert_eq!(raw32(checksum_last.data), 0xfeed_face);
2690        assert!(!misaligned.valid);
2691    }
2692}