Skip to main content

rhdl_dsig_scalable_core/
aes_table.rs

1//! AES T-table ROM: one BRAM port replaces one S-box and its MixColumns share.
2//!
3//! # The story so far
4//!
5//! A DSig signature needs 204 Haraka chain steps, and each folded chain step
6//! needs 494 AES S-box evaluations (see [`crate::chain_step`]). An *S-box*
7//! ("substitution box") is the one non-linear part of AES: a fixed table that
8//! maps every byte value `0..256` to another byte. Everything else in an AES
9//! round is XOR and wiring. So the S-boxes are where the chip area goes, and
10//! *where each S-box physically lives* is the biggest area decision in the
11//! signer.
12//!
13//! This module owns that physical side. It builds every S-box and every
14//! T-table as a read-only memory (ROM) out of RHDL parts, and leaves the
15//! choice of memory type to [`crate::sbox_placement`]. Its neighbours:
16//!
17//! ```text
18//!   crate::haraka          the math: what an AES round computes (reference + kernels)
19//!        │
20//!        ▼
21//!   crate::aes_table       this file: the S-box / T-table ROMs, registered
22//!        │                 (SboxStage, SboxStages, SboxLoader, DualTableRom)
23//!        ▼
24//!   crate::chain_step      the lane: 20 stages that use these ROMs every cycle
25//!        ▲
26//!   crate::sbox_placement  which of the 494 live boxes are UltraRAM / BRAM / LUT
27//! ```
28//!
29//! # One AES round, and which part this file builds
30//!
31//! An AES round (Intel's `AESENC` instruction) runs on a 16-byte state, laid out
32//! as four columns of four bytes. It has four steps:
33//!
34//! ```text
35//!   state ──▶ SubBytes ──▶ ShiftRows ──▶ MixColumns ──▶ AddRoundKey ──▶ state'
36//!             (16 S-box    (row r moves   (each column   (XOR a 16-byte
37//!              lookups)     left r places) times a fixed  round constant)
38//!                                          4×4 matrix)
39//!             └─── non-linear ───┘└───────────── linear (XOR only) ─────────┘
40//!
41//!   In this crate:  SubBytes+ShiftRows  = SboxStage   (one register stage)
42//!                   MixColumns+AddKey   = mix_key512_kernel in crate::haraka
43//!                                         (the next register stage)
44//! ```
45//!
46//! ShiftRows is only a re-ordering of bytes, so it costs no logic: it just
47//! decides *which* input byte each S-box reads. That is why [`SboxStage`]
48//! does SubBytes and ShiftRows together.
49//!
50//! # The idea in one picture
51//!
52//! An AES round turns each four-byte column `(s0, s1, s2, s3)` into
53//!
54//! ```text
55//!   out = 2·S(s0) ⊕ 3·S(s1) ⊕ S(s2) ⊕ S(s3)       (row 0, and rotations)
56//! ```
57//!
58//! Because MixColumns is linear, each input byte's contribution to the whole
59//! output column depends on that byte alone. Precompute it once:
60//!
61//! ```text
62//!   T0[x] = bytes (2·S(x), S(x), S(x), 3·S(x))      — a 256 × 32-bit table
63//!   out   = T0[s0] ⊕ rotl8(T0[s1]) ⊕ rotl16(T0[s2]) ⊕ rotl24(T0[s3]) ⊕ key
64//! ```
65//!
66//! In LUT logic an S-box costs about 32 LUTs. As a table it costs one read
67//! port of a block RAM, and the only LUTs left are one 5-input XOR per output
68//! bit. See [`sustained`](crate::sustained) for the budget.
69//!
70//! # Built only from RHDL parts
71//!
72//! Every table here is an [`rhdl_fpga`] [`SyncBRAM`] that is filled at
73//! configuration time and never written, i.e. a ROM with a registered read:
74//!
75//! | Item | Made of | Vivado maps it to |
76//! |---|---|---|
77//! | [`SboxStage`] | 32 × [`SboxPair`] (two S-box lookups each, [`SboxStageRoms`]) | LUT ROMs (≈32 LUTs per lookup), or one BRAM18 / URAM per pair |
78//! | [`DualTableRom`] | one dual-port `256 × 32` ROM holding [`T0`] | one BRAM18, two reads |
79//!
80//! The ROM registers its output like any synchronous BRAM read. In a pipeline
81//! that register *is* the stage register, so a table round has the same
82//! latency as a LUT round (see [`chain_step`](crate::chain_step)).
83//!
84//! This is the textbook "T-table" software AES trick moved to FPGA block RAM.
85//! In software it leaks timing through caches. A dedicated ROM port has fixed
86//! single-cycle latency regardless of address, so that concern does not apply.
87//!
88//! # Three places an S-box can live
89//!
90//! The same 256 × 8 ROM can be built three ways on the U280. The numbers are
91//! the ones in [`crate::sbox_placement`] and [`sustained`](crate::sustained):
92//!
93//! | Place | Cost per S-box | Filled how |
94//! |---|---|---|
95//! | LUT ROM (the default) | ~32 CLB LUTs | constant logic, nothing to fill |
96//! | Block RAM | ½ BRAM18 (an [`SboxPair`] serves two boxes) | preloaded by the bitstream |
97//! | UltraRAM | ½ URAM (an [`SboxPair`] serves two boxes) | **cannot be preloaded**: [`SboxLoader`] writes it after reset |
98//!
99//! UltraRAM is the odd one out. Because the bitstream cannot fill it, every
100//! UltraRAM S-box needs a write port, and the lane needs a small loader that
101//! runs after every reset. Until the loader is done the lane reports
102//! "not ready" ([`ChainStepOutput::ready`](crate::chain_step::ChainStepOutput::ready)),
103//! and nothing upstream starts a key. That keeps the one-time-key rule safe:
104//! no key is ever spent on a lane whose tables are still empty.
105//!
106//! # Reading order
107//!
108//! 1. [`T0`] and [`DualTableRom`]: the T-table trick (used by
109//!    [`TableChainStepLane`](crate::chain_step::TableChainStepLane)).
110//! 2. [`SboxPair`] and [`SboxStageRoms`]: 32 dual-port S-box pairs for one
111//!    round, wired through ShiftRows.
112//! 3. [`SboxStage`]: the same hardware, plus a fast simulation model.
113//! 4. [`SboxStages`]: ten of them, one per AES round of a Haraka-512 lane.
114//! 5. [`SboxLoader`]: fills the UltraRAM boxes after reset.
115
116use crate::haraka::aes_sbox;
117use rhdl::{
118    core::{ScopedName, circuit::descriptor::SyncKind},
119    prelude::*,
120};
121use crate::haraka::AES_ROUNDS;
122use crate::sbox_placement::{BLOCK_MASK, LANE_NEEDS_LOAD, ULTRA_MASK};
123use rhdl_fpga::core::{
124    dff::DFF,
125    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
126};
127
128/// Multiply a byte by 2 in the AES field GF(2^8) (compile-time helper).
129///
130/// "GF(2^8)" is the AES byte arithmetic: addition is XOR, and multiplying by
131/// 2 is a left shift that, if the top bit falls off, XORs in `0x1b` (the AES
132/// reduction polynomial `x^8 + x^4 + x^3 + x + 1`). MixColumns only ever
133/// multiplies by 1, 2 and 3, and `3·s = 2·s ⊕ s`, so this is all [`T0`] needs.
134const fn xtime(value: u8) -> u8 {
135    (value << 1) ^ if value & 0x80 == 0 { 0 } else { 0x1b }
136}
137
138/// Build [`T0`] at compile time: for every byte `x`, pack the four MixColumns
139/// multiples of `S(x)` into one 32-bit word.
140const fn t0_table() -> [u32; 256] {
141    // The finished 256-entry table, one 32-bit word per possible input byte.
142    let mut table = [0; 256];
143    // The input byte being tabulated (a `usize` so it can index `table`).
144    let mut x = 0;
145    while x < 256 {
146        // `S(x)`: the AES S-box output for this input byte.
147        let s = aes_sbox(x as u8);
148        // `2·S(x)` in GF(2^8).
149        let two = xtime(s);
150        // `3·S(x) = 2·S(x) ⊕ S(x)`.
151        let three = two ^ s;
152        // Little-endian rows: row 0 in bits 0..8, matching the column layout.
153        table[x] = two as u32 | (s as u32) << 8 | (s as u32) << 16 | (three as u32) << 24;
154        x += 1;
155    }
156    table
157}
158
159/// `T0[x] = (2·S(x), S(x), S(x), 3·S(x))`, row 0 in the low byte.
160///
161/// Row `j` of a column uses `T0[x].rotate_left(8 * j)`.
162///
163/// 256 entries × 32 bits = 1 KiB. It is the contents of every
164/// [`DualTableRom`] port. Why it exists: one lookup returns an S-box result
165/// already multiplied by every MixColumns coefficient it will ever need, so a
166/// table round needs no separate S-box and almost no MixColumns logic.
167pub const T0: [u32; 256] = t0_table();
168
169/// Two independent byte addresses presented in the same cycle.
170///
171/// One [`DualTableRom`] serves two of a round's 64 state bytes, so a table
172/// round has 32 of these (see
173/// [`table_addresses_kernel`](crate::chain_step::table_addresses_kernel)).
174#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
175pub struct TableRead {
176    /// Port A address: an 8-bit state byte, used as the `T0` index.
177    pub a: b8,
178    /// Port B address: an 8-bit state byte, used as the `T0` index.
179    pub b: b8,
180}
181
182/// `T0` entries for the addresses presented one cycle earlier.
183///
184/// The one-cycle delay is the BRAM's registered read. In the pipeline that
185/// register doubles as the stage register, so it costs no extra latency.
186#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
187pub struct TableWords {
188    /// `T0[a]` from the previous cycle (32 bits: four MixColumns multiples).
189    pub a: b32,
190    /// `T0[b]` from the previous cycle (32 bits: four MixColumns multiples).
191    pub b: b32,
192}
193
194/// A read-only `SyncBRAM` input: read `address`, never write.
195///
196/// RHDL's `SyncBRAM` always has a write port. A ROM is simply a RAM whose
197/// write enable is tied to `false`. Synthesis sees the constant and removes
198/// the write logic. Input: an 8-bit address. Output: the RAM input bundle
199/// for a 256 × 32-bit memory. Purely combinational; the read result appears
200/// on the RAM's output one clock later.
201#[kernel]
202pub fn rom_read8_kernel(address: b8) -> RamIn<b32, 8> {
203    RamIn::<b32, 8> {
204        read_addr: address,
205        // Write port tied off: address 0, value 0, never enabled.
206        write: RamWrite::<b32, 8> {
207            addr: b8(0),
208            value: b32(0),
209            enable: false,
210        },
211    }
212}
213
214/// Like [`rom_read8_kernel`] for a byte-wide ROM.
215///
216/// Input: the 8-bit byte to substitute. Output: the RAM input bundle for a
217/// 256 × 8-bit S-box memory, with the write port disabled. An UltraRAM box
218/// later overwrites the `write` field with the loader's write (see
219/// [`sbox_stage_kernel`]); every other box keeps it disabled.
220#[kernel]
221pub fn sbox_read_kernel(address: b8) -> RamIn<b8, 8> {
222    RamIn::<b8, 8> {
223        read_addr: address,
224        // Write port tied off; only UltraRAM boxes replace this.
225        write: RamWrite::<b8, 8> {
226            addr: b8(0),
227            value: b8(0),
228            enable: false,
229        },
230    }
231}
232
233/// A fresh AES S-box ROM (contents preloaded; LUTs unless styled).
234///
235/// A 256 × 8-bit `SyncBRAM` holding `S(x)` at address `x`. Without a
236/// `RamStyle`, Vivado turns a memory this small into LUT logic (~32 LUTs).
237fn sbox_rom() -> SyncBRAM<b8, 8> {
238    // Pair every address `x` with the S-box output `S(x)`.
239    SyncBRAM::new((0..256_u128).map(|x| (b8(x), b8(u128::from(aes_sbox(x as u8))))))
240}
241
242/// Two registered `T0` lookups per cycle from **one** true dual-port ROM.
243///
244/// # Why this is a hand-written RHDL primitive
245///
246/// A block RAM tile on the U280 (a BRAM18) has two independent read ports.
247/// RHDL's own `SyncBRAM` exposes only one of them, so the first version of
248/// this type was two `SyncBRAM`s holding the same table: one BRAM18 *per
249/// lookup*. Build v4 (four slices, T-tables in rounds 3–8) needed 4,033
250/// BRAM18 sites in the kernel region, which has 3,552, and failed placement.
251/// Serving both lookups from one memory halves that: a table round needs 32
252/// BRAM18s instead of 64.
253///
254/// RHDL builds its memories the same way this type does: `SyncBRAM` is a
255/// Rust simulation model plus a Verilog template, registered with RHDL as a
256/// netlist primitive. `DualTableRom` follows that recipe exactly (compare
257/// `rhdl_fpga::core::ram::synchronous`), with a second read port and no write
258/// port. Nothing about it is opaque: the simulation model below is ten lines,
259/// the Verilog is printed in full by [`DualTableRom::hdl`], and the tests in
260/// `tests/chain_step.rs` check every table round against the AES reference.
261///
262/// # Timing
263///
264/// Addresses presented in cycle `t` give their words in cycle `t + 1`, on
265/// both ports, exactly like the two-ROM version it replaces. The output
266/// before the first clock edge is unspecified; ownership lives in the
267/// enclosing pipeline's valid bits. Reset is ignored: a ROM has no state.
268///
269/// ```text
270///            ┌──────────── one BRAM18 (ram_style = "block") ────────────┐
271///  a ──────▶ │ port A: o_a <= T0[a]       256 × 32 T0, preloaded        │ ──▶ o.a
272///  b ──────▶ │ port B: o_b <= T0[b]       by the bitstream              │ ──▶ o.b
273///            └──────────────────────────────────────────────────────────┘
274/// ```
275#[derive(Clone, Debug)]
276pub struct DualTableRom {
277    /// The 256 table words, `contents[x] = T0[x]`.
278    contents: [u32; 256],
279}
280
281/// The ROM holds [`T0`].
282impl Default for DualTableRom {
283    fn default() -> Self {
284        Self { contents: T0 }
285    }
286}
287
288/// Ports of [`DualTableRom`]: two addresses in, two table words out.
289impl SynchronousIO for DualTableRom {
290    /// Two 8-bit addresses per cycle.
291    type I = TableRead;
292    /// The two 32-bit `T0` words, one cycle later.
293    type O = TableWords;
294    /// No RHDL kernel: the behaviour is the hand-written `sim` and `hdl`.
295    type Kernel = NoSynchronousKernel<ClockReset, TableRead, (), (TableWords, ())>;
296}
297
298/// A primitive has no child circuits, so no `D`/`Q` wiring.
299impl SynchronousDQ for DualTableRom {
300    type D = ();
301    type Q = ();
302}
303
304/// Native-simulation state of [`DualTableRom`]: the two output registers.
305#[derive(PartialEq, Debug, Digital, Clone, Copy)]
306#[doc(hidden)]
307pub struct DualTableRomState {
308    /// The clock level seen on the previous `sim` call, to detect a rising edge.
309    clock: Clock,
310    /// What both ports show: the words read at the last rising edge.
311    current: TableWords,
312    /// What both ports will capture at the next rising edge.
313    next: TableWords,
314}
315
316/// Behaviour (simulation) and structure (Verilog) of the dual-port ROM.
317impl Synchronous for DualTableRom {
318    /// Two output registers, modelled by [`DualTableRomState`].
319    type S = DualTableRomState;
320
321    /// Power-up output is "don't care", like the RAM's output register.
322    fn init(&self) -> Self::S {
323        Self::S::dont_care()
324    }
325
326    /// One half-clock step: look both addresses up while the clock is low,
327    /// capture on the rising edge, always show the captured words.
328    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
329        // The clock level of this call.
330        let clock = clock_reset.clock;
331        if !clock.raw() {
332            // The words each port will register at the next edge.
333            let word = |address: b8| b32(u128::from(self.contents[address.raw() as usize]));
334            state.next = TableWords {
335                a: word(input.a),
336                b: word(input.b),
337            };
338        }
339        // Rising edge (was low, now high): both output registers capture.
340        if clock.raw() && !state.clock.raw() {
341            state.current = state.next;
342        }
343        state.clock = clock;
344        state.current
345    }
346
347    /// Register the Verilog below with RHDL as a primitive, the same way
348    /// `SyncBRAM` does.
349    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
350        let name = scoped_name.to_string();
351        Descriptor::<SyncKind> {
352            name: scoped_name,
353            input_kind: <<Self as SynchronousIO>::I as Digital>::static_kind(),
354            output_kind: <<Self as SynchronousIO>::O as Digital>::static_kind(),
355            d_kind: Kind::Empty,
356            q_kind: Kind::Empty,
357            kernel: None,
358            netlist: None,
359            hdl: Some(self.hdl(&name)?),
360            _phantom: std::marker::PhantomData,
361        }
362        .with_embedded_netlist_black_box_unchecked()
363    }
364}
365
366impl DualTableRom {
367    /// The Verilog module: one preloaded `256 × 32` array, `ram_style =
368    /// "block"`, read on both ports at every rising edge.
369    ///
370    /// Input `i` packs [`TableRead`] and output `o` packs [`TableWords`]; the
371    /// bit ranges of their fields come from RHDL's own layout (`bit_range`),
372    /// so the module always matches the Rust types. Two reads of one array in
373    /// one clocked block is the form Vivado maps to a single true dual-port
374    /// block RAM (UG901, "Dual-Port Block RAM").
375    fn hdl(&self, name: &str) -> Result<HDLDescriptor, RHDLError> {
376        use quote::{format_ident, quote};
377        let module = format_ident!("{}", name);
378        let i_kind = TableRead::static_kind();
379        let o_kind = TableWords::static_kind();
380        let i = TableRead::dont_care();
381        let o = TableWords::dont_care();
382        let input_bits: vlog::BitRange = (0..TableRead::BITS).into();
383        let output_bits: vlog::BitRange = (0..TableWords::BITS).into();
384        let a_in: vlog::BitRange = bit_range(i_kind, &path!(i.a))?.0.into();
385        let b_in: vlog::BitRange = bit_range(i_kind, &path!(i.b))?.0.into();
386        let a_out: vlog::BitRange = bit_range(o_kind, &path!(o.a))?.0.into();
387        let b_out: vlog::BitRange = bit_range(o_kind, &path!(o.b))?.0.into();
388        let cr: ClockReset = ClockReset::dont_care();
389        let clock_index: vlog::BitRange = bit_range(ClockReset::static_kind(), &path!(cr.clock))?.0.into();
390        // `mem[x] = T0[x];` for every entry: the bitstream's preload.
391        let initial_values = self.contents.iter().enumerate().map(|(x, word)| {
392            let value: vlog::LitVerilog = b32(u128::from(*word)).typed_bits().into();
393            let x = syn::Index::from(x);
394            quote! { mem[#x] = #value; }
395        });
396        let module: vlog::ModuleDef = syn::parse_quote! {
397            module #module(
398                input wire [1:0] clock_reset,
399                input wire [#input_bits] i,
400                output wire [#output_bits] o
401            );
402                wire [0:0] clock;
403                wire [7:0] addr_a;
404                wire [7:0] addr_b;
405                reg [31:0] word_a;
406                reg [31:0] word_b;
407                (* ram_style = "block" *) reg [31:0] mem[255:0];
408                initial begin
409                    #(#initial_values)*
410                end
411                assign clock = clock_reset[#clock_index];
412                assign addr_a = i[#a_in];
413                assign addr_b = i[#b_in];
414                always @(posedge clock) begin
415                    word_a <= mem[addr_a];
416                    word_b <= mem[addr_b];
417                end
418                assign o[#a_out] = word_a;
419                assign o[#b_out] = word_b;
420            endmodule
421        };
422        Ok(HDLDescriptor {
423            name: name.to_owned(),
424            modules: module.into(),
425        })
426    }
427}
428
429/// One write into an UltraRAM S-box: entry `addr` becomes `value`.
430///
431/// Only UltraRAM boxes ever see this. [`SboxLoader`] produces one per cycle
432/// after reset, and every UltraRAM box of the lane receives the same write,
433/// so all of them fill in parallel.
434#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
435pub struct RomLoad {
436    /// Write this cycle.
437    pub enable: bool,
438    /// Table entry (8-bit S-box input `0..256`).
439    pub addr: b8,
440    /// `S(addr)` (8-bit S-box output).
441    pub value: b8,
442}
443
444/// Which memory an [`SboxPair`] is built from.
445#[derive(Clone, Copy, Debug, PartialEq, Eq)]
446pub enum PairStyle {
447    /// Preloaded ROM that Vivado builds from LUTs (≈32 LUTs per lookup).
448    Lut,
449    /// Preloaded ROM in one true dual-port BRAM18: two lookups per tile.
450    Block,
451    /// UltraRAM, empty at power-up and filled by the lane's [`SboxLoader`]:
452    /// two lookups per URAM.
453    Ultra,
454}
455
456/// The two addresses of an [`SboxPair`], and the loader's write.
457#[derive(Clone, Copy, Debug, PartialEq, Digital)]
458pub struct PairRead {
459    /// Port A address (the state byte of the pair's even position).
460    pub a: b8,
461    /// Port B address (the state byte of the pair's odd position).
462    pub b: b8,
463    /// Table load; only an UltraRAM pair uses it.
464    pub load: RomLoad,
465}
466
467/// `S(a)` and `S(b)` for the addresses presented one cycle earlier.
468#[derive(Clone, Copy, Debug, PartialEq, Digital)]
469pub struct PairBytes {
470    /// Port A output.
471    pub a: b8,
472    /// Port B output.
473    pub b: b8,
474}
475
476/// Two registered S-box lookups per cycle from **one** dual-port memory.
477///
478/// # Why pairs
479///
480/// A block RAM tile (BRAM18) and an UltraRAM each have two ports, but RHDL's
481/// `SyncBRAM` uses only one. Built from `SyncBRAM`s, every S-box placed in
482/// RAM cost a whole BRAM18 or URAM, so four slices could not afford to move
483/// their leftover S-boxes (rounds 0–2 and 9 of a T-table lane) out of LUTs:
484/// build v5 still spent 17,040 LUT-RAM per slice on them. A pair serves two
485/// S-boxes of the same round from one memory, halving the RAM per box, the
486/// same step [`DualTableRom`] took for T-tables.
487///
488/// ```text
489///            ┌──────── one memory (style: LUT, block or ultra) ────────┐
490///  a ──────▶ │ port A: o_a <= S[a]   (UltraRAM: or write the loader's   │ ──▶ o.a
491///            │                        entry while loading)               │
492///  b ──────▶ │ port B: o_b <= S[b]                                      │ ──▶ o.b
493///            └──────────────────────────────────────────────────────────┘
494/// ```
495///
496/// # Timing
497///
498/// Addresses in cycle `t` give their bytes in cycle `t + 1` on both ports,
499/// like a `SyncBRAM` ROM. Reset is ignored: ownership lives in the lane's
500/// valid bits, and an UltraRAM pair's lane reports "not ready" until its
501/// loader has written all 256 entries. While loading, port A does the write
502/// (its read output is meaningless then), so the memory never needs more
503/// than the two ports UltraRAM has.
504///
505/// Like [`DualTableRom`] this is a hand-specified RHDL primitive: a Rust
506/// simulation model plus the Verilog printed by [`SboxPair::hdl`], checked
507/// against each other in Icarus by the tests below.
508#[derive(Clone, Debug)]
509pub struct SboxPair {
510    /// Which memory the pair is built from.
511    style: PairStyle,
512}
513
514impl SboxPair {
515    /// A pair built from `style` memory.
516    #[must_use]
517    pub fn new(style: PairStyle) -> Self {
518        Self { style }
519    }
520}
521
522/// Ports of [`SboxPair`].
523impl SynchronousIO for SboxPair {
524    /// Two addresses and the loader's write.
525    type I = PairRead;
526    /// The two substituted bytes, one cycle later.
527    type O = PairBytes;
528    /// No RHDL kernel: the behaviour is the hand-written `sim` and `hdl`.
529    type Kernel = NoSynchronousKernel<ClockReset, PairRead, (), (PairBytes, ())>;
530}
531
532/// A primitive has no child circuits, so no `D`/`Q` wiring.
533impl SynchronousDQ for SboxPair {
534    type D = ();
535    type Q = ();
536}
537
538/// Native-simulation state of [`SboxPair`].
539#[derive(PartialEq, Debug, Digital, Clone, Copy)]
540#[doc(hidden)]
541pub struct SboxPairState {
542    /// The clock level seen on the previous `sim` call, to detect a rising edge.
543    clock: Clock,
544    /// The 256 entries: the S-box for a ROM, written by the loader for UltraRAM.
545    mem: [b8; 256],
546    /// What both ports show: the bytes read at the last rising edge.
547    current: PairBytes,
548    /// What both ports will capture at the next rising edge.
549    next: PairBytes,
550    /// The write an UltraRAM pair applies at the next rising edge.
551    write: RomLoad,
552}
553
554/// Behaviour (simulation) and structure (Verilog) of the pair.
555impl Synchronous for SboxPair {
556    /// Memory contents and the two output registers.
557    type S = SboxPairState;
558
559    /// A ROM starts holding the S-box; an UltraRAM starts empty (zeros
560    /// here, unknown on the chip). Outputs start as "don't care".
561    fn init(&self) -> Self::S {
562        let mut state = Self::S::dont_care();
563        if self.style != PairStyle::Ultra {
564            state.mem = core::array::from_fn(|x| b8(u128::from(aes_sbox(x as u8))));
565        }
566        state
567    }
568
569    /// One half-clock step: read both addresses while the clock is low,
570    /// capture (and, for UltraRAM, write) on the rising edge.
571    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
572        let clock = clock_reset.clock;
573        if !clock.raw() {
574            // Port A of an UltraRAM pair takes the loader's address while it
575            // writes (read-first, like the Verilog).
576            let port_a = if self.style == PairStyle::Ultra && input.load.enable {
577                input.load.addr
578            } else {
579                input.a
580            };
581            state.next = PairBytes {
582                a: state.mem[port_a.raw() as usize],
583                b: state.mem[input.b.raw() as usize],
584            };
585            state.write = input.load;
586        }
587        if clock.raw() && !state.clock.raw() {
588            state.current = state.next;
589            if self.style == PairStyle::Ultra && state.write.enable {
590                state.mem[state.write.addr.raw() as usize] = state.write.value;
591            }
592        }
593        state.clock = clock;
594        state.current
595    }
596
597    /// Register the Verilog below with RHDL as a primitive.
598    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
599        let name = scoped_name.to_string();
600        Descriptor::<SyncKind> {
601            name: scoped_name,
602            input_kind: <<Self as SynchronousIO>::I as Digital>::static_kind(),
603            output_kind: <<Self as SynchronousIO>::O as Digital>::static_kind(),
604            d_kind: Kind::Empty,
605            q_kind: Kind::Empty,
606            kernel: None,
607            netlist: None,
608            hdl: Some(self.hdl(&name)?),
609            _phantom: std::marker::PhantomData,
610        }
611        .with_embedded_netlist_black_box_unchecked()
612    }
613}
614
615impl SboxPair {
616    /// The Verilog module: one `256 × 8` array read on two ports.
617    ///
618    /// LUT and block pairs are preloaded ROMs: block adds `ram_style =
619    /// "block"`, LUT adds `rom_style = "distributed"`. The LUT style must
620    /// say so. Without an attribute, Vivado maps a `256 × 8` ROM read on two
621    /// registered ports to a BRAM18, as it did in build v8 (2026-10-16):
622    /// 142 BRAM18 per slice instead of LUTs, every SLR's block RAM 91 %
623    /// full, and slices spread over all three SLRs. A single-port
624    /// `SyncBRAM` ROM (before [`SboxPair`]) stayed in LUTs. An UltraRAM pair has `ram_style = "ultra"`, no preload, and a
625    /// write on port A: port A's address is the loader's while it writes.
626    /// Bit positions come from RHDL's layout (`bit_range`).
627    fn hdl(&self, name: &str) -> Result<HDLDescriptor, RHDLError> {
628        use quote::{format_ident, quote};
629        let module = format_ident!("{}", name);
630        let i_kind = PairRead::static_kind();
631        let o_kind = PairBytes::static_kind();
632        let i = PairRead::dont_care();
633        let o = PairBytes::dont_care();
634        let input_bits: vlog::BitRange = (0..PairRead::BITS).into();
635        let output_bits: vlog::BitRange = (0..PairBytes::BITS).into();
636        let a_in: vlog::BitRange = bit_range(i_kind.clone(), &path!(i.a))?.0.into();
637        let b_in: vlog::BitRange = bit_range(i_kind.clone(), &path!(i.b))?.0.into();
638        let we_in: vlog::BitRange = bit_range(i_kind.clone(), &path!(i.load.enable))?.0.into();
639        let wa_in: vlog::BitRange = bit_range(i_kind.clone(), &path!(i.load.addr))?.0.into();
640        let wd_in: vlog::BitRange = bit_range(i_kind, &path!(i.load.value))?.0.into();
641        let a_out: vlog::BitRange = bit_range(o_kind.clone(), &path!(o.a))?.0.into();
642        let b_out: vlog::BitRange = bit_range(o_kind, &path!(o.b))?.0.into();
643        let cr: ClockReset = ClockReset::dont_care();
644        let clock_index: vlog::BitRange = bit_range(ClockReset::static_kind(), &path!(cr.clock))?.0.into();
645        let initial_values = (0..256_u128).map(|x| {
646            let value: vlog::LitVerilog = b8(u128::from(aes_sbox(x as u8))).typed_bits().into();
647            let x = syn::Index::from(x as usize);
648            quote! { mem[#x] = #value; }
649        });
650        let module: vlog::ModuleDef = match self.style {
651            PairStyle::Ultra => syn::parse_quote! {
652                module #module(
653                    input wire [1:0] clock_reset,
654                    input wire [#input_bits] i,
655                    output wire [#output_bits] o
656                );
657                    wire [0:0] clock;
658                    wire [0:0] write_enable;
659                    wire [7:0] port_a;
660                    wire [7:0] addr_b;
661                    reg [7:0] byte_a;
662                    reg [7:0] byte_b;
663                    (* ram_style = "ultra" *) reg [7:0] mem[255:0];
664                    assign clock = clock_reset[#clock_index];
665                    assign write_enable = i[#we_in];
666                    assign port_a = write_enable ? i[#wa_in] : i[#a_in];
667                    assign addr_b = i[#b_in];
668                    always @(posedge clock) begin
669                        if (write_enable) begin
670                            mem[port_a] <= i[#wd_in];
671                        end
672                        byte_a <= mem[port_a];
673                    end
674                    always @(posedge clock) begin
675                        byte_b <= mem[addr_b];
676                    end
677                    assign o[#a_out] = byte_a;
678                    assign o[#b_out] = byte_b;
679                endmodule
680            },
681            style => {
682                let memory = if style == PairStyle::Block {
683                    quote! { (* ram_style = "block" *) reg [7:0] mem[255:0]; }
684                } else {
685                    quote! { (* rom_style = "distributed" *) reg [7:0] mem[255:0]; }
686                };
687                syn::parse_quote! {
688                    module #module(
689                        input wire [1:0] clock_reset,
690                        input wire [#input_bits] i,
691                        output wire [#output_bits] o
692                    );
693                        wire [0:0] clock;
694                        wire [7:0] addr_a;
695                        wire [7:0] addr_b;
696                        reg [7:0] byte_a;
697                        reg [7:0] byte_b;
698                        #memory
699                        initial begin
700                            #(#initial_values)*
701                        end
702                        assign clock = clock_reset[#clock_index];
703                        assign addr_a = i[#a_in];
704                        assign addr_b = i[#b_in];
705                        always @(posedge clock) begin
706                            byte_a <= mem[addr_a];
707                            byte_b <= mem[addr_b];
708                        end
709                        assign o[#a_out] = byte_a;
710                        assign o[#b_out] = byte_b;
711                    endmodule
712                }
713            }
714        };
715        Ok(HDLDescriptor {
716            name: name.to_owned(),
717            modules: module.into(),
718        })
719    }
720}
721
722/// Input of one S-box stage: the 64 state bytes, and the loader's write.
723#[derive(Clone, Copy, Debug, PartialEq, Digital)]
724pub struct SboxStageIn {
725    /// State bytes entering SubBytes: the full 64-byte (512-bit) Haraka
726    /// state, i.e. four 16-byte AES lanes side by side.
727    pub bytes: [b8; 64],
728    /// Table load, used only by UltraRAM S-boxes.
729    pub load: RomLoad,
730}
731
732/// The hardware of [`SboxStage`]: 64 S-box ROMs (`SyncBRAM<b8, 8>`), one per
733/// output byte, addressed through ShiftRows.
734///
735/// Bit `k` of `ULTRA` makes box `k` an UltraRAM (written by the lane's
736/// [`SboxLoader`]); the constructor's `block` mask makes box `k` a block-RAM
737/// ROM; every other box is a LUT ROM. See
738/// [`sbox_placement`](crate::sbox_placement).
739///
740/// Output byte `16·lane + 4·column + row` is `S(input[16·lane + 4·((column +
741/// row) mod 4) + row])`, exactly
742/// [`sub_shift512_kernel`](crate::haraka::sub_shift512_kernel) one cycle
743/// later. ROMs whose output the parent never reads are removed by synthesis.
744///
745/// ```text
746///   state byte  ──ShiftRows wiring──▶ ROM k (256 × 8) ──reg──▶ output byte k
747///   (64 of them)                      LUT, BRAM18 or URAM       (64 of them)
748///                                     chosen by ULTRA / block
749/// ```
750///
751/// Why `ULTRA` is a type parameter but `block` is a constructor argument:
752/// UltraRAM boxes need a write port wired in the *kernel*, and RHDL kernels
753/// can only branch on compile-time constants. Block RAM is just a synthesis
754/// style hint on an otherwise identical ROM, so a run-time value is enough.
755#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
756pub struct SboxStageRoms<const ULTRA: usize> {
757    /// The 32 S-box pairs: pair `p` serves output bytes `2p` (port A) and
758    /// `2p + 1` (port B), output byte `k` being `16·lane + 4·column + row`.
759    pairs: [SboxPair; 32],
760}
761
762impl<const ULTRA: usize> SboxStageRoms<ULTRA> {
763    /// A stage whose boxes in `block` are block-RAM ROMs.
764    ///
765    /// `block` is a 64-bit mask, one bit per output byte, normally a row of
766    /// [`BLOCK_MASK`]. Bit `k` set in `ULTRA` wins over `block`.
767    #[must_use]
768    pub fn new(block: usize) -> Self {
769        Self {
770            // Pair `p` (bits `2p`, `2p + 1` of the masks): UltraRAM if either
771            // box is UltraRAM, else block RAM if either is, else LUTs.
772            // `sbox_placement` hands out whole pairs, so both bits agree.
773            pairs: core::array::from_fn(|p| {
774                let bits = 0b11 << (2 * p);
775                if ULTRA & bits != 0 {
776                    // UltraRAM: starts empty; the SboxLoader fills it.
777                    SboxPair::new(PairStyle::Ultra)
778                } else if block & bits != 0 {
779                    // Block RAM: preloaded, one BRAM18 for both boxes.
780                    SboxPair::new(PairStyle::Block)
781                } else {
782                    // Preloaded ROM that Vivado builds from LUTs.
783                    SboxPair::new(PairStyle::Lut)
784                }
785            }),
786        }
787    }
788}
789
790/// Ports of [`SboxStageRoms`]: 64 bytes (plus a load) in, 64 bytes out.
791impl<const ULTRA: usize> SynchronousIO for SboxStageRoms<ULTRA> {
792    /// The 64 state bytes and the loader's write.
793    type I = SboxStageIn;
794    /// `ShiftRows(SubBytes(state))`, one cycle later.
795    type O = [b8; 64];
796    /// The wiring kernel below.
797    type Kernel = sbox_stage_kernel<ULTRA>;
798}
799
800/// ShiftRows decides which input byte each ROM reads; only UltraRAM boxes
801/// see the loader's writes.
802///
803/// Inputs: `input.bytes` (the 64-byte state), `input.load` (the loader's
804/// write) and `q` (the 64 ROM outputs). Outputs: the 64 substituted bytes
805/// read last cycle, and `d` (each ROM's next address and write). Latency:
806/// one clock. Reset is not used.
807#[allow(clippy::needless_range_loop)]
808#[kernel]
809pub fn sbox_stage_kernel<const ULTRA: usize>(
810    _clock_reset: ClockReset,
811    input: SboxStageIn,
812    q: SboxStageRomsQ<ULTRA>,
813) -> ([b8; 64], SboxStageRomsD<ULTRA>) {
814    // The state byte each output position reads (ShiftRows): output
815    // (column, row) of an AES lane reads input column (column + row) mod 4
816    // of the same row. `& 3` is `mod 4` without a divider.
817    let mut addresses = [b8(0); 64];
818    for lane in 0..4 {
819        for column in 0..4 {
820            for row in 0..4 {
821                addresses[16 * lane + 4 * column + row] = input.bytes[16 * lane + 4 * ((column + row) & 3) + row];
822            }
823        }
824    }
825    // Pair `p` reads positions `2p` (port A) and `2p + 1` (port B). Every
826    // pair sees the loader's write; only UltraRAM pairs have a write port
827    // (`SboxPair` ignores it otherwise), so `ULTRA` needs no branch here.
828    let mut pairs = [PairRead {
829        a: b8(0),
830        b: b8(0),
831        load: input.load,
832    }; 32];
833    for p in 0..32 {
834        pairs[p] = PairRead {
835            a: addresses[2 * p],
836            b: addresses[2 * p + 1],
837            load: input.load,
838        };
839    }
840    // Output: the bytes the pairs registered on the last edge.
841    let mut out = [b8(0); 64];
842    for p in 0..32 {
843        out[2 * p] = q.pairs[p].a;
844        out[2 * p + 1] = q.pairs[p].b;
845    }
846    (out, SboxStageRomsD::<ULTRA> { pairs })
847}
848
849/// Registered `SubBytes ∘ ShiftRows` for all four Haraka AES lanes.
850///
851/// **Hardware** is exactly [`SboxStageRoms`]: [`Synchronous::descriptor`]
852/// returns its descriptor, so the generated Verilog is the 64 RHDL ROMs.
853///
854/// **Native simulation** uses a faster but cycle-identical model: one register
855/// loaded with [`sub_shift512_kernel`](crate::haraka::sub_shift512_kernel).
856/// Simulating 64 separate memories per stage made whole-slice tests ~90×
857/// slower. The test `fast_model_matches_the_rom_hardware_cycle_for_cycle`
858/// pins the two together. (The fast model treats UltraRAM boxes as already
859/// loaded; the lane's `ready` output keeps them unused until they are.)
860#[derive(Clone, Debug)]
861pub struct SboxStage<const ULTRA: usize> {
862    /// The real hardware. Used for Verilog generation; native simulation
863    /// uses the fast model in [`Synchronous::sim`] instead.
864    roms: SboxStageRoms<ULTRA>,
865}
866
867impl<const ULTRA: usize> SboxStage<ULTRA> {
868    /// A stage whose boxes in `block` are block-RAM ROMs.
869    ///
870    /// `block` is the round's 64-bit block-RAM mask (a row of [`BLOCK_MASK`]).
871    #[must_use]
872    pub fn new(block: usize) -> Self {
873        Self {
874            roms: SboxStageRoms::new(block),
875        }
876    }
877}
878
879/// Same ports as [`SboxStageRoms`].
880impl<const ULTRA: usize> SynchronousIO for SboxStage<ULTRA> {
881    /// The 64 state bytes and the loader's write.
882    type I = SboxStageIn;
883    /// `ShiftRows(SubBytes(state))`, one cycle later.
884    type O = [b8; 64];
885    /// No kernel of its own: [`Synchronous::sim`] and
886    /// [`Synchronous::descriptor`] are written by hand below.
887    type Kernel = NoSynchronousKernel<ClockReset, SboxStageIn, (), ([b8; 64], ())>;
888}
889
890/// No RHDL-visible children of its own; the hardware comes from `roms`.
891impl<const ULTRA: usize> SynchronousDQ for SboxStage<ULTRA> {
892    /// Nothing to drive.
893    type D = ();
894    /// Nothing to read.
895    type Q = ();
896}
897
898/// Native-simulation state of [`SboxStage`] (a single register).
899#[derive(PartialEq, Debug, Digital, Clone, Copy)]
900#[doc(hidden)]
901pub struct SboxStageState {
902    /// The clock level seen on the previous `sim` call, to detect a rising edge.
903    clock: Clock,
904    /// The register's output: the 64 bytes captured at the last rising edge.
905    current: [b8; 64],
906    /// The value the register will capture at the next rising edge.
907    next: [b8; 64],
908}
909
910/// Hand-written behaviour: a fast model for simulation, the real ROMs for HDL.
911impl<const ULTRA: usize> Synchronous for SboxStage<ULTRA> {
912    /// One 64-byte register, modelled by [`SboxStageState`].
913    type S = SboxStageState;
914
915    /// Power-up state is "don't care", just like an un-reset register.
916    fn init(&self) -> Self::S {
917        Self::S::dont_care()
918    }
919
920    /// One half-clock step of the fast model: compute while the clock is low,
921    /// capture on the rising edge, always show the captured value.
922    fn sim(&self, clock_reset: ClockReset, input: Self::I, state: &mut Self::S) -> Self::O {
923        // The clock level of this call.
924        let clock = clock_reset.clock;
925        // Clock low: work out what the ROMs would return for this input.
926        if !clock.raw() {
927            state.next = crate::haraka::sub_shift512_kernel(input.bytes);
928        }
929        // Rising edge (was low, now high): the register captures it.
930        if clock.raw() && !state.clock.raw() {
931            state.current = state.next;
932        }
933        // Remember this level for edge detection next call.
934        state.clock = clock;
935        state.current
936    }
937
938    /// Hand RHDL the descriptor of the real 64-ROM hardware, so the
939    /// generated Verilog is [`SboxStageRoms`], not the fast model.
940    fn descriptor(&self, scoped_name: ScopedName) -> Result<Descriptor<SyncKind>, RHDLError> {
941        self.roms.descriptor(scoped_name)
942    }
943}
944
945/// The ten S-box stages of one chain-step lane, each placed by
946/// [`sbox_placement`](crate::sbox_placement).
947///
948/// One struct field per AES round (not an array), because each round's
949/// UltraRAM mask is a different compile-time constant and so a different type.
950///
951/// Ten stages because Haraka-512 has 5 rounds of 2 AES rounds each
952/// ([`AES_ROUNDS`]). Each stage serves all four 16-byte AES lanes at once,
953/// so the struct holds 10 × 64 = 640 S-box ROMs before folding; synthesis
954/// deletes the ones that are constant or unobserved, leaving the 494 live
955/// ones counted in [`crate::chain_step`].
956#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
957pub struct SboxStages {
958    /// AES round 0's SubBytes/ShiftRows.
959    s0: SboxStage<{ ULTRA_MASK[0] }>,
960    /// AES round 1's SubBytes/ShiftRows.
961    s1: SboxStage<{ ULTRA_MASK[1] }>,
962    /// AES round 2's SubBytes/ShiftRows.
963    s2: SboxStage<{ ULTRA_MASK[2] }>,
964    /// AES round 3's SubBytes/ShiftRows.
965    s3: SboxStage<{ ULTRA_MASK[3] }>,
966    /// AES round 4's SubBytes/ShiftRows.
967    s4: SboxStage<{ ULTRA_MASK[4] }>,
968    /// AES round 5's SubBytes/ShiftRows.
969    s5: SboxStage<{ ULTRA_MASK[5] }>,
970    /// AES round 6's SubBytes/ShiftRows.
971    s6: SboxStage<{ ULTRA_MASK[6] }>,
972    /// AES round 7's SubBytes/ShiftRows.
973    s7: SboxStage<{ ULTRA_MASK[7] }>,
974    /// AES round 8's SubBytes/ShiftRows.
975    s8: SboxStage<{ ULTRA_MASK[8] }>,
976    /// AES round 9's SubBytes/ShiftRows.
977    s9: SboxStage<{ ULTRA_MASK[9] }>,
978}
979
980/// Each round gets its own block-RAM mask from [`BLOCK_MASK`].
981impl Default for SboxStages {
982    /// Ten stages, placed as [`crate::sbox_placement`] decided.
983    fn default() -> Self {
984        Self {
985            s0: SboxStage::new(BLOCK_MASK[0]),
986            s1: SboxStage::new(BLOCK_MASK[1]),
987            s2: SboxStage::new(BLOCK_MASK[2]),
988            s3: SboxStage::new(BLOCK_MASK[3]),
989            s4: SboxStage::new(BLOCK_MASK[4]),
990            s5: SboxStage::new(BLOCK_MASK[5]),
991            s6: SboxStage::new(BLOCK_MASK[6]),
992            s7: SboxStage::new(BLOCK_MASK[7]),
993            s8: SboxStage::new(BLOCK_MASK[8]),
994            s9: SboxStage::new(BLOCK_MASK[9]),
995        }
996    }
997}
998
999/// Input of [`SboxStages`]: every round's bytes plus the shared table load.
1000#[derive(Clone, Copy, Debug, PartialEq, Digital)]
1001pub struct SboxStagesIn {
1002    /// State bytes entering each round's SubBytes: ten 64-byte states, one
1003    /// per AES round, all in flight at once for ten different tokens.
1004    pub bytes: [[b8; 64]; AES_ROUNDS],
1005    /// Table load from the lane's [`SboxLoader`].
1006    pub load: RomLoad,
1007}
1008
1009/// Ports of [`SboxStages`]: ten states in, ten substituted states out.
1010impl SynchronousIO for SboxStages {
1011    /// Every round's input bytes plus the shared load.
1012    type I = SboxStagesIn;
1013    /// Every round's `ShiftRows(SubBytes(·))`, one cycle later.
1014    type O = [[b8; 64]; AES_ROUNDS];
1015    /// The fan-out kernel below.
1016    type Kernel = sbox_stages_kernel;
1017}
1018
1019/// Hand each round its bytes; broadcast the table load.
1020///
1021/// Pure wiring, no logic. Output `[r]` is round `r`'s stage output (one
1022/// clock after its input). Every stage receives the same `load`, because the
1023/// loader writes the same entry into every UltraRAM box of the lane.
1024#[kernel]
1025pub fn sbox_stages_kernel(
1026    _clock_reset: ClockReset,
1027    input: SboxStagesIn,
1028    q: SboxStagesQ,
1029) -> ([[b8; 64]; AES_ROUNDS], SboxStagesD) {
1030    // Round r's bytes go to stage s<r>; the load goes everywhere.
1031    let d = SboxStagesD {
1032        s0: SboxStageIn { bytes: input.bytes[0], load: input.load },
1033        s1: SboxStageIn { bytes: input.bytes[1], load: input.load },
1034        s2: SboxStageIn { bytes: input.bytes[2], load: input.load },
1035        s3: SboxStageIn { bytes: input.bytes[3], load: input.load },
1036        s4: SboxStageIn { bytes: input.bytes[4], load: input.load },
1037        s5: SboxStageIn { bytes: input.bytes[5], load: input.load },
1038        s6: SboxStageIn { bytes: input.bytes[6], load: input.load },
1039        s7: SboxStageIn { bytes: input.bytes[7], load: input.load },
1040        s8: SboxStageIn { bytes: input.bytes[8], load: input.load },
1041        s9: SboxStageIn { bytes: input.bytes[9], load: input.load },
1042    };
1043    // Collect the ten stage outputs back into one array, in round order.
1044    (
1045        [q.s0, q.s1, q.s2, q.s3, q.s4, q.s5, q.s6, q.s7, q.s8, q.s9],
1046        d,
1047    )
1048}
1049
1050/// What the loader presents each cycle.
1051///
1052/// `load` goes to every UltraRAM S-box of the lane; `done` becomes the lane's
1053/// [`ready`](crate::chain_step::ChainStepOutput::ready) bit.
1054#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
1055pub struct LoaderOut {
1056    /// The write for every UltraRAM S-box of the lane.
1057    pub load: RomLoad,
1058    /// All 256 entries are written (or the lane has no UltraRAM S-boxes).
1059    pub done: bool,
1060}
1061
1062/// Fills a lane's UltraRAM S-boxes after reset: entry `i` gets `S(i)` for
1063/// `i = 0..256`, all boxes in parallel, from one LUT S-box ROM.
1064///
1065/// UltraRAM cannot be preloaded by the bitstream, so this runs on every
1066/// reset (about 258 cycles). Until `done`, the lane reports not ready and the
1067/// factory starts no keys. A lane with no UltraRAM S-boxes is ready at once.
1068///
1069/// Timeline after reset (one row per clock):
1070///
1071/// ```text
1072///   cycle   next   ROM read   pending (addr)   write presented   done
1073///     0       0     S(0)       -                 -                 no
1074///     1       1     S(1)       0                 S(0) → entry 0    no
1075///     2       2     S(2)       1                 S(1) → entry 1    no
1076///    ...
1077///   256     256     -          255               S(255) → 255      no
1078///   257     256     -          -                 -                 yes
1079/// ```
1080///
1081/// Why it is safe to use the LUT ROM as the source: it is preloaded by the
1082/// bitstream, so it is correct from the first cycle.
1083#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
1084pub struct SboxLoader {
1085    /// A preloaded LUT S-box ROM: the source of the values to copy.
1086    rom: SyncBRAM<b8, 8>,
1087    /// The next entry to read, `0..=256` (9 bits so that 256 means "finished").
1088    /// Resets to 0, so the load restarts after every reset.
1089    next: DFF<b9>,
1090    /// The write waiting for the ROM's registered read: `enable` and `addr`
1091    /// are known one cycle before `value` arrives from `rom`.
1092    pending: DFF<RomLoad>,
1093}
1094
1095/// Start at entry 0 with nothing pending.
1096impl Default for SboxLoader {
1097    /// A loader that will fill all 256 entries after reset.
1098    fn default() -> Self {
1099        Self {
1100            rom: sbox_rom(),
1101            next: DFF::new(b9(0)),
1102            pending: DFF::new(RomLoad::default()),
1103        }
1104    }
1105}
1106
1107/// Ports of [`SboxLoader`]: no input, one write plus `done` out.
1108impl SynchronousIO for SboxLoader {
1109    /// No input: the loader runs by itself after reset.
1110    type I = ();
1111    /// This cycle's write and the `done` flag.
1112    type O = LoaderOut;
1113    /// The counter kernel below.
1114    type Kernel = sbox_loader_kernel;
1115}
1116
1117/// Read `S(i)` one cycle, write it the next.
1118///
1119/// Inputs: only `q` (the counter, the pending write and the ROM output).
1120/// Outputs: the [`LoaderOut`] for this cycle and `d` (the next counter,
1121/// the next pending write and the next ROM address). A two-stage pipeline:
1122/// stage 1 reads the ROM, stage 2 presents the write.
1123#[kernel]
1124pub fn sbox_loader_kernel(_clock_reset: ClockReset, _input: (), q: SboxLoaderQ) -> (LoaderOut, SboxLoaderD) {
1125    // Still entries left to read?
1126    let issuing = q.next < b9(256);
1127    // The low 8 bits of the counter are the table entry to read now.
1128    let address: b8 = q.next.resize();
1129    let d = SboxLoaderD {
1130        // Stage 1: read S(address); the value appears on `q.rom` next cycle.
1131        rom: sbox_read_kernel(address),
1132        // Count up to 256, then hold there.
1133        next: if issuing { q.next + b9(1) } else { q.next },
1134        // Remember which entry that read was for. `value` is a placeholder;
1135        // the real value comes from the ROM output next cycle.
1136        pending: RomLoad {
1137            enable: issuing,
1138            addr: address,
1139            value: b8(0),
1140        },
1141    };
1142    // Stage 2: the write for last cycle's read, with the ROM's value.
1143    let load = RomLoad {
1144        enable: q.pending.enable,
1145        addr: q.pending.addr,
1146        value: q.rom,
1147    };
1148    // Done when no read is left and the last write has been presented, or
1149    // at once when this build has no UltraRAM S-boxes (`LANE_NEEDS_LOAD`
1150    // is a compile-time constant from crate::sbox_placement).
1151    let done = !LANE_NEEDS_LOAD || (!issuing && !q.pending.enable);
1152    (LoaderOut { load, done }, d)
1153}
1154
1155/// Unit tests: each ROM agrees with the reference AES in [`crate::haraka`].
1156#[cfg(test)]
1157mod tests {
1158    use super::*;
1159    use crate::haraka::{aes_round, sub_shift512_kernel};
1160
1161    /// The T-table formula `T0[s0] ⊕ rotl8(T0[s1]) ⊕ …` gives exactly the
1162    /// SubBytes+ShiftRows+MixColumns columns of the reference `aes_round`
1163    /// (with a zero key, so AddRoundKey drops out).
1164    #[test]
1165    fn t_table_columns_equal_the_reference_aes_round() {
1166        // Build one column's output purely from T0 and compare with aes_round.
1167        for seed in 0..64_u32 {
1168            // A deterministic pseudo-random 16-byte AES state.
1169            let state: [u8; 16] = core::array::from_fn(|i| (seed * 37 + i as u32 * 91) as u8);
1170            // Zero key, so the reference output is pure Sub+Shift+Mix.
1171            let key = [0_u8; 16];
1172            let reference = aes_round(&state, &key);
1173            for column in 0..4 {
1174                // The column's four output bytes, accumulated as one word.
1175                let mut word = 0_u32;
1176                for row in 0..4 {
1177                    // ShiftRows source byte for this (column, row).
1178                    let source = state[4 * ((column + row) % 4) + row];
1179                    word ^= T0[usize::from(source)].rotate_left(8 * row as u32);
1180                }
1181                assert_eq!(word.to_le_bytes(), reference[4 * column..4 * column + 4]);
1182            }
1183        }
1184    }
1185
1186    /// Run one full clock cycle (low half, then rising edge) with reset off,
1187    /// returning the output seen just after the edge.
1188    fn clocked<C: Synchronous>(circuit: &C, input: C::I, state: &mut C::S) -> C::O {
1189        circuit.sim(clock_reset(clock(false), reset(false)), input, state);
1190        circuit.sim(clock_reset(clock(true), reset(false)), input, state)
1191    }
1192
1193    /// A stage input with the given bytes and no loader write.
1194    fn bytes_in(bytes: [b8; 64]) -> SboxStageIn {
1195        SboxStageIn {
1196            bytes,
1197            load: RomLoad::default(),
1198        }
1199    }
1200
1201    /// The fast model returns `sub_shift512_kernel(input)` after one clock.
1202    #[test]
1203    fn sbox_stage_is_sub_shift_one_cycle_later() {
1204        // All-LUT stage: no UltraRAM, no block RAM.
1205        let stage = SboxStage::<0>::new(0);
1206        let mut state = stage.init();
1207        for seed in 0..8_u128 {
1208            // A deterministic pseudo-random 64-byte state.
1209            let input: [b8; 64] = core::array::from_fn(|i| b8((seed * 53 + i as u128 * 29) & 0xff));
1210            assert_eq!(
1211                clocked(&stage, bytes_in(input), &mut state),
1212                sub_shift512_kernel(input)
1213            );
1214        }
1215    }
1216
1217    /// Half the boxes in UltraRAM, a quarter in block RAM, the rest in LUTs,
1218    /// in whole pairs (bits `2p`, `2p + 1` agree, as `sbox_placement` makes them).
1219    const MIXED_ULTRA: usize = 0x3333_3333_3333_3333;
1220    /// The block-RAM quarter of that mix (bits disjoint from [`MIXED_ULTRA`]).
1221    const MIXED_BLOCK: usize = 0x0C0C_0C0C_0C0C_0C0C;
1222
1223    /// Empty UltraRAM boxes become correct S-boxes once the loader has run,
1224    /// and the loader writes each of the 256 entries exactly once.
1225    #[test]
1226    fn ultraram_boxes_work_once_the_loader_has_filled_them() {
1227        let loader = SboxLoader::default();
1228        // Real ROM hardware (not the fast model), with all three placements.
1229        let roms = SboxStageRoms::<MIXED_ULTRA>::new(MIXED_BLOCK);
1230        // Simulation state of the loader (`ls`) and of the ROMs (`rs`).
1231        let (mut ls, mut rs) = (loader.init(), roms.init());
1232        // Placeholder state bytes while loading; their outputs are ignored.
1233        let idle = [b8(0); 64];
1234        // Reset, then let the loader run to completion, feeding the stage.
1235        let cr = clock_reset(clock(false), reset(true));
1236        loader.sim(cr, (), &mut ls);
1237        loader.sim(clock_reset(clock(true), reset(true)), (), &mut ls);
1238        // Number of enabled writes seen; must end at exactly 256.
1239        let mut writes = 0;
1240        // 300 cycles is comfortably more than the ~258 the load takes.
1241        for _ in 0..300 {
1242            let out = clocked(&loader, (), &mut ls);
1243            if out.load.enable {
1244                assert_eq!(out.load.value, b8(u128::from(aes_sbox(out.load.addr.raw() as u8))));
1245                writes += 1;
1246            }
1247            clocked(&roms, SboxStageIn { bytes: idle, load: out.load }, &mut rs);
1248        }
1249        assert_eq!(writes, 256, "every table entry written exactly once");
1250        // Now every box, whatever its placement, must behave as an S-box.
1251        for seed in 0..8_u128 {
1252            let input: [b8; 64] = core::array::from_fn(|i| b8((seed * 71 + i as u128 * 13) & 0xff));
1253            assert_eq!(clocked(&roms, bytes_in(input), &mut rs), sub_shift512_kernel(input));
1254        }
1255    }
1256
1257    /// Both ports of [`DualTableRom`] return `T0` one cycle after the address,
1258    /// for every address, with the two ports reading different entries.
1259    #[test]
1260    fn dual_table_rom_reads_t0_on_both_ports() {
1261        let rom = DualTableRom::default();
1262        let mut state = rom.init();
1263        // Port A walks up while port B walks down.
1264        for x in 0..256_u128 {
1265            let words = clocked(
1266                &rom,
1267                TableRead {
1268                    a: b8(x),
1269                    b: b8(255 - x),
1270                },
1271                &mut state,
1272            );
1273            assert_eq!(words.a, b32(u128::from(T0[x as usize])));
1274            assert_eq!(words.b, b32(u128::from(T0[255 - x as usize])));
1275        }
1276    }
1277
1278    /// The Verilog of a [`DualTableRom`] is one block-RAM array read on two
1279    /// ports in one clocked block: the shape Vivado maps to a single true
1280    /// dual-port BRAM18 (build v4 used two single-port ROMs instead).
1281    #[test]
1282    fn dual_table_rom_is_one_array_with_two_registered_reads() -> Result<(), RHDLError> {
1283        let hdl = DualTableRom::default().descriptor("t_rom".into())?.hdl()?.modules.pretty();
1284        assert_eq!(hdl.matches("reg [31:0] mem[255:0]").count(), 1, "{hdl}");
1285        assert_eq!(hdl.matches("ram_style = \"block\"").count(), 1);
1286        assert_eq!(hdl.matches("<= mem[").count(), 2, "two read ports");
1287        assert!(!hdl.contains("mem[addr] <="), "no write port");
1288        Ok(())
1289    }
1290
1291    /// The generated Verilog of [`DualTableRom`], run in Icarus, matches its
1292    /// Rust model on every cycle of a stream of address pairs (both ports,
1293    /// equal and different addresses, reset in the middle).
1294    #[test]
1295    fn dual_table_rom_rtl_matches_the_model() -> Result<(), RHDLError> {
1296        let rom = DualTableRom::default();
1297        // 64 address pairs covering low/high entries and a = b.
1298        let reads = (0..64_u128).map(|n| {
1299            rhdl::core::sim::ResetOrData::Data(TableRead {
1300                a: b8((n * 37 + 5) & 0xff),
1301                b: b8(if n % 8 == 0 { (n * 37 + 5) & 0xff } else { (255 + 1024 - n * 11) & 0xff }),
1302            })
1303        });
1304        let stream = std::iter::once(rhdl::core::sim::ResetOrData::Reset)
1305            .chain(reads)
1306            .chain(std::iter::once(rhdl::core::sim::ResetOrData::Reset))
1307            .chain((0..8_u128).map(|n| rhdl::core::sim::ResetOrData::Data(TableRead { a: b8(n), b: b8(255 - n) })))
1308            .clock_pos_edge(10);
1309        let bench = rom.run(stream).collect::<SynchronousTestBench<_, _>>();
1310        // Skip the first samples: the output register starts as "don't care".
1311        bench.rtl(&rom, &TestBenchOptions::default().skip(2))?.run_iverilog()?;
1312        Ok(())
1313    }
1314
1315    /// The generated Verilog of a block-RAM and an UltraRAM [`SboxPair`], run
1316    /// in Icarus, matches the Rust model on every cycle: the UltraRAM pair
1317    /// is loaded first through port A, then both pairs read on both ports.
1318    #[test]
1319    fn sbox_pair_rtl_matches_the_model() -> Result<(), RHDLError> {
1320        let idle = RomLoad::default();
1321        // 256 load cycles (port B reads meanwhile), then 64 read pairs.
1322        let load = (0..256_u128).map(|x| PairRead {
1323            a: b8(0),
1324            b: b8(x),
1325            load: RomLoad { enable: true, addr: b8(x), value: b8(u128::from(aes_sbox(x as u8))) },
1326        });
1327        let reads = (0..64_u128).map(|n| PairRead {
1328            a: b8((n * 37 + 5) & 0xff),
1329            b: b8((255 + 1024 - n * 11) & 0xff),
1330            load: idle,
1331        });
1332        let inputs: Vec<PairRead> = load.chain(reads).collect();
1333        for style in [PairStyle::Block, PairStyle::Ultra, PairStyle::Lut] {
1334            let pair = SboxPair::new(style);
1335            let stream = std::iter::once(rhdl::core::sim::ResetOrData::Reset)
1336                .chain(inputs.iter().copied().map(rhdl::core::sim::ResetOrData::Data))
1337                .clock_pos_edge(10);
1338            let bench = pair.run(stream).collect::<SynchronousTestBench<_, _>>();
1339            // An UltraRAM reads unknown values until written, so for it the
1340            // comparison starts after the load (the bench has four samples
1341            // per clock cycle; 262 cycles covers reset and 256 writes).
1342            let skip = if style == PairStyle::Ultra { 4 * 262 } else { 2 };
1343            bench.rtl(&pair, &TestBenchOptions::default().skip(skip))?.run_iverilog()?;
1344        }
1345        Ok(())
1346    }
1347
1348    /// An UltraRAM pair holds the S-box once the loader has written it.
1349    #[test]
1350    fn ultra_pair_reads_what_the_loader_wrote() {
1351        let pair = SboxPair::new(PairStyle::Ultra);
1352        let mut state = pair.init();
1353        for x in 0..256_u128 {
1354            let load = RomLoad { enable: true, addr: b8(x), value: b8(u128::from(aes_sbox(x as u8))) };
1355            clocked(&pair, PairRead { a: b8(0), b: b8(0), load }, &mut state);
1356        }
1357        // The output after a clock edge is what that edge registered.
1358        let out = clocked(&pair, PairRead { a: b8(5), b: b8(255), load: RomLoad::default() }, &mut state);
1359        assert_eq!((out.a, out.b), (b8(u128::from(aes_sbox(5))), b8(u128::from(aes_sbox(255)))));
1360    }
1361
1362    /// Both ports of a ROM pair return `S(x)` one cycle after the address.
1363    #[test]
1364    fn sbox_pair_reads_the_sbox_on_both_ports() {
1365        let pair = SboxPair::new(PairStyle::Block);
1366        let mut state = pair.init();
1367        for x in 0..256_u128 {
1368            let out = clocked(&pair, PairRead { a: b8(x), b: b8(255 - x), load: RomLoad::default() }, &mut state);
1369            assert_eq!(out.a, b8(u128::from(aes_sbox(x as u8))));
1370            assert_eq!(out.b, b8(u128::from(aes_sbox(255 - x as u8))));
1371        }
1372    }
1373
1374    /// The fast simulation model of [`SboxStage`] and the real
1375    /// [`SboxStageRoms`] give the same output on every half-cycle. This is
1376    /// what makes it safe to simulate with the fast model.
1377    #[test]
1378    fn fast_model_matches_the_rom_hardware_cycle_for_cycle() {
1379        // No UltraRAM here: the fast model assumes UltraRAM is already loaded.
1380        let fast = SboxStage::<0>::new(MIXED_BLOCK);
1381        let roms = SboxStageRoms::<0>::new(MIXED_BLOCK);
1382        // Simulation states: `fs` for the fast model, `rs` for the ROMs.
1383        let (mut fs, mut rs) = (fast.init(), roms.init());
1384        for seed in 0..32_u128 {
1385            let input: [b8; 64] = core::array::from_fn(|i| b8((seed * 97 + i as u128 * 41) & 0xff));
1386            // Compare at both clock levels, not just after the edge.
1387            for phase in [false, true] {
1388                let cr = clock_reset(clock(phase), reset(false));
1389                let (a, b) = (
1390                    fast.sim(cr, bytes_in(input), &mut fs),
1391                    roms.sim(cr, bytes_in(input), &mut rs),
1392                );
1393                // Skip the first cycle: both start as "don't care".
1394                if seed > 0 {
1395                    assert_eq!(a, b, "seed {seed} phase {phase}");
1396                }
1397            }
1398        }
1399    }
1400
1401    /// The generated Verilog of an [`SboxStage`] is 32 pair memories, with the
1402    /// requested UltraRAM (16 pairs = half) and block-RAM (8 pairs = a
1403    /// quarter) styles; the other 8 are marked for LUTs.
1404    #[test]
1405    fn stage_lowers_to_rhdl_roms_with_the_placed_styles() -> Result<(), RHDLError> {
1406        // Lower the stage to Verilog text.
1407        let hdl = SboxStage::<MIXED_ULTRA>::new(MIXED_BLOCK)
1408            .descriptor("s_stage".into())?
1409            .hdl()?
1410            .modules
1411            .pretty();
1412        assert_eq!(hdl.matches("reg [7:0] mem[255:0]").count(), 32, "one memory per pair of bytes");
1413        assert_eq!(hdl.matches("ram_style = \"ultra\"").count(), 16);
1414        assert_eq!(hdl.matches("ram_style = \"block\"").count(), 8);
1415        assert_eq!(hdl.matches("rom_style = \"distributed\"").count(), 8);
1416        Ok(())
1417    }
1418}