rhdl_dsig_scalable_core/slice_factory.rs
1//! A slice's key factory: one complete one-time key every 68 cycles.
2//!
3//! This is the hardware form of [`slice_model::model_key`](crate::slice_model::model_key):
4//! it issues exactly the requests listed there, in the order fixed by the
5//! [`slice_schedule`](crate::slice_schedule), and writes every chain node to
6//! the slice's cache.
7//!
8//! # The story so far
9//!
10//! DSig signs a message with a *one-time key*: a W-OTS+ key with Winternitz
11//! parameter `w = 4`. Such a key is 68 hash chains (64 message digits plus 4
12//! checksum digits). Each chain starts at an 18-byte secret and is hashed
13//! three times, so it has four positions, 0 to 3. Position 3 is the public
14//! chain end. When we sign, each message digit tells us which position of its
15//! chain to reveal. Making keys ahead of time, and storing all four positions,
16//! turns signing into "look up 68 nodes and copy them out".
17//!
18//! A one-time key must never sign twice. So every key gets a fresh number
19//! (`batch`, `index`), and its secrets are derived from that number. 128 keys
20//! form a *batch*; the hash of each key's chain ends is a leaf of a Merkle
21//! tree, and one Ed25519 signature certifies the batch root. A finished DSig
22//! signature is 1,642 bytes.
23//!
24//! This module is the slice's key maker. Its job, every 68 clock cycles at the
25//! 205 MHz design clock, is:
26//!
27//! 1. derive the key's secret seed from `(epoch, batch, index)` (BLAKE3);
28//! 2. derive two nonces from the seed: the public chain salt `pk_nonce` and the
29//! private message nonce;
30//! 3. expand the seed into 68 × 18 secret bytes (a BLAKE3 *XOF*, an
31//! "extendable output", read as 20 blocks of 64 bytes);
32//! 4. push the 68 chain secrets through the chain engine, one per cycle, and
33//! write all four positions of each chain into the cache;
34//! 5. hash `pk_nonce` and the 68 chain ends into the key's `pk_hash` (the
35//! *endpoint commitment*, 20 BLAKE3 blocks plus one parent);
36//! 6. report the finished key ([`KeyDone`]) so the subtree unit can use
37//! `pk_hash` as a Merkle leaf.
38//!
39//! # The reasoning behind the shape
40//!
41//! Step 4 sets the pace: the chain engine ([`crate::slice_chain`]) accepts one
42//! chain per cycle, and a key has 68 chains, so a new key must start every 68
43//! cycles. Steps 1, 2, 3 and 5 need 44 BLAKE3 compressions per key
44//! (1 seed + 2 nonces + 20 XOF + 20 commitment blocks + 1 parent), and two
45//! more slots per period are lent to outsiders (see below): 46 in all. A
46//! dedicated hash core per
47//! step would cost far too many LUTs, so they all share **one** pipelined
48//! BLAKE3 lane that accepts one compression per cycle. 46 of every 68 cycles
49//! are therefore busy; the rest are idle.
50//!
51//! Sharing creates two problems:
52//!
53//! * **Who uses the lane in this cycle?** Several keys are in flight at once
54//! (a key's work spans several 68-cycle periods). Instead of an arbiter, we
55//! use a fixed *modulo schedule* ([`crate::slice_schedule`]): every op of a
56//! key has a fixed offset from that key's start, and no two offsets are
57//! equal modulo 68. So the cycle number modulo 68 (the *residue*) alone says
58//! which op runs. The table lookup is all the control logic there is.
59//! * **Where does each in-flight key keep its half-done state?** Each key in
60//! flight owns a *context*: one entry in a small ring of RAMs indexed by the
61//! key's number modulo [`CONTEXTS`]. The seed, nonces and BLAKE3 chaining
62//! values of a key live in its context until the key is done.
63//!
64//! An op's *lag* is how many 68-cycle periods after its key's start it runs
65//! (negative for ops that run before chain 0). From the current period and an
66//! op's lag, the factory knows which key, and thus which context, the op
67//! belongs to: `context = (period − lag) mod CONTEXTS`.
68//!
69//! # Picture
70//!
71//! ```text
72//! residue r = cycle mod 68 ──▶ timetable ──▶ (op, key context)
73//! │
74//! prefix_cv, epoch, batch ──┐ ▼
75//! context RAMs (seed, CVs) ─┼──▶ request mux ──▶ BLAKE3 lane (28 stages, II=1)
76//! commitment words ─────────┘ │ result + tag
77//! ▲ ▼
78//! │ write back into the key's context RAMs
79//! packer ◀── chain ends ◀── chain engine ◀── XOF window ◀── XOF RAM
80//! │
81//! └──▶ cache writes (all four positions)
82//! ```
83//!
84//! Read the picture from the top. A counter gives the residue; the timetable
85//! ([`SLOTS`]) turns it into an op and a lag; the lag picks the key context.
86//! The request mux builds that op's 64-byte BLAKE3 block from the run
87//! configuration and the context RAMs, and the lane hashes it. 28 cycles
88//! later the result comes out with its *tag* (a 32-bit label that says which
89//! op and which context it belongs to) and is written back into that
90//! context. XOF results go to a separate XOF RAM. From there, a two-block
91//! *window* feeds 18-byte secrets into the chain engine, one per cycle. Chain
92//! nodes go to the cache, and chain ends go through the packer into the
93//! commitment RAM. Later commitment ops read them back as BLAKE3 blocks.
94//!
95//! # Where the data lives
96//!
97//! | Storage | Width × depth | Written by | Read by |
98//! |---|---|---|---|
99//! | `contexts.seed` | 8 words × [`CONTEXTS`] | `Seed` result | nonce and XOF requests |
100//! | `contexts.pkn_feed` | 16 bytes × [`CONTEXTS`] | `PkNonce` result | chain-salt latch (residue 66/67) |
101//! | `contexts.pkn_meta` | 16 bytes × [`CONTEXTS`] | `PkNonce` result | [`KeyDone`] metadata |
102//! | `contexts.nonce` | 16 bytes × [`CONTEXTS`] | `Nonce` result | [`KeyDone`] metadata |
103//! | `contexts.cv0` / `cv1` | 8 words × [`CONTEXTS`] | chunk-0 / chunk-1 results | next block of that chunk, parent |
104//! | `xof_ram` | 16 words × 64 | `Xof` results | chain window |
105//! | `commit_ram` | 64 bytes × (32 × [`CONTEXTS`]) | packer | chunk requests |
106//!
107//! `pk_nonce` is stored twice because two different readers need it at
108//! different addresses in the same cycle, and each RAM has one read port.
109//!
110//! # Cycle contract
111//!
112//! Key `k` is decided (real or bubble) [`KEY_LEAD`] periods before its chain
113//! 0 enters the chain engine at residue 0 of period `k`. Each op of key `k`
114//! issues at cycle `68k + offset`, the offsets coming from
115//! [`SCHEDULE`]. Every op is two registered steps:
116//!
117//! 1. **decode** (cycle `y − 1`): look up residue `y mod 68`, compute the key's
118//! context index, present context-RAM read addresses, register the op;
119//! 2. **issue** (cycle `y`): assemble the request from the RAM outputs and
120//! feed the lane.
121//!
122//! Results leave the lane 28 cycles after issue, are registered once, and are
123//! written into the key's context RAM. They are readable by an op issuing
124//! [`use_delay`](crate::slice_schedule::Schedule::use_delay) cycles after the
125//! producer, which the schedule guarantees.
126//!
127//! ```text
128//! cycle: y−1 y y+1 … y+27 y+28 y+29
129//! decode issue lane stages lane output result register
130//! residue request ─▶ BLAKE3 lane ─▶ (q.lane) ─▶ (q.data.result)
131//! lookup, enters ─▶ context-RAM write
132//! RAM addr lane
133//!
134//! An op that consumes this result issues use_delay = 28 + 4 = 32 cycles
135//! after y, once the written value can be read back.
136//! ```
137//!
138//! # How a key moves through periods
139//!
140//! One *period* is 68 cycles. Key `k`'s chain 0 enters the chain engine at
141//! residue 0 of period `k`. Its preparation ops (seed, nonces, XOF) run in
142//! earlier periods, its commitment ops in later ones. Meanwhile other keys
143//! do their own ops in the same periods, on other residues:
144//!
145//! ```text
146//! period: k−KEY_LEAD … k−1 k k+1 …
147//! key k: decided seed, nonces, XOF chains 0..67 commitment, parent → KeyDone
148//! key k+1: decided … seed, XOF … chains 0..67 …
149//! ```
150//!
151//! The ring of [`CONTEXTS`] contexts must be larger than the number of
152//! periods a key spans, so a context is never reused while its key still has
153//! ops pending; compile-time assertions below check this.
154//!
155//! The Merkle and message slots carry requests from outside (the subtree unit
156//! and the signer). A request offered on [`SliceFactoryInput::merkle`] or
157//! [`SliceFactoryInput::message`] is taken at that slot's residue, signalled
158//! by `merkle_taken` or `message_taken`, and its 16 result words return on
159//! [`SliceFactoryOutput::slot_result`] with the caller's tag.
160//!
161//! # Safety rules carried by this block
162//!
163//! * Each real key gets a fresh `(batch, index)` from a monotonic counter; a
164//! key index is never issued twice within a run, and reset ends the run.
165//! * Bubble keys issue nothing: their slots are idle.
166//! * The secret prefix CV is an input, never stored beyond the run's config.
167//!
168//! # Neighbours
169//!
170//! * [`crate::slice_schedule`] computes the timetable this block follows.
171//! * [`crate::slice_chain`] is the chain engine fed one chain per cycle.
172//! * [`crate::chain_lane`] picks the chain hash (Haraka-512 by default) and
173//! with it the chain engine's latency, which shapes [`END_DELAY`],
174//! [`PACK_HISTORY`] and [`CTX_BITS`].
175//! * [`crate::packer`] turns 18-byte chain ends into 64-byte commitment words.
176//! * [`crate::blake3_core`] supplies the shared BLAKE3 lane.
177//! * [`crate::slice_model`] is the software model the tests compare against.
178//! * [`crate::slice`] wires this factory to the cache, subtree unit and signer.
179
180use crate::blake3_core::{
181 CHUNK_END, CHUNK_START, CompressionInput, DERIVE_KEY_MATERIAL, IV, InlineCompressionPipeline,
182 PARENT, PIPELINE_STAGES, ROOT, SINGLE_BLOCK_ROOT,
183};
184use crate::packer::{COMMITMENT_PREFIX, ElementPacker, PackerInput};
185use crate::slice_chain::{SLICE_CHAIN_LATENCY, SliceChainEngine, SliceChainInput};
186use crate::slice_model::{NONCE_PREFIX, PK_NONCE_PREFIX};
187use crate::slice_schedule::{
188 CHUNK0_BLOCKS, KEY_PERIOD, OPS, Op, Schedule, XOF_BLOCKS, first_chain_of_xof, op_at, solve,
189};
190use crate::{CHAINS, ELEMENT_BYTES, KEY_ONE, KEY_ZERO, KEYS_PER_SLICE, KeyIndex, SUBTREE_LEVELS};
191use rhdl::prelude::*;
192use rhdl_fpga::core::{
193 dff::DFF,
194 ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
195};
196
197/// Cycles from a chain's issue until its commitment word is readable.
198///
199/// The chain engine takes [`SLICE_CHAIN_LATENCY`] cycles (three chain-hash
200/// lanes in series); two more cycles cover the registers on the way from the
201/// engine output through the packer into the commitment RAM. Haraka: 3 × 20 + 2 = 62. BLAKE3: 3 × 28 + 2 = 86.
202/// SHA-512: 3 × 82 + 2 = 248. The schedule solver needs this number to know
203/// when a commitment block's chain ends are all present.
204pub const END_DELAY: i32 = SLICE_CHAIN_LATENCY as i32 + 2;
205/// Earlier keys' `pk_nonce`s kept for the commitment prefix.
206///
207/// A key's chain ends leave the chain engine [`END_DELAY`] cycles after its
208/// chains start. With Haraka (62 cycles) that is still the key's own period;
209/// with SHA-512 chains (248 cycles) three periods later, when newer keys are
210/// already being fed. The packer picks the nonce by the key's age.
211///
212/// The formula counts how many whole periods the chain engine spans, plus
213/// one. Haraka: 62 / 68 + 1 = 1. BLAKE3: 86 / 68 + 1 = 2. SHA-512:
214/// 248 / 68 + 1 = 4. Each entry is one 16-byte register, so this is cheap.
215pub const PACK_HISTORY: usize = END_DELAY as usize / KEY_PERIOD + 1;
216/// The timetable this factory implements.
217///
218/// Computed at compile time by [`solve`] from the BLAKE3 lane latency
219/// ([`PIPELINE_STAGES`] = 28) and [`END_DELAY`]. The hardware never runs the
220/// solver; it only reads the resulting tables ([`SLOTS`], [`WINDOW_LOADS`]).
221pub const SCHEDULE: Schedule = solve(PIPELINE_STAGES as i32, END_DELAY);
222/// Bits of a key-context index (and of an op lag). The SHA-512 chain lanes
223/// (82 cycles each) stretch one key over more 68-cycle periods, so that profile
224/// needs 32 contexts; Haraka and BLAKE3 need 16.
225///
226/// A *context* is the slot of per-key state (seed, nonces, chaining values,
227/// commitment words) that a key owns while it is in flight. More contexts
228/// cost deeper (but still small) RAMs, so we use the fewest that fit.
229pub const CTX_BITS: usize = if cfg!(dsig_chain = "sha512") { 5 } else { 4 };
230/// Key contexts held at once; must exceed the span of op lags.
231///
232/// Key number `k` uses context `k mod CONTEXTS`. If a key's ops spanned
233/// `CONTEXTS` periods or more, a newer key would overwrite its state.
234pub const CONTEXTS: usize = 1 << CTX_BITS;
235/// The last key number of a slice's batch share.
236const LAST_KEY: KeyIndex = bits((KEYS_PER_SLICE - 1) as u128);
237/// A key-context index (wraps modulo [`CONTEXTS`]).
238///
239/// [`CTX_BITS`] wide. Because the arithmetic wraps, "period minus lag" gives
240/// the right context even across the counter's wrap-around.
241pub type Ctx = Bits<CTX_BITS>;
242/// Commitment-RAM address: context and word.
243///
244/// [`CTX_BITS`] + 5 bits: `ctx << 5 | word`. The low 5 bits select one of 32
245/// word slots per context (20 are used: 16 for chunk 0, 4 for chunk 1).
246pub type CommitAddress = Bits<{ CTX_BITS + 5 }>;
247/// `1` as a [`Ctx`].
248///
249/// RHDL kernels cannot convert integers on the fly, so commonly used values
250/// are prepared as typed constants.
251pub const CTX_ONE: Ctx = bits(1);
252/// [`KEY_LEAD`] as a [`Ctx`].
253pub const CTX_LEAD: Ctx = bits(KEY_LEAD as u128);
254/// [`LAG_BIAS`] as a [`Ctx`].
255pub const CTX_BIAS: Ctx = bits(LAG_BIAS as u128);
256/// Every context index as a [`Ctx`] (kernels compare against these).
257///
258/// `CTX_INDEX[i] == i`. A kernel that needs "the ring entry at a run-time
259/// index" loops over all `i` and compares, which synthesizes to a plain
260/// multiplexer ([`ring_entry_kernel`]).
261pub const CTX_INDEX: [Ctx; CONTEXTS] = ctx_index();
262
263/// Builds [`CTX_INDEX`] at compile time: entry `k` holds the value `k`.
264const fn ctx_index() -> [Ctx; CONTEXTS] {
265 // Start from zeros, then write each index.
266 let mut out = [bits(0); CONTEXTS];
267 let mut k = 0;
268 while k < CONTEXTS {
269 out[k] = bits(k as u128);
270 k += 1;
271 }
272 out
273}
274/// Bits of a cache slot number.
275///
276/// 9 bits name 512 slots ([`CACHE_SLOTS`]); a slot holds one whole key.
277pub const SLOT_BITS: usize = 9;
278/// One-time keys the slice cache can hold (ring size): 32 batches of 16.
279///
280/// Why 32 batches: a key cannot be spent until its batch certificate returns.
281/// The Ed25519 core reaches one signature per ~758 cycles only with ~32
282/// requests in flight, each taking ~25,000 cycles, so roughly 25 batches per
283/// slice wait in the cache at the full rate. See [`sustained`](crate::sustained), section 4a.
284pub const CACHE_SLOTS: usize = 1 << SLOT_BITS;
285/// Rows of each cache bank: keys packed densely, `CHAINS` rows per key.
286///
287/// 512 × 68 = 34,816 rows. One row holds one chain position (18 bytes).
288pub const CACHE_ROWS: usize = CACHE_SLOTS * CHAINS;
289/// Bits of a cache-bank row address.
290///
291/// 16 bits reach 65,536 rows, enough for [`CACHE_ROWS`] = 34,816.
292pub const CACHE_ADDRESS_BITS: usize = 16;
293
294/// Dense cache row of `(slot, chain)`: `slot × 68 + chain`.
295///
296/// `68 = 64 + 4`, so this is two shifts and two adds. A power-of-two layout
297/// (`slot << 7 | chain`) would waste 60 of every 128 rows and need 1,024
298/// UltraRAMs for 512 keys per slice, more than the U280's 960.
299///
300/// Inputs: `slot` (9 bits, 0..512) and `chain` (7 bits, 0..68). Output: the
301/// 16-bit row address. Purely combinational: the result is ready in the same
302/// cycle.
303#[kernel]
304pub fn cache_address_kernel(slot: b9, chain: b7) -> b16 {
305 // Widen the slot first so the shifts do not lose high bits.
306 let s: b16 = slot.resize();
307 // slot × 64 + slot × 4 + chain = slot × 68 + chain.
308 (s << 6) + (s << 2) + chain.resize::<16>()
309}
310
311/// Op codes in the residue table.
312///
313/// Each residue of the 68-cycle timetable runs one kind of op. These small
314/// integers (4 bits in hardware, see [`Slot::kind`]) name the kinds. They are
315/// `usize` because RHDL kernels accept `usize` constants and turn them into
316/// bit patterns where they are compared.
317pub mod code {
318 /// No op this cycle.
319 pub const IDLE: usize = 0;
320 /// Seed derivation.
321 pub const SEED: usize = 1;
322 /// Public chain salt.
323 pub const PK_NONCE: usize = 2;
324 /// Private message nonce.
325 pub const NONCE: usize = 3;
326 /// XOF block (argument = block).
327 pub const XOF: usize = 4;
328 /// Commitment chunk-0 block (argument = block).
329 pub const CHUNK0: usize = 5;
330 /// Commitment chunk-1 block (argument = block within chunk).
331 pub const CHUNK1: usize = 6;
332 /// Commitment parent, yielding `pk_hash`.
333 pub const PARENT: usize = 7;
334 /// External Merkle-subtree request.
335 pub const MERKLE: usize = 8;
336 /// External message-digit request.
337 pub const MESSAGE: usize = 9;
338}
339
340/// The *lag* of an op: how many whole 68-cycle periods after its key's
341/// chain-0 cycle `T` it issues.
342///
343/// `offset` is the op's issue cycle relative to `T` (from [`SCHEDULE`]). A
344/// negative offset gives a negative lag (the op runs in an earlier period).
345/// `div_euclid` rounds toward minus infinity, so offset −1 has lag −1, not 0.
346const fn lag_of(offset: i32) -> i32 {
347 offset.div_euclid(KEY_PERIOD as i32)
348}
349
350/// Smallest and largest op lag (period of an op relative to its key's period).
351///
352/// The difference tells how many periods one key spans, which bounds how many
353/// contexts must exist ([`CONTEXTS`]) and when a key must be decided
354/// ([`KEY_LEAD`]).
355const fn lag_range() -> (i32, i32) {
356 // Start with impossible extremes; every op tightens them.
357 let mut low = i32::MAX;
358 let mut high = i32::MIN;
359 let mut index = 0;
360 while index < OPS {
361 // Lag of op number `index` in the timetable.
362 let lag = lag_of(SCHEDULE.offsets[index]);
363 if lag < low {
364 low = lag;
365 }
366 if lag > high {
367 high = lag;
368 }
369 index += 1;
370 }
371 (low, high)
372}
373
374/// Bias added to lags so they fit an unsigned [`CTX_BITS`]-bit field.
375///
376/// Lags can be negative (preparation ops run before chain 0), but a hardware
377/// field is unsigned. The timetable stores `lag + LAG_BIAS`, and the decode
378/// step adds [`CTX_BIAS`] back in, so the bias cancels:
379/// `period + BIAS − (lag + BIAS) = period − lag`. A compile-time assertion
380/// below checks that every biased lag fits.
381pub const LAG_BIAS: i32 = 4;
382/// Periods between deciding key `k` and its chain 0: one before its first op.
383///
384/// A key must be decided (real key or bubble) before its earliest op decodes,
385/// because decode reads the key's [`KeyEntry::real`] flag. The earliest op has
386/// lag `lag_range().0` (negative); deciding one period earlier than that is
387/// `1 − lag_range().0` periods before chain 0.
388pub const KEY_LEAD: i32 = 1 - lag_range().0;
389
390/// [`LAG_BIAS`] as a `usize`, usable inside RHDL kernels.
391pub const LAG_BIAS_U: usize = LAG_BIAS as usize;
392/// [`KEY_LEAD`] as a `usize`, usable inside RHDL kernels.
393pub const KEY_LEAD_U: usize = KEY_LEAD as usize;
394
395// RHDL kernels accept only RHDL-typed or `usize` constants, so every BLAKE3
396// constant the request mux needs is converted here, once.
397/// Converts the BLAKE3 initial vector [`IV`] (`u32` words) into RHDL `b32`
398/// words, at compile time.
399const fn iv_bits() -> [b32; 8] {
400 let mut out = [b32(0); 8];
401 let mut w = 0;
402 while w < 8 {
403 out[w] = b32(IV[w] as u128);
404 w += 1;
405 }
406 out
407}
408/// BLAKE3 IV words.
409///
410/// The chaining value a BLAKE3 hash starts from. Used for every request that
411/// is the first block of a hash (nonces, XOF, first commitment blocks, parent,
412/// Merkle, message). The seed request starts from the secret prefix CV instead.
413pub const IV_BITS: [b32; 8] = iv_bits();
414/// Flags of the seed request.
415///
416/// BLAKE3 "derive key" mode: the 20-byte material block is the whole (last,
417/// root) block of a hash keyed by the secret prefix CV.
418pub const FLAGS_SEED: b32 = b32((CHUNK_END | ROOT | DERIVE_KEY_MATERIAL) as u128);
419/// Flags of a complete single-block root hash.
420///
421/// The default for a request: one block that is both the start and the end of
422/// its chunk and the root of the hash.
423pub const FLAGS_SINGLE: b32 = b32(SINGLE_BLOCK_ROOT as u128);
424/// Flag marking a chunk's first block.
425pub const FLAG_START: b32 = b32(CHUNK_START as u128);
426/// Flag marking a chunk's last block.
427pub const FLAG_END: b32 = b32(CHUNK_END as u128);
428/// Flags of the commitment parent.
429///
430/// The commitment is 1,240 bytes, so it spans two BLAKE3 chunks; a parent
431/// node marked root joins their chaining values into `pk_hash`.
432pub const FLAGS_PARENT: b32 = b32((PARENT | ROOT) as u128);
433/// `CHUNK_END | ROOT`: profile 2's message tail block.
434pub const FLAGS_END_ROOT: b32 = b32((CHUNK_END | ROOT) as u128);
435/// Profile 2 (`DSIG_MESSAGE_BYTES=64`): the message digest is two blocks.
436pub const TWO_BLOCK_MESSAGE: bool = crate::MESSAGE_BYTES == 64;
437/// `pk_nonce` derivation prefix word.
438///
439/// Placed in word 0 of the nonce request so the two nonces derived from the
440/// same seed differ (domain separation).
441pub const PK_NONCE_WORD: b32 = b32(PK_NONCE_PREFIX as u128);
442/// Message-nonce derivation prefix word.
443pub const NONCE_WORD: b32 = b32(NONCE_PREFIX as u128);
444
445// Every biased lag fits the unsigned CTX_BITS-bit field of `Slot::lag`.
446const _: () = assert!(lag_range().0 + LAG_BIAS >= 0 && lag_range().1 + LAG_BIAS < CONTEXTS as i32);
447// From its decision to its last op, a key spans fewer than CONTEXTS periods,
448// so no newer key can reuse its context while it still has work pending.
449const _: () = assert!(lag_range().1 - lag_range().0 + KEY_LEAD < CONTEXTS as i32);
450
451/// Maps a schedule [`Op`] to its `(code, argument)` pair for the hardware
452/// table. The argument is the block number for XOF and chunk ops, else 0.
453const fn code_of(op: Op) -> (usize, usize) {
454 match op {
455 Op::Seed => (code::SEED, 0),
456 Op::PkNonce => (code::PK_NONCE, 0),
457 Op::Nonce => (code::NONCE, 0),
458 Op::Xof(n) => (code::XOF, n as usize),
459 Op::Chunk0(n) => (code::CHUNK0, n as usize),
460 Op::Chunk1(n) => (code::CHUNK1, n as usize),
461 Op::Parent => (code::PARENT, 0),
462 Op::Merkle => (code::MERKLE, 0),
463 Op::Message => (code::MESSAGE, 0),
464 // Profile 2's tail residue: the same slot code, argument 1.
465 Op::MessageTail => (code::MESSAGE, 1),
466 }
467}
468
469/// One timetable row in RHDL types.
470///
471/// The decode step reads one `Slot` per cycle, chosen by the residue. It says
472/// *what* to do (`kind`, `arg`) and *for which key* (`lag`, turned into a
473/// context by subtracting it from the current period).
474#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
475pub struct Slot {
476 /// [`code`] of the op at this residue.
477 ///
478 /// 4 bits: values 0 ([`code::IDLE`]) to 9 ([`code::MESSAGE`]).
479 pub kind: b4,
480 /// Op argument (block number).
481 ///
482 /// 5 bits: XOF block 0..20, chunk-0 block 0..16 or chunk-1 block 0..4.
483 pub arg: b5,
484 /// Lag of the op plus [`LAG_BIAS`].
485 ///
486 /// [`CTX_BITS`] wide, so it can be subtracted directly from a [`Ctx`].
487 pub lag: Ctx,
488}
489
490/// Builds [`SLOTS`]: for each residue `r`, the op the schedule places there
491/// (or an idle row), converted into RHDL types. Runs at compile time.
492const fn slot_table() -> [Slot; KEY_PERIOD] {
493 // Every residue starts idle (kind 0).
494 let mut table = [Slot {
495 kind: b4(0),
496 arg: b5(0),
497 lag: bits(0),
498 }; KEY_PERIOD];
499 let mut r = 0;
500 while r < KEY_PERIOD {
501 // `index` is the op number the solver placed at residue `r`, if any.
502 if let Some(index) = SCHEDULE.residue[r] {
503 let (kind, arg) = code_of(op_at(index as usize));
504 // Store the lag biased, so it is never negative.
505 let lag = lag_of(SCHEDULE.offsets[index as usize]) + LAG_BIAS;
506 table[r] = Slot {
507 kind: b4(kind as u128),
508 arg: b5(arg as u128),
509 lag: bits(lag as u128),
510 };
511 }
512 r += 1;
513 }
514 table
515}
516
517/// The residue table as RHDL constants.
518///
519/// 68 rows, one per residue. In hardware this is a small ROM (a constant
520/// lookup), selected by the residue counter.
521pub const SLOTS: [Slot; KEY_PERIOD] = slot_table();
522
523/// XOF window load: which block (if any) to *address* at each residue.
524///
525/// Block `t` is loaded into the window at chain `first_chain(t) − 1`, so its
526/// RAM read is addressed one cycle earlier. Block 0 is addressed at residue 66
527/// of the previous period (for the next key).
528///
529/// Why a window: chain `i` needs XOF bytes `18i..18i+18`, which may straddle
530/// two 64-byte blocks. Keeping the current and the next block in two
531/// registers (indexed by block parity) lets every chain pick its 18 bytes
532/// with a fixed multiplexer ([`pick_secret_kernel`]), and the XOF RAM needs
533/// only one read per block.
534///
535/// ```text
536/// residue: first_chain(t) − 2 first_chain(t) − 1 first_chain(t)
537/// address xof_ram RAM data → window[t%2] chain reads window
538/// ```
539#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
540pub struct WindowLoad {
541 /// A block read is addressed this residue.
542 pub active: bool,
543 /// The block number.
544 ///
545 /// 5 bits: 0..[`XOF_BLOCKS`] (20).
546 pub block: b5,
547 /// The block belongs to the key that starts next period.
548 ///
549 /// Only block 0 has this set: its read happens two cycles before chain 0,
550 /// which is at the end of the previous period.
551 pub next_key: bool,
552}
553
554/// Builds [`WINDOW_LOADS`] at compile time: for each XOF block, mark the
555/// residue at which its RAM read is addressed.
556const fn window_table() -> [WindowLoad; KEY_PERIOD] {
557 // All residues start with no load.
558 let mut table = [WindowLoad {
559 active: false,
560 block: b5(0),
561 next_key: false,
562 }; KEY_PERIOD];
563 let mut t = 0;
564 while t < XOF_BLOCKS {
565 // Address two cycles before the first chain that needs block `t`:
566 // one cycle for the RAM read, one for the window register.
567 let address = first_chain_of_xof(t) - 2;
568 // A negative cycle falls in the previous period (block 0 only).
569 let next_key = address < 0;
570 table[address.rem_euclid(KEY_PERIOD as i32) as usize] = WindowLoad {
571 active: true,
572 block: b5(t as u128),
573 next_key,
574 };
575 t += 1;
576 }
577 table
578}
579
580/// Window-load table as RHDL constants.
581///
582/// 68 rows, indexed by the residue of the current cycle.
583pub const WINDOW_LOADS: [WindowLoad; KEY_PERIOD] = window_table();
584
585/// Where chain `i`'s 18 secret bytes sit in the two-block XOF window.
586///
587/// Chain `i` starts at XOF byte `18i`. That byte lies in block `18i / 64` at
588/// offset `18i mod 64`. Because 18 is even, every offset is even, which halves
589/// the multiplexer in [`pick_secret_kernel`].
590#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
591pub struct ChainPick {
592 /// Parity of the block holding the chain's first byte.
593 ///
594 /// The window stores even blocks in `window[0]` and odd ones in
595 /// `window[1]`; this says which register is the "low" (earlier) block.
596 pub low_parity: bool,
597 /// Byte offset of the first byte within that block (even, 0..64).
598 ///
599 /// 6 bits.
600 pub offset: b6,
601}
602
603/// Builds [`CHAIN_PICKS`] at compile time: entry `i` locates chain `i`'s
604/// secret. The table has [`KEY_PERIOD`] entries because it is indexed by
605/// residue, and residue `i` feeds chain `i`.
606const fn pick_table() -> [ChainPick; KEY_PERIOD] {
607 let mut table = [ChainPick {
608 low_parity: false,
609 offset: b6(0),
610 }; KEY_PERIOD];
611 let mut i = 0;
612 while i < CHAINS {
613 // First XOF byte of chain `i`.
614 let start = ELEMENT_BYTES * i;
615 table[i] = ChainPick {
616 low_parity: (start / 64) % 2 == 1,
617 offset: b6((start % 64) as u128),
618 };
619 i += 1;
620 }
621 table
622}
623
624/// Chain-pick table as RHDL constants.
625///
626/// Indexed by residue; only the first [`CHAINS`] (= all 68) entries are
627/// meaningful.
628pub const CHAIN_PICKS: [ChainPick; KEY_PERIOD] = pick_table();
629
630/// Run configuration; hold stable while `run` is set.
631///
632/// Written by the host before a run. Together, `epoch`, the batch id and the
633/// key index make every key's number unique, and the seed is derived from
634/// that number. This is how the design guarantees fresh secrets per key.
635#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
636pub struct FactoryConfig {
637 /// Secret derive-key prefix CV ([`slice_model::prefix_cv`](crate::slice_model::prefix_cv)).
638 ///
639 /// Eight 32-bit words: the BLAKE3 chaining value after hashing the secret
640 /// key-derivation key. The seed request starts from it, so seeds cannot be
641 /// predicted without the secret.
642 pub prefix_cv: [b32; 8],
643 /// Durably allocated epoch.
644 ///
645 /// 64 bits. The host persists it before use, so a restart never repeats an
646 /// epoch, and therefore never repeats a key number.
647 pub epoch: b64,
648 /// First batch id of this run.
649 ///
650 /// 64 bits. Batch `b` of the run has id `first_batch + b`.
651 pub first_batch: b64,
652 /// Batches to make (0 = no limit).
653 ///
654 /// 32 bits. When [`FactoryControl::next_batch`] reaches it, no new key is
655 /// admitted.
656 pub batches: b32,
657 /// This slice's number; key index within a batch is
658 /// `KEYS_PER_SLICE·slice + j`.
659 ///
660 /// 3 bits: up to 8 slices.
661 pub slice: b3,
662}
663
664/// A request for one of the two externally fed slots.
665///
666/// The subtree unit and the signer also need single-block BLAKE3 hashes. The
667/// timetable reserves one residue per period for each of them, so they can
668/// borrow the factory's lane instead of building their own.
669#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
670pub struct SlotRequest {
671 /// Hold true until taken.
672 ///
673 /// The request is taken only at its slot's residue, so the caller may wait
674 /// up to one period (68 cycles).
675 pub valid: bool,
676 /// The 64-byte block as words (single-block root hash, IV key).
677 pub block: [b32; 16],
678 /// Profile 2's message digest only (`false` otherwise): this is the
679 /// tail block (48 bytes, `CHUNK_END | ROOT`, chained from [`Self::cv`]);
680 /// when `false` a profile-2 message block is the first one
681 /// (`CHUNK_START`, not a root, its CV returned).
682 pub tail: bool,
683 /// The chaining value of a tail block.
684 pub cv: [b32; 8],
685 /// Returned with the result.
686 ///
687 /// 16 bits chosen by the caller, so it can match results to requests.
688 pub tag: b16,
689}
690
691/// Factory inputs.
692#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
693pub struct SliceFactoryInput {
694 /// Configuration, stable while running.
695 pub config: FactoryConfig,
696 /// Allow new keys to be started.
697 ///
698 /// Sampled once per period, at residue 0. Clearing it turns later keys
699 /// into bubbles; keys already admitted finish normally.
700 pub run: bool,
701 /// The cache has room for one more key.
702 ///
703 /// Also sampled at residue 0. Without room, the key becomes a bubble
704 /// instead of overwriting a cached key that may not be spent yet.
705 pub space: bool,
706 /// Merkle-subtree request.
707 pub merkle: SlotRequest,
708 /// Message-digit request.
709 pub message: SlotRequest,
710}
711
712/// Four positions of one chain for the cache.
713///
714/// One write per cycle while a real key's chains leave the chain engine:
715/// 68 writes per key, one per chain.
716#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
717pub struct CacheWrite {
718 /// A write this cycle.
719 pub valid: bool,
720 /// Cache slot of the key.
721 ///
722 /// 9 bits (0..[`CACHE_SLOTS`]).
723 pub slot: b9,
724 /// Chain number.
725 ///
726 /// 7 bits (0..68).
727 pub chain: b7,
728 /// Positions 0..=3.
729 ///
730 /// Four 18-byte nodes: the secret, the two intermediate nodes and the
731 /// public chain end. All four are stored so signing needs no hashing.
732 pub nodes: [[b8; ELEMENT_BYTES]; 4],
733}
734
735/// One finished key's public and private metadata.
736///
737/// Emitted once per real key, the cycle after its parent result leaves the
738/// BLAKE3 lane. By then all 68 chains are already in the cache.
739#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
740pub struct KeyDone {
741 /// A key finished this cycle.
742 pub valid: bool,
743 /// Cache slot holding its nodes.
744 pub slot: b9,
745 /// Batch number relative to `first_batch`.
746 pub batch: b32,
747 /// Position `j` of the key within this slice's share of the batch
748 /// (`0..KEYS_PER_SLICE`).
749 pub j: KeyIndex,
750 /// Endpoint commitment (Merkle leaf).
751 ///
752 /// 32 bytes: BLAKE3 of `pk_nonce ‖ end₀ ‖ … ‖ end₆₇`.
753 pub pk_hash: [b8; 32],
754 /// Public chain salt.
755 ///
756 /// 16 bytes. A verifier needs it to recompute the chains, so it goes into
757 /// the signature.
758 pub pk_nonce: [b8; 16],
759 /// Private message nonce.
760 ///
761 /// 16 bytes, kept by the signer until the key is used.
762 pub nonce: [b8; 16],
763}
764
765/// Result of a Merkle or message slot request.
766#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
767pub struct SlotResult {
768 /// A result this cycle.
769 pub valid: bool,
770 /// True for a message result, false for Merkle.
771 pub message: bool,
772 /// All sixteen output words.
773 ///
774 /// The full 512-bit compression output; callers keep what they need.
775 pub words: [b32; 16],
776 /// The request's tag.
777 pub tag: b16,
778}
779
780/// Factory outputs.
781#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
782pub struct SliceFactoryOutput {
783 /// Chain nodes for the cache.
784 pub cache_write: CacheWrite,
785 /// Finished key metadata.
786 pub key_done: KeyDone,
787 /// Merkle/message results.
788 pub slot_result: SlotResult,
789 /// The Merkle request was taken this cycle.
790 ///
791 /// The caller drops or replaces its request on the next cycle.
792 pub merkle_taken: bool,
793 /// The message request was taken this cycle.
794 pub message_taken: bool,
795 /// Real keys started so far.
796 ///
797 /// A 32-bit counter for status and tests; bubbles are not counted.
798 pub keys_started: b32,
799}
800
801/// Per-key bookkeeping, one entry per context.
802///
803/// Written at admission (residue 0, [`KEY_LEAD`] periods before chain 0) and
804/// read by every op of the key. Small enough to keep in registers, so any
805/// stage can read any entry in the same cycle.
806#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
807pub struct KeyEntry {
808 /// The key is real (not a bubble).
809 ///
810 /// A *bubble* is a period in which no key is made; its ops do nothing.
811 pub real: bool,
812 /// Cache slot.
813 pub slot: b9,
814 /// Batch relative to the run's first batch.
815 pub batch: b32,
816 /// Position within this slice's share of the batch.
817 pub j: KeyIndex,
818}
819
820/// A decoded op waiting to issue next cycle.
821///
822/// The output of the decode step (cycle `y − 1`), registered so that the
823/// issue step (cycle `y`) sees it together with the context-RAM outputs.
824#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
825pub struct Decoded {
826 /// [`code`] value.
827 pub kind: b4,
828 /// Op argument.
829 pub arg: b5,
830 /// Key context.
831 pub ctx: Ctx,
832 /// The op should issue (real key or valid external request).
833 pub live: bool,
834}
835
836/// Resettable control state.
837///
838/// Everything here returns to zero on reset. That kills all in-flight work:
839/// the ring forgets every key, so their remaining ops become idle. Keys cut
840/// off this way are never used. Reset ends the run; the host starts any new
841/// run with a fresh batch range, so no key number repeats.
842#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
843pub struct FactoryControl {
844 /// Residue of the *current* cycle.
845 ///
846 /// 7 bits, counts 0..=67 and wraps.
847 pub residue: b7,
848 /// Low bits of the current period.
849 ///
850 /// Increments when the residue wraps. Only the low [`CTX_BITS`] bits are
851 /// kept, which is exactly the context index of the key whose chains
852 /// run in this period.
853 pub period: Ctx,
854 /// Key contexts.
855 ///
856 /// One [`KeyEntry`] per context, indexed by key number mod [`CONTEXTS`].
857 pub ring: [KeyEntry; CONTEXTS],
858 /// Next cache slot to hand out.
859 ///
860 /// Wraps after 511; the `space` input stops admission before a slot still
861 /// in use would be handed out again.
862 pub next_slot: b9,
863 /// Next `j` within the slice's share of a batch.
864 pub next_j: KeyIndex,
865 /// Batch currently being filled, relative.
866 pub next_batch: b32,
867 /// Real keys started.
868 pub started: b32,
869 /// Op decoded last cycle.
870 pub decoded: Decoded,
871 /// A window load was addressed last cycle (load the data now).
872 pub window_pending: bool,
873 /// Parity of the pending window block.
874 pub window_parity: bool,
875 /// Latch the next key's `pk_nonce` from the RAM output now.
876 pub latch_pending: bool,
877 /// Result register holds a valid result.
878 pub result_valid: bool,
879 /// Commitment packer context for the key whose ends are streaming.
880 pub pack_ctx: Ctx,
881 /// Next commitment word index.
882 ///
883 /// 5 bits, 0..20, reset to 0 after the packer's last word of a key.
884 pub pack_word: b5,
885}
886
887/// Resetless datapath state.
888///
889/// Wide data registers without reset: reset logic on hundreds of bits costs
890/// area, and these values are only used when a matching valid flag in
891/// [`FactoryControl`] says so.
892#[derive(Clone, Copy, Debug, PartialEq, Digital)]
893pub struct FactoryData {
894 /// Two-block XOF window, indexed by block parity.
895 ///
896 /// `window[0]` holds the latest even block, `window[1]` the latest odd
897 /// block, 64 bytes each.
898 pub window: [[b8; 64]; 2],
899 /// Current key's `pk_nonce` for chain salting and the commitment prefix.
900 pub pk_nonce: [b8; 16],
901 /// The [`PACK_HISTORY`] keys before it, newest first.
902 ///
903 /// Needed because chain ends of a key can emerge after newer keys have
904 /// started feeding (see [`PACK_HISTORY`]).
905 pub older_nonces: [[b8; 16]; PACK_HISTORY],
906 /// Registered lane result.
907 ///
908 /// Sixteen 32-bit words: the full BLAKE3 compression output.
909 pub result: [b32; 16],
910 /// Tag of the registered result.
911 ///
912 /// Layout from [`lane_tag_kernel`]: kind in bits 0..4, argument in bits
913 /// 4..9, context from bit 9, external tag in bits 16..32.
914 pub result_tag: b32,
915}
916
917/// The six small per-key context RAMs, grouped into one child.
918///
919/// RHDL's `Synchronous` derive stores child state in a tuple, and Rust only
920/// compares tuples of up to twelve elements, so a parent can have at most
921/// eleven direct children. Grouping these LUT RAMs keeps the factory legal.
922///
923/// Each RAM has [`CONTEXTS`] entries, one per key context, a registered read
924/// (data appears the cycle after the address) and one write port.
925#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
926pub struct ContextRams {
927 /// The key's 32-byte one-time seed, as eight words.
928 seed: SyncBRAM<[b32; 8], CTX_BITS>,
929 /// `pk_nonce` copy for the chain-salt latch.
930 pkn_feed: SyncBRAM<[b8; 16], CTX_BITS>,
931 /// `pk_nonce` copy for the finished-key metadata.
932 pkn_meta: SyncBRAM<[b8; 16], CTX_BITS>,
933 /// The private message nonce.
934 nonce: SyncBRAM<[b8; 16], CTX_BITS>,
935 /// Running chaining value of commitment chunk 0.
936 cv0: SyncBRAM<[b32; 8], CTX_BITS>,
937 /// Running chaining value of commitment chunk 1.
938 cv1: SyncBRAM<[b32; 8], CTX_BITS>,
939}
940
941/// Six empty RAMs; their contents are don't-care until written.
942impl Default for ContextRams {
943 /// Builds the six RAMs with default (unloaded) contents.
944 fn default() -> Self {
945 Self {
946 seed: SyncBRAM::default(),
947 pkn_feed: SyncBRAM::default(),
948 pkn_meta: SyncBRAM::default(),
949 nonce: SyncBRAM::default(),
950 cv0: SyncBRAM::default(),
951 cv1: SyncBRAM::default(),
952 }
953 }
954}
955
956/// Ports of [`ContextRams`], one simple-dual-port interface each.
957///
958/// "Simple dual port" means one read address and one independent write port
959/// per RAM, both usable in the same cycle.
960#[derive(Clone, Copy, Debug, PartialEq, Digital)]
961pub struct ContextRamsIn {
962 /// One-time seed, written by `Seed`, read by nonce and XOF ops.
963 pub seed: RamIn<[b32; 8], CTX_BITS>,
964 /// `pk_nonce` copy read when latching the next key's salt.
965 pub pkn_feed: RamIn<[b8; 16], CTX_BITS>,
966 /// `pk_nonce` copy read when a key finishes.
967 pub pkn_meta: RamIn<[b8; 16], CTX_BITS>,
968 /// Private nonce, read when a key finishes.
969 pub nonce: RamIn<[b8; 16], CTX_BITS>,
970 /// Chunk-0 chaining value.
971 pub cv0: RamIn<[b32; 8], CTX_BITS>,
972 /// Chunk-1 chaining value.
973 pub cv1: RamIn<[b32; 8], CTX_BITS>,
974}
975
976/// Registered read data of [`ContextRams`].
977///
978/// Each field is the entry addressed on the *previous* cycle.
979#[derive(Clone, Copy, Debug, PartialEq, Digital)]
980pub struct ContextRamsOut {
981 /// Seed words.
982 pub seed: [b32; 8],
983 /// `pk_nonce` for the salt latch.
984 pub pkn_feed: [b8; 16],
985 /// `pk_nonce` for key metadata.
986 pub pkn_meta: [b8; 16],
987 /// Private nonce.
988 pub nonce: [b8; 16],
989 /// Chunk-0 chaining value.
990 pub cv0: [b32; 8],
991 /// Chunk-1 chaining value.
992 pub cv1: [b32; 8],
993}
994
995/// Connects [`ContextRams`] to its port types and wiring kernel.
996impl SynchronousIO for ContextRams {
997 /// All six RAMs' read addresses and write ports.
998 type I = ContextRamsIn;
999 /// All six RAMs' registered read data.
1000 type O = ContextRamsOut;
1001 /// The pass-through wiring.
1002 type Kernel = context_rams_kernel;
1003}
1004
1005/// Pure wiring between the grouped RAM ports and their children.
1006///
1007/// Inputs go straight to the child RAMs (`D`), and the RAMs' read data goes
1008/// straight out (`Q` to output). No logic and no extra cycle: this block only
1009/// exists to group six children under one name.
1010#[kernel]
1011pub fn context_rams_kernel(
1012 _clock_reset: ClockReset,
1013 input: ContextRamsIn,
1014 q: ContextRamsQ,
1015) -> (ContextRamsOut, ContextRamsD) {
1016 (
1017 ContextRamsOut {
1018 seed: q.seed,
1019 pkn_feed: q.pkn_feed,
1020 pkn_meta: q.pkn_meta,
1021 nonce: q.nonce,
1022 cv0: q.cv0,
1023 cv1: q.cv1,
1024 },
1025 ContextRamsD {
1026 seed: input.seed,
1027 pkn_feed: input.pkn_feed,
1028 pkn_meta: input.pkn_meta,
1029 nonce: input.nonce,
1030 cv0: input.cv0,
1031 cv1: input.cv1,
1032 },
1033 )
1034}
1035
1036/// The factory: one BLAKE3 lane, one chain engine and their bookkeeping.
1037///
1038/// `TABLE_MASK` is the chain-lane area knob, passed through to
1039/// [`SliceChainEngine`]: it chooses which AES rounds of the Haraka lane use
1040/// table ROMs. See [`crate::chain_step`].
1041#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
1042pub struct SliceFactory<const TABLE_MASK: usize> {
1043 /// The shared BLAKE3 compression pipeline: one request per cycle,
1044 /// [`PIPELINE_STAGES`] (28) cycles latency.
1045 lane: InlineCompressionPipeline,
1046 /// The chain engine: one chain in per cycle, four positions out
1047 /// [`SLICE_CHAIN_LATENCY`] cycles later.
1048 chains: SliceChainEngine<TABLE_MASK>,
1049 /// Packs `pk_nonce` and 68 chain ends (18 bytes each) into 64-byte
1050 /// commitment words.
1051 packer: ElementPacker<COMMITMENT_PREFIX>,
1052 /// The six per-key context RAMs.
1053 contexts: ContextRams,
1054 /// XOF blocks, 16 words each. 64 entries addressed `parity << 5 | block`:
1055 /// two keys' worth (the key feeding chains and the next one), selected by
1056 /// the low bit of the key's context.
1057 xof_ram: SyncBRAM<[b32; 16], 6>,
1058 /// Commitment words, 64 bytes each, addressed `ctx << 5 | word`
1059 /// ([`CommitAddress`]).
1060 commit_ram: SyncBRAM<[b8; 64], { CTX_BITS + 5 }>,
1061 /// Counters, the key ring and pipeline flags; cleared by reset.
1062 control: DFF<FactoryControl>,
1063 /// Wide data registers; not reset.
1064 data: DFF<FactoryData>,
1065}
1066
1067/// A factory at power-up: control state zeroed, data registers don't-care.
1068impl<const TABLE_MASK: usize> Default for SliceFactory<TABLE_MASK> {
1069 /// Builds every child in its default state.
1070 fn default() -> Self {
1071 Self {
1072 lane: InlineCompressionPipeline,
1073 chains: SliceChainEngine::default(),
1074 packer: ElementPacker::default(),
1075 contexts: ContextRams::default(),
1076 xof_ram: SyncBRAM::default(),
1077 commit_ram: SyncBRAM::default(),
1078 control: DFF::new(FactoryControl::default()),
1079 data: DFF::resetless(),
1080 }
1081 }
1082}
1083
1084/// Connects [`SliceFactory`] to its port types and wiring kernel.
1085impl<const TABLE_MASK: usize> SynchronousIO for SliceFactory<TABLE_MASK> {
1086 /// Run control, configuration and the two external slot requests.
1087 type I = SliceFactoryInput;
1088 /// Cache writes, finished keys and slot results.
1089 type O = SliceFactoryOutput;
1090 /// The factory's wiring, [`slice_factory_kernel`].
1091 type Kernel = slice_factory_kernel<TABLE_MASK>;
1092}
1093
1094/// Little-endian bytes to words.
1095///
1096/// Input: 64 bytes. Output: 16 words of 32 bits, word `w` made of bytes
1097/// `4w..4w+4` with byte `4w` as the lowest. BLAKE3 reads its message block
1098/// this way. Pure wiring: no logic gates, no cycles.
1099#[allow(clippy::needless_range_loop)]
1100#[kernel]
1101pub fn words_from_bytes_kernel(bytes: [b8; 64]) -> [b32; 16] {
1102 let mut words = [b32(0); 16];
1103 for w in 0..16 {
1104 // Byte 4w is the least significant byte of word w.
1105 words[w] = bytes[4 * w].resize::<32>()
1106 | (bytes[4 * w + 1].resize::<32>() << 8)
1107 | (bytes[4 * w + 2].resize::<32>() << 16)
1108 | (bytes[4 * w + 3].resize::<32>() << 24);
1109 }
1110 words
1111}
1112
1113/// Words to little-endian bytes.
1114///
1115/// The inverse of [`words_from_bytes_kernel`]: 16 words in, 64 bytes out.
1116/// Used to turn BLAKE3 outputs into the byte strings the protocol defines.
1117#[allow(clippy::needless_range_loop)]
1118#[kernel]
1119pub fn bytes_from_words_kernel(words: [b32; 16]) -> [b8; 64] {
1120 let mut bytes = [b8(0); 64];
1121 for w in 0..16 {
1122 bytes[4 * w] = words[w].resize();
1123 bytes[4 * w + 1] = (words[w] >> 8).resize();
1124 bytes[4 * w + 2] = (words[w] >> 16).resize();
1125 bytes[4 * w + 3] = (words[w] >> 24).resize();
1126 }
1127 bytes
1128}
1129
1130/// Pick a chain's 18 secret bytes from the two-block window.
1131///
1132/// Inputs: `low` (the block holding the secret's first byte), `high` (the
1133/// block after it) and `offset` (even byte offset 0..62 within `low`).
1134/// Output: the 18 bytes starting at `offset` in `low ‖ high`. Combinational.
1135///
1136/// Why written as an OR over 32 cases: RHDL turns a dynamic array index into
1137/// whole-array copies, which is very large. Comparing `offset` against each
1138/// of the 32 possible even values with constant indices instead gives a
1139/// 32-way select per output byte (see the RHDL notes in [`crate::packer`]).
1140#[allow(clippy::needless_range_loop)]
1141#[kernel]
1142pub fn pick_secret_kernel(low: [b8; 64], high: [b8; 64], offset: b6) -> [b8; ELEMENT_BYTES] {
1143 // The two blocks side by side, so a secret that crosses the block
1144 // boundary is one contiguous run of bytes.
1145 let mut joined = [b8(0); 128];
1146 for byte in 0..64 {
1147 joined[byte] = low[byte];
1148 joined[64 + byte] = high[byte];
1149 }
1150 let mut secret = [b8(0); ELEMENT_BYTES];
1151 for k in 0..ELEMENT_BYTES {
1152 // Byte k of the secret: exactly one case below matches, the others
1153 // contribute zero.
1154 let mut value = b8(0);
1155 for f in 0..32 {
1156 // Case f: the secret starts at byte 2f.
1157 if offset == b6((2 * f) as u128) {
1158 value = value | joined[2 * f + k];
1159 }
1160 }
1161 secret[k] = value;
1162 }
1163 secret
1164}
1165
1166/// Pack (op, ctx) into a lane tag; slot requests keep their 16-bit tag above.
1167///
1168/// The BLAKE3 lane carries a 32-bit tag beside each request and returns it
1169/// with the result. 28 cycles later, the tag alone tells the result path what
1170/// the result is and where to write it.
1171///
1172/// ```text
1173/// bit: 31 16 15 9+CTX_BITS 9 8 4 3 0
1174/// [ external tag ] [ 0 ] [ ctx ] [ arg ] [kind]
1175/// ```
1176#[kernel]
1177pub fn lane_tag_kernel(kind: b4, arg: b5, ctx: Ctx, external: b16) -> b32 {
1178 kind.resize::<32>()
1179 | (arg.resize::<32>() << 4)
1180 | (ctx.resize::<32>() << 9)
1181 | (external.resize::<32>() << 16)
1182}
1183
1184/// Read one ring entry by dynamic context index.
1185///
1186/// Inputs: the whole ring and a context index. Output: `ring[ctx]`. Written
1187/// as a compare-and-select loop for the same reason as
1188/// [`pick_secret_kernel`]: a [`CONTEXTS`]-way multiplexer instead of array
1189/// copies. Combinational.
1190#[kernel]
1191pub fn ring_entry_kernel(ring: [KeyEntry; CONTEXTS], ctx: Ctx) -> KeyEntry {
1192 let mut entry = KeyEntry::default();
1193 for index in 0..CONTEXTS {
1194 // Exactly one index matches.
1195 if ctx == CTX_INDEX[index] {
1196 entry = ring[index];
1197 }
1198 }
1199 entry
1200}
1201
1202/// The factory's wiring.
1203///
1204/// Like every RHDL kernel, this runs once per clock: it reads the current
1205/// register and child outputs (`q`) and the inputs, and returns this cycle's
1206/// outputs and the next values for every register and child input (`d`).
1207///
1208/// It does six jobs each cycle, in the sections marked below:
1209///
1210/// | Section | What happens this cycle |
1211/// |---|---|
1212/// | time | advance the residue (0..=67) and period counters |
1213/// | key admission | at residue 0, decide key `period + KEY_LEAD`: real or bubble |
1214/// | decode (y − 1) | look up next cycle's op, find its context, address the RAMs |
1215/// | issue (y) | build the BLAKE3 request of the op decoded last cycle |
1216/// | result path | register the lane's output; route it to a RAM or an output |
1217/// | chain feed | feed chain `residue` of the key whose period this is |
1218/// | chain results → cache | write four positions per chain; pack chain ends |
1219/// | RAM ports | collect every RAM's read address and write port |
1220///
1221/// Inputs: [`SliceFactoryInput`]. Outputs: [`SliceFactoryOutput`]; the
1222/// cache write appears [`SLICE_CHAIN_LATENCY`] cycles after its chain was
1223/// fed, [`KeyDone`] one cycle after the key's parent result leaves the lane.
1224#[allow(clippy::too_many_lines, clippy::needless_range_loop)]
1225#[kernel]
1226pub fn slice_factory_kernel<const TABLE_MASK: usize>(
1227 clock_reset: ClockReset,
1228 input: SliceFactoryInput,
1229 q: SliceFactoryQ<TABLE_MASK>,
1230) -> (SliceFactoryOutput, SliceFactoryD<TABLE_MASK>) {
1231 // True during reset: every valid flag below is forced low.
1232 let resetting = clock_reset.reset.any();
1233 // Short name for the run configuration.
1234 let cfg = input.config;
1235 // Next-state copies: start from the current state and change fields.
1236 let mut control = q.control;
1237 let mut data = q.data;
1238
1239 // ---------------------------------------------------------------- time
1240 // Residue of this cycle (0..=67) and of the next one.
1241 let residue = q.control.residue;
1242 let next_residue = if residue == b7((KEY_PERIOD - 1) as u128) {
1243 b7(0)
1244 } else {
1245 residue + b7(1)
1246 };
1247 // Period of the *next* cycle (decode looks one cycle ahead).
1248 let next_period = if residue == b7((KEY_PERIOD - 1) as u128) {
1249 q.control.period + CTX_ONE
1250 } else {
1251 q.control.period
1252 };
1253 control.residue = next_residue;
1254 control.period = next_period;
1255
1256 // ------------------------------------------------------- key admission
1257 // At residue 0 of period p, decide key p + KEY_LEAD.
1258 if residue == b7(0) {
1259 // Context of the key being decided (key number mod CONTEXTS).
1260 let decided: Ctx = q.control.period + CTX_LEAD;
1261 // The run asked for a fixed number of batches and they are all made.
1262 let limit_reached = cfg.batches != b32(0) && q.control.next_batch >= cfg.batches;
1263 // No key starts until every UltraRAM S-box is loaded (see
1264 // `aes_table::SboxLoader`); instantly true with no UltraRAM boxes.
1265 let real = input.run && input.space && !limit_reached && q.chains.ready;
1266 // A bubble gets the default entry (real = false).
1267 let mut entry = KeyEntry::default();
1268 if real {
1269 // A real key takes the next slot and the next fresh index. These
1270 // counters only move forward, so no index is handed out twice.
1271 entry = KeyEntry {
1272 real: true,
1273 slot: q.control.next_slot,
1274 batch: q.control.next_batch,
1275 j: q.control.next_j,
1276 };
1277 control.next_slot = q.control.next_slot + b9(1);
1278 control.started = q.control.started + b32(1);
1279 control.next_j = q.control.next_j + KEY_ONE;
1280 // After the slice's last key of a batch, move to the next batch.
1281 if q.control.next_j == LAST_KEY {
1282 control.next_j = KEY_ZERO;
1283 control.next_batch = q.control.next_batch + b32(1);
1284 }
1285 }
1286 // Store the entry in the decided key's context.
1287 for index in 0..CONTEXTS {
1288 if decided == CTX_INDEX[index] {
1289 control.ring[index] = entry;
1290 }
1291 }
1292 }
1293
1294 // ----------------------------------------------------- decode (y − 1)
1295 // The op that issues next cycle: the table row of next cycle's residue.
1296 let mut slot = Slot::default();
1297 for r in 0..KEY_PERIOD {
1298 if next_residue == b7(r as u128) {
1299 slot = SLOTS[r];
1300 }
1301 }
1302 // Context of the key this op belongs to: (period − lag) mod CONTEXTS.
1303 // (The table's lag carries LAG_BIAS, which CTX_BIAS cancels.)
1304 let ctx: Ctx = next_period + CTX_BIAS - slot.lag;
1305 // Bookkeeping of that key: is it real?
1306 let owner = ring_entry_kernel(q.control.ring, ctx);
1307 // External slots run only when their caller has a request waiting;
1308 // key ops run only for real keys. Idle residues never run.
1309 let is_merkle = slot.kind == b4(code::MERKLE as u128);
1310 let is_message = slot.kind == b4(code::MESSAGE as u128);
1311 let live = if is_merkle {
1312 input.merkle.valid
1313 } else if is_message {
1314 // Each message residue takes only its own kind of request: argument
1315 // 0 first blocks, argument 1 (profile 2's tail residue) tails. A
1316 // first block taken at the tail residue would burn a second key in
1317 // the same period while its tail had nowhere to go.
1318 input.message.valid && (input.message.tail == (slot.arg == b5(1)))
1319 } else {
1320 slot.kind != b4(code::IDLE as u128) && owner.real
1321 };
1322 control.decoded = Decoded {
1323 kind: slot.kind,
1324 arg: slot.arg,
1325 ctx,
1326 live,
1327 };
1328 // Context-RAM read addresses for the op issuing next cycle.
1329 // Commitment word to read: chunk-0 block n is word n, chunk-1 block n
1330 // is word 16 + n.
1331 let word_index: b5 = if slot.kind == b4(code::CHUNK1 as u128) {
1332 slot.arg + b5(CHUNK0_BLOCKS as u128)
1333 } else {
1334 slot.arg
1335 };
1336 let ctx_wide: CommitAddress = ctx.resize();
1337 let word_wide: CommitAddress = word_index.resize();
1338 // Address = ctx << 5 | word (see `CommitAddress`).
1339 let commit_address: CommitAddress = (ctx_wide << 5) | word_wide;
1340
1341 // ------------------------------------------------------- issue (y)
1342 // The op decoded last cycle; the RAM data it asked for is ready now.
1343 let op = q.control.decoded;
1344 let issuing = op.live && !resetting;
1345 // Its key's bookkeeping (batch and j for the seed request).
1346 let entry = ring_entry_kernel(q.control.ring, op.ctx);
1347 // The key's seed, read from its context.
1348 let seed = q.contexts.seed;
1349 // Default request: a single-block root hash starting from the IV, with a
1350 // full 64-byte block of zeros. Each op kind below changes what it needs.
1351 let mut request = CompressionInput {
1352 cv: IV_BITS,
1353 block: [b32(0); 16],
1354 counter_low: b32(0),
1355 counter_high: b32(0),
1356 block_len: b32(64),
1357 flags: FLAGS_SINGLE,
1358 tag: lane_tag_kernel(op.kind, op.arg, op.ctx, b16(0)),
1359 valid: issuing,
1360 };
1361 if op.kind == b4(code::SEED as u128) {
1362 // Seed: derive-key hash, keyed by the secret prefix CV, of the
1363 // 20-byte material epoch ‖ batch id ‖ key index (little-endian).
1364 // Key index within the batch = slice << SUBTREE_LEVELS | j.
1365 let index: b32 =
1366 (cfg.slice.resize::<32>() << (SUBTREE_LEVELS as u128)) | entry.j.resize::<32>();
1367 // Absolute batch id.
1368 let batch_id = cfg.first_batch + entry.batch.resize::<64>();
1369 request.cv = cfg.prefix_cv;
1370 request.block[0] = cfg.epoch.resize();
1371 request.block[1] = (cfg.epoch >> 32).resize();
1372 request.block[2] = batch_id.resize();
1373 request.block[3] = (batch_id >> 32).resize();
1374 request.block[4] = index;
1375 request.block_len = b32(20);
1376 request.flags = FLAGS_SEED;
1377 } else if op.kind == b4(code::PK_NONCE as u128) || op.kind == b4(code::NONCE as u128) {
1378 // Nonces: hash of a 4-byte prefix word and the 32-byte seed (36 bytes).
1379 // The prefix word separates the public and the private nonce.
1380 request.block[0] = if op.kind == b4(code::PK_NONCE as u128) {
1381 PK_NONCE_WORD
1382 } else {
1383 NONCE_WORD
1384 };
1385 for w in 0..8 {
1386 request.block[1 + w] = seed[w];
1387 }
1388 request.block_len = b32(36);
1389 } else if op.kind == b4(code::XOF as u128) {
1390 // XOF block n: hash of the 32-byte seed with the BLAKE3 block counter
1391 // set to n, so each n gives 64 new bytes of the stream.
1392 for w in 0..8 {
1393 request.block[w] = seed[w];
1394 }
1395 request.counter_low = op.arg.resize();
1396 request.block_len = b32(32);
1397 } else if op.kind == b4(code::CHUNK0 as u128) || op.kind == b4(code::CHUNK1 as u128) {
1398 // Commitment blocks. Chunk 0 has blocks 0..16, chunk 1 blocks 0..4.
1399 let chunk1 = op.kind == b4(code::CHUNK1 as u128);
1400 // First block of a chunk starts from the IV; later blocks continue
1401 // from the chunk's chaining value stored in the context.
1402 let first = op.arg == b5(0);
1403 let last = if chunk1 {
1404 op.arg == b5(3)
1405 } else {
1406 op.arg == b5((CHUNK0_BLOCKS - 1) as u128)
1407 };
1408 if !first {
1409 request.cv = if chunk1 {
1410 q.contexts.cv1
1411 } else {
1412 q.contexts.cv0
1413 };
1414 }
1415 // The 64-byte commitment word, read from the commitment RAM.
1416 request.block = words_from_bytes_kernel(q.commit_ram);
1417 // BLAKE3's chunk counter: 0 for chunk 0, 1 for chunk 1.
1418 request.counter_low = if chunk1 { b32(1) } else { b32(0) };
1419 // The commitment is 1,240 = 19 × 64 + 24 bytes: the last block is short.
1420 request.block_len = if chunk1 && last { b32(24) } else { b32(64) };
1421 // Not a root here: the parent below is the root.
1422 let mut flags = b32(0);
1423 if first {
1424 flags = flags | FLAG_START;
1425 }
1426 if last {
1427 flags = flags | FLAG_END;
1428 }
1429 request.flags = flags;
1430 } else if op.kind == b4(code::PARENT as u128) {
1431 // Parent: the two chunk chaining values side by side form the block.
1432 for w in 0..8 {
1433 request.block[w] = q.contexts.cv0[w];
1434 request.block[8 + w] = q.contexts.cv1[w];
1435 }
1436 request.flags = FLAGS_PARENT;
1437 } else if op.kind == b4(code::MERKLE as u128) {
1438 // External slots: the caller supplies the whole block and its tag.
1439 request.block = input.merkle.block;
1440 request.tag = lane_tag_kernel(op.kind, op.arg, op.ctx, input.merkle.tag);
1441 } else if op.kind == b4(code::MESSAGE as u128) {
1442 request.block = input.message.block;
1443 request.tag = lane_tag_kernel(op.kind, op.arg, op.ctx, input.message.tag);
1444 if TWO_BLOCK_MESSAGE {
1445 if input.message.tail {
1446 request.cv = input.message.cv;
1447 request.block_len = b32(48);
1448 request.flags = FLAGS_END_ROOT;
1449 } else {
1450 request.flags = FLAG_START;
1451 }
1452 }
1453 }
1454 // Tell the callers their request entered the lane this cycle.
1455 let merkle_taken = issuing && op.kind == b4(code::MERKLE as u128);
1456 let message_taken = issuing && op.kind == b4(code::MESSAGE as u128);
1457
1458 // ------------------------------------------------------ result path
1459 // Lane output → result register (this cycle) → RAM writes (next cycle).
1460 control.result_valid = q.lane.valid && !resetting;
1461 data.result = q.lane.words;
1462 data.result_tag = q.lane.tag;
1463 // Unpack the registered result's tag: what op, which argument, which key.
1464 let r_kind: b4 = q.data.result_tag.resize();
1465 let r_ctx: Ctx = (q.data.result_tag >> 9).resize();
1466 let r_arg: b5 = (q.data.result_tag >> 4).resize();
1467 let r_valid = q.control.result_valid;
1468 let result = q.data.result;
1469 let result_bytes = bytes_from_words_kernel(result);
1470 // Nonces are the first 16 bytes of their hash output.
1471 let mut first16 = [b8(0); 16];
1472 for byte in 0..16 {
1473 first16[byte] = result_bytes[byte];
1474 }
1475 // Seeds and chaining values are the first 8 words.
1476 let mut cv8 = [b32(0); 8];
1477 for w in 0..8 {
1478 cv8[w] = result[w];
1479 }
1480 // Meta reads for a parent result are addressed when it leaves the lane.
1481 // (So `pkn_meta` and `nonce` output that key's values next cycle, when
1482 // the parent result sits in the result register.)
1483 let lane_ctx: Ctx = (q.lane.tag >> 9).resize();
1484 // Bookkeeping of the result's key.
1485 let r_entry = ring_entry_kernel(q.control.ring, r_ctx);
1486 // pk_hash is the first 32 bytes of the parent output.
1487 let mut pk_hash = [b8(0); 32];
1488 for byte in 0..32 {
1489 pk_hash[byte] = result_bytes[byte];
1490 }
1491 // A parent result means the key is complete.
1492 let key_done = KeyDone {
1493 valid: r_valid && r_kind == b4(code::PARENT as u128),
1494 slot: r_entry.slot,
1495 batch: r_entry.batch,
1496 j: r_entry.j,
1497 pk_hash,
1498 pk_nonce: q.contexts.pkn_meta,
1499 nonce: q.contexts.nonce,
1500 };
1501 // Merkle and message results go back to their callers, with the
1502 // caller's 16-bit tag from the top half of the lane tag.
1503 let slot_result = SlotResult {
1504 valid: r_valid
1505 && (r_kind == b4(code::MERKLE as u128) || r_kind == b4(code::MESSAGE as u128)),
1506 message: r_kind == b4(code::MESSAGE as u128),
1507 words: result,
1508 tag: (q.data.result_tag >> 16).resize(),
1509 };
1510
1511 // ---------------------------------------------------- chain feed (T+i)
1512 // Window loads: address at residue first_chain(t) − 2, data next cycle.
1513 // This residue's row of the window-load table.
1514 let mut load = WindowLoad::default();
1515 for r in 0..KEY_PERIOD {
1516 if residue == b7(r as u128) {
1517 load = WINDOW_LOADS[r];
1518 }
1519 }
1520 // Which key the block belongs to: this period's key, or (block 0) the
1521 // next period's key.
1522 let feed_period: Ctx = if load.next_key {
1523 q.control.period + CTX_ONE
1524 } else {
1525 q.control.period
1526 };
1527 // XOF parity storage: (key parity, block).
1528 let xof_read: b6 = (feed_period.resize::<6>() & b6(1)) << 5 | load.block.resize::<6>();
1529 // Remember to capture the RAM data next cycle, and into which half.
1530 control.window_pending = load.active;
1531 control.window_parity = (load.block & b5(1)) == b5(1);
1532 // The read addressed last cycle has arrived: store it in the window.
1533 if q.control.window_pending {
1534 let block = bytes_from_words_kernel(q.xof_ram);
1535 if q.control.window_parity {
1536 data.window[1] = block;
1537 } else {
1538 data.window[0] = block;
1539 }
1540 }
1541 // pk_nonce of the next key: address at residue 66, latch at 67.
1542 control.latch_pending = residue == b7((KEY_PERIOD - 2) as u128);
1543 // At residue 67: the current pk_nonce becomes history (shift by one,
1544 // oldest dropped), and the next key's pk_nonce becomes current, ready
1545 // for its chain 0 at residue 0.
1546 if q.control.latch_pending {
1547 data.older_nonces[0] = q.data.pk_nonce;
1548 for k in 1..PACK_HISTORY {
1549 data.older_nonces[k] = q.data.older_nonces[k - 1];
1550 }
1551 data.pk_nonce = q.contexts.pkn_feed;
1552 }
1553 // Where this cycle's chain secret sits in the window.
1554 let mut pick = ChainPick::default();
1555 for r in 0..KEY_PERIOD {
1556 if residue == b7(r as u128) {
1557 pick = CHAIN_PICKS[r];
1558 }
1559 }
1560 // Order the two window halves as (block with the first byte, next block).
1561 let (low, high) = if pick.low_parity {
1562 (q.data.window[1], q.data.window[0])
1563 } else {
1564 (q.data.window[0], q.data.window[1])
1565 };
1566 // The key whose chains run in this period is the key numbered `period`.
1567 let feed_ctx = q.control.period;
1568 let feed_entry = ring_entry_kernel(q.control.ring, feed_ctx);
1569 // At residue i we feed chain i (KEY_PERIOD = CHAINS = 68).
1570 let chain_index: b7 = residue;
1571 // Bubbles feed nothing.
1572 let feeding = feed_entry.real && residue < b7(CHAINS as u128) && !resetting;
1573 // Chain tag: chain in bits 0..7, cache slot in bits 7..16, context from
1574 // bit 16. The engine returns it unchanged with the finished chain.
1575 let chain_tag: b32 = chain_index.resize::<32>()
1576 | (feed_entry.slot.resize::<32>() << 7)
1577 | (feed_ctx.resize::<32>() << 16);
1578
1579 // --------------------------------------------- chain results → cache
1580 // A finished chain from the engine, and its unpacked tag.
1581 let out = q.chains;
1582 let out_chain: b7 = out.tag.resize();
1583 let out_slot: b9 = (out.tag >> 7).resize();
1584 let out_ctx: Ctx = (out.tag >> 16).resize();
1585 let cache_write = CacheWrite {
1586 valid: out.valid,
1587 slot: out_slot,
1588 chain: out_chain,
1589 nodes: out.nodes,
1590 };
1591 // Chain 0 of a key starts a new commitment stream; remember its context
1592 // so the packer's words land in that key's part of the commitment RAM.
1593 let starting = out.valid && out_chain == b7(0);
1594 if starting {
1595 control.pack_ctx = out_ctx;
1596 }
1597 // The emerging key's pk_nonce: current, or `age` keys back.
1598 // age 0 = this period's key (current pk_nonce); age k + 1 =
1599 // older_nonces[k]. With a long chain hash, ends emerge periods late.
1600 let age: Ctx = q.control.period - out_ctx;
1601 let mut pack_nonce = q.data.pk_nonce;
1602 for k in 0..PACK_HISTORY {
1603 if age == CTX_INDEX[k + 1] {
1604 pack_nonce = q.data.older_nonces[k];
1605 }
1606 }
1607 // The packer's prefix: pk_nonce in the first 16 bytes, rest unused.
1608 let mut prefix = [b8(0); 64];
1609 for byte in 0..16 {
1610 prefix[byte] = pack_nonce[byte];
1611 }
1612 // Packer words → commitment RAM at (ctx, word).
1613 let packed = q.packer;
1614 let pack_ctx_wide: CommitAddress = q.control.pack_ctx.resize();
1615 let pack_word_wide: CommitAddress = q.control.pack_word.resize();
1616 let pack_address: CommitAddress = (pack_ctx_wide << 5) | pack_word_wide;
1617 // Advance the word index; wrap to 0 after the key's last word.
1618 if packed.valid {
1619 control.pack_word = if packed.last {
1620 b5(0)
1621 } else {
1622 q.control.pack_word + b5(1)
1623 };
1624 }
1625
1626 // --------------------------------------------------------- RAM ports
1627 // Each result kind is written to its own RAM, at its key's context.
1628 let write_seed = r_valid && r_kind == b4(code::SEED as u128);
1629 let write_pkn = r_valid && r_kind == b4(code::PK_NONCE as u128);
1630 let write_nonce = r_valid && r_kind == b4(code::NONCE as u128);
1631 let write_xof = r_valid && r_kind == b4(code::XOF as u128);
1632 let write_cv0 = r_valid && r_kind == b4(code::CHUNK0 as u128);
1633 let write_cv1 = r_valid && r_kind == b4(code::CHUNK1 as u128);
1634 // XOF RAM address: key parity in bit 5, block number in bits 0..5.
1635 let xof_write: b6 = (r_ctx.resize::<6>() & b6(1)) << 5 | r_arg.resize::<6>();
1636
1637 // Next-cycle inputs of every child and register.
1638 let d = SliceFactoryD::<TABLE_MASK> {
1639 // The request built in the issue section.
1640 lane: request,
1641 // This cycle's chain: its secret from the window, the key's salt.
1642 chains: SliceChainInput {
1643 secret: pick_secret_kernel(low, high, pick.offset),
1644 pk_nonce: q.data.pk_nonce,
1645 index: chain_index,
1646 tag: chain_tag,
1647 valid: feeding,
1648 },
1649 // Only the chain end (position 3) goes into the commitment.
1650 packer: PackerInput {
1651 prefix,
1652 start: starting,
1653 node: out.nodes[3],
1654 valid: out.valid,
1655 },
1656 // Reads use the decoded op's context (`ctx`) except where noted;
1657 // writes use the result's context (`r_ctx`).
1658 contexts: ContextRamsIn {
1659 seed: RamIn::<[b32; 8], CTX_BITS> {
1660 read_addr: ctx,
1661 write: RamWrite::<[b32; 8], CTX_BITS> {
1662 addr: r_ctx,
1663 value: cv8,
1664 enable: write_seed,
1665 },
1666 },
1667 // Always reads the next key's pk_nonce, for the latch at 67.
1668 pkn_feed: RamIn::<[b8; 16], CTX_BITS> {
1669 read_addr: q.control.period + CTX_ONE,
1670 write: RamWrite::<[b8; 16], CTX_BITS> {
1671 addr: r_ctx,
1672 value: first16,
1673 enable: write_pkn,
1674 },
1675 },
1676 // Reads follow the lane output, for the KeyDone metadata.
1677 pkn_meta: RamIn::<[b8; 16], CTX_BITS> {
1678 read_addr: lane_ctx,
1679 write: RamWrite::<[b8; 16], CTX_BITS> {
1680 addr: r_ctx,
1681 value: first16,
1682 enable: write_pkn,
1683 },
1684 },
1685 nonce: RamIn::<[b8; 16], CTX_BITS> {
1686 read_addr: lane_ctx,
1687 write: RamWrite::<[b8; 16], CTX_BITS> {
1688 addr: r_ctx,
1689 value: first16,
1690 enable: write_nonce,
1691 },
1692 },
1693 cv0: RamIn::<[b32; 8], CTX_BITS> {
1694 read_addr: ctx,
1695 write: RamWrite::<[b32; 8], CTX_BITS> {
1696 addr: r_ctx,
1697 value: cv8,
1698 enable: write_cv0,
1699 },
1700 },
1701 cv1: RamIn::<[b32; 8], CTX_BITS> {
1702 read_addr: ctx,
1703 write: RamWrite::<[b32; 8], CTX_BITS> {
1704 addr: r_ctx,
1705 value: cv8,
1706 enable: write_cv1,
1707 },
1708 },
1709 },
1710 // XOF RAM: window reads, XOF result writes.
1711 xof_ram: RamIn::<[b32; 16], 6> {
1712 read_addr: xof_read,
1713 write: RamWrite::<[b32; 16], 6> {
1714 addr: xof_write,
1715 value: result,
1716 enable: write_xof,
1717 },
1718 },
1719 // Commitment RAM: chunk-op reads, packer writes.
1720 commit_ram: RamIn::<[b8; 64], { CTX_BITS + 5 }> {
1721 read_addr: commit_address,
1722 write: RamWrite::<[b8; 64], { CTX_BITS + 5 }> {
1723 addr: pack_address,
1724 value: packed.word,
1725 enable: packed.valid,
1726 },
1727 },
1728 control,
1729 data,
1730 };
1731 (
1732 SliceFactoryOutput {
1733 cache_write,
1734 key_done,
1735 slot_result,
1736 merkle_taken,
1737 message_taken,
1738 keys_started: q.control.started,
1739 },
1740 d,
1741 )
1742}