Skip to main content

rhdl_dsig_scalable_core/
cert_unit.rs

1//! The engine's only shared logic: the Merkle top and the Ed25519 certificate.
2//!
3//! # The problem
4//!
5//! Every batch of 128 one-time keys needs one *certificate*: an Ed25519
6//! signature over the batch's Merkle root, so that a verifier who trusts the
7//! signer's Ed25519 public key can trust all 128 keys. The keys of a batch are
8//! spread over all the slices, and each slice can only hash its own
9//! [subtree](crate::slice_merkle). Somebody has to join the subtree roots into
10//! the batch root and get it signed. That is this unit, and it is the only
11//! piece of logic all slices share.
12//!
13//! ```text
14//!   slice 0 ─ subtree root r0 ─┐                                  ┌─▶ slice 0: certificate
15//!   slice 1 ─ subtree root r1 ─┤   Merkle top   ──▶  Ed25519  ──▶ ├─▶ slice 1: certificate
16//!     …                        ├─▶ (this unit)       (this unit)  │     …
17//!   slice 7 ─ subtree root r7 ─┘                                  └─▶ slice 7: certificate
18//! ```
19//!
20//! # Joining the subtrees
21//!
22//! Each [slice](crate::slice) builds one subtree of every batch (see
23//! [`KEYS_PER_SLICE`](crate::KEYS_PER_SLICE)). This unit joins the
24//! [`SLICES`](crate::SLICES) subtree roots into the batch root, using the
25//! classic array ("heap") layout of a binary tree:
26//!
27//! ```text
28//!   tree[1]                     root               (index 1 is the root)
29//!   tree[2], tree[3]          ┌──┴──┐               parent i = H(tree[2i] ‖ tree[2i+1])
30//!   …                                              children of i are 2i and 2i + 1
31//!   tree[SLICES + s]        slice roots r0 … r(SLICES−1)
32//! ```
33//!
34//! For the 8-slice build that is 4 + 2 + 1 compressions (levels 5, 6, 7); for
35//! 2 slices it is a single one. They run one tree level at a time.
36//!
37//! ```text
38//!   8-slice heap, by index
39//!                          [1] batch root
40//!               [2]                          [3]
41//!        [4]           [5]            [6]           [7]
42//!     [8]   [9]    [10]  [11]     [12]  [13]    [14]  [15]
43//!      r0    r1     r2    r3       r4    r5      r6    r7
44//! ```
45//!
46//! The heap layout makes "my sibling" and "my parent" plain bit operations
47//! (`i ^ 1`, `i >> 1`), which is what hardware wants.
48//!
49//! # Signing and handing out
50//!
51//! ```text
52//!   envelope  DSIGFP01 ‖ 1 ‖ 128 ‖ epoch ‖ batch ‖ root  ──▶ Ed25519 ──▶ signature
53//! ```
54//!
55//! Every slice then receives the same commitment and signature, plus the
56//! [`TOP_LEVELS`](crate::TOP_LEVELS) siblings above *its* subtree: at level
57//! `t`, `tree[((SLICES + s) >> t) ^ 1]` (the other child of its ancestor).
58//! With the siblings inside its own subtree, a signature carries the full
59//! 7-sibling inclusion proof.
60//!
61//! # Why it is pipelined
62//!
63//! The Ed25519 core is barrel-threaded: one signature takes about 25,000
64//! cycles from request to result, but with ~32 requests in flight it
65//! completes one every ~895 cycles (the vendored `Ed25519SignCoreSmall`; the
66//! earlier 1,1,1,0 profile managed ~758). One certificate covers a batch of
67//! 128 keys, so 30M signatures/s at 205 MHz needs one every
68//! 128 × 205 M / 30 M ≈ 875 cycles: right at the core's rate, which is why
69//! this unit must keep many batches in the core at once:
70//!
71//! ```text
72//!  roots ─▶ Merkle front ──request(tag = batch)──▶ Ed25519 core (≤ 32 in flight)
73//!            │  ~100 cycles/batch                      │ results, any order
74//!            ▼                                         ▼
75//!     records[batch mod 32]                  signatures[tag mod 32], done bit
76//!     (8 roots, 6 inner nodes, envelope)               │
77//!            └──────────────┬──────────────────────────┘
78//!                           ▼
79//!          distribution, strictly in batch order ──▶ eight slices
80//! ```
81//!
82//! (*Barrel-threaded* means the core interleaves many independent signatures
83//! through one datapath, so it is slow per request but fast in total. The
84//! planner, [`crate::config`], uses the same measured ~895 cycles per
85//! signature with ≥ 32 in flight.)
86//!
87//! The front never waits for a signature, only for a free ring entry. A
88//! completion is accepted only if its tag names a requested, not yet completed
89//! batch; anything else is a sticky fault (the core's contract: reject unknown
90//! or reused tags). [`IN_FLIGHT`] = 32 matches both the core's credits and the
91//! slices' 32-batch key caches. `busy_cycles` counts cycles with any batch
92//! inside the unit.
93//!
94//! # Tags and in-order delivery
95//!
96//! Each request carries its relative batch number as a *tag*, and the core
97//! returns the tag with the signature. Results may come back out of order, so
98//! the unit files each signature by `tag mod 32` and sets a done bit. The
99//! slices, however, spend keys strictly in order, so certificates must leave
100//! in order. Distribution therefore waits for the done bit of the oldest
101//! undistributed batch, `dist`, even if younger batches are already done.
102//!
103//! ```text
104//!   ring entry:   0    1    2    3    4   …        dist = 1, front = 5
105//!   batch:        -    b1   b2   b3   b4           (b0 already distributed)
106//!   done bit:     -    0    1    1    0            b2, b3 signed early: they wait
107//!                      ▲ distribution waits here until b1's signature returns
108//!   accepted tag = a batch in [dist, front) whose done bit is still 0
109//! ```
110//!
111//! Each slice offers at most two subtree roots ahead (the root queue of each slice holds two);
112//! roots arrive in batch order because each slice finishes its keys in order.
113
114use crate::TOP_LEVELS;
115use crate::blake3_core::{CompressionInput, InlineCompressionPipeline, SINGLE_BLOCK_ROOT};
116use crate::certificate::batch_commitment_words;
117use crate::ed25519::{CertificateSigner, CertificateSignerInput, ScalarWords};
118use crate::slice_factory::{IV_BITS, bytes_from_words_kernel, words_from_bytes_kernel};
119use crate::slice_merkle::SubtreeRoot;
120use crate::slice_signer::Certificate;
121use rhdl::prelude::*;
122use rhdl_fpga::core::{
123    dff::DFF,
124    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
125};
126
127/// Slices joined by this unit (the crate-wide area knob).
128pub use crate::SLICES;
129/// Heap slots of the Merkle top: index 0 unused, `1..SLICES` inner nodes,
130/// `SLICES..2·SLICES` slice roots.
131pub const TREE_NODES: usize = 2 * SLICES;
132/// Heap index of the first node of the bottom inner level.
133///
134/// That level has `SLICES / 2` nodes, at heap indices `SLICES/2 .. SLICES`.
135const BOTTOM_LEVEL: b4 = b4((SLICES / 2) as u128);
136
137/// Flags for a 64-byte single-block root hash.
138pub const FLAGS_SINGLE_BLOCK: b32 = b32(SINGLE_BLOCK_ROOT as u128);
139
140/// Configuration, stable while running.
141#[derive(Clone, Copy, Debug, PartialEq, Digital)]
142pub struct CertConfig {
143    /// Durably allocated epoch.
144    pub epoch: b64,
145    /// Batch id of relative batch 0.
146    pub first_batch: b64,
147    /// Secret Ed25519 seed.
148    pub key_seed: ScalarWords,
149    /// Load the key and certify batches.
150    pub run: bool,
151}
152
153/// Inputs from the slices.
154#[derive(Clone, Copy, Debug, PartialEq, Digital)]
155pub struct CertInput {
156    /// Configuration.
157    pub config: CertConfig,
158    /// Subtree-root pulses, one per slice.
159    pub roots: [SubtreeRoot; SLICES],
160    /// Each slice accepted the offered certificate this cycle.
161    pub taken: [bool; SLICES],
162}
163
164/// Outputs to the slices.
165#[derive(Clone, Copy, Debug, PartialEq, Digital)]
166pub struct CertOutput {
167    /// Certificate offered to each slice (held until taken).
168    pub certificates: [Certificate; SLICES],
169    /// Ed25519 public key once loaded.
170    pub public_key: ScalarWords,
171    /// The Ed25519 key is loaded.
172    pub key_loaded: bool,
173    /// Batches fully certified and distributed.
174    pub certified: b32,
175    /// Batches whose subtree roots have left the root queues (= batches
176    /// sent to the Ed25519 core). Slices use it as root credit: a slice may
177    /// work on batch `b` only while `b < roots_popped + ROOT_QUEUE`, so no
178    /// root ever arrives at a full queue.
179    pub roots_popped: b32,
180    /// Cycles spent with a batch in progress (Merkle, signing, distribution).
181    pub busy_cycles: b64,
182    /// A root arrived for a full queue, or the signer reported an error.
183    pub fault: bool,
184}
185
186/// Phase of the Merkle front (one batch at a time, about 100 cycles each).
187#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
188pub enum Phase {
189    /// Waiting for every slice's root of the next batch.
190    #[default]
191    Idle,
192    /// Compressions of one tree level in flight (see `CertControl::level`).
193    Hashing,
194    /// Offering the signing request until the core accepts it.
195    Sign,
196}
197
198/// Batches that may be between "signature requested" and "distributed".
199///
200/// Equal to the Ed25519 core's admission credits (32 for profile 1,1,1,0) and
201/// to the slices' cache depth in batches, so neither side is the bottleneck.
202pub const IN_FLIGHT: usize = 32;
203
204/// Subtree roots each slice may have waiting in the cert unit's root queue.
205///
206/// The queue is two deep per slice. Before this credit existed, a slice
207/// offered roots as fast as it made keys (up to 8 batches ahead), so while
208/// the Ed25519 core was still loading its key a third root met a full queue,
209/// was dropped and set the sticky fault. The first card run stopped at 6 of
210/// 8 batches that way (2026-10-14). Slices now wait for credit
211/// ([`CertOutput::roots_popped`]).
212pub const ROOT_QUEUE: usize = 2;
213
214/// Batches the front may have outstanding: [`IN_FLIGHT`], or 1 with the
215/// measurement-only `cert-serial` feature, which reproduces the earlier
216/// one-batch-at-a-time unit so its cost can be measured (see the crate page,
217/// "How it got fast").
218pub const MAX_OUTSTANDING: usize = if cfg!(feature = "cert-serial") { 1 } else { IN_FLIGHT };
219
220/// Everything about one batch except its signature, kept until distribution.
221///
222/// Written into the `records` RAM at `batch mod 32` when the signing request
223/// is issued, read back when the batch is distributed.
224#[derive(Clone, Copy, Debug, PartialEq, Digital)]
225pub struct CertRecord {
226    /// The batch's Merkle top in heap layout (slice roots included).
227    pub tree: [[b8; 32]; TREE_NODES],
228    /// Signed envelope bytes.
229    pub commitment: [b8; 64],
230}
231
232/// Resettable control.
233#[derive(Clone, Copy, Debug, PartialEq, Digital)]
234pub struct CertControl {
235    /// Merkle front phase.
236    pub phase: Phase,
237    /// Roots queued per slice (0..=2).
238    pub queued: [b2; SLICES],
239    /// Heap index of the first node of the level being hashed; that level
240    /// has exactly this many nodes (`SLICES/2`, …, 2, 1).
241    pub level: b4,
242    /// Compressions issued in the current level.
243    pub issued: b4,
244    /// Results returned in the current level.
245    pub returned: b4,
246    /// The key-load request was accepted.
247    pub key_requested: bool,
248    /// Relative batch in the Merkle front (= batches requested so far).
249    pub front: b32,
250    /// Relative batch next to distribute (= batches certified so far).
251    pub dist: b32,
252    /// Signature received, per ring entry `batch mod IN_FLIGHT`.
253    pub done: [bool; IN_FLIGHT],
254    /// Distribution step: 0 wait for signature, 1 RAM read, 2 offering.
255    pub stage: b2,
256    /// Slices that took the current certificate.
257    pub delivered: [bool; SLICES],
258    /// Busy-cycle counter.
259    pub busy: b64,
260    /// Sticky fault.
261    pub fault: bool,
262}
263
264/// Resetless datapath of the Merkle front.
265#[derive(Clone, Copy, Debug, PartialEq, Digital)]
266pub struct CertData {
267    /// Two queued roots per slice, oldest in index 0.
268    pub roots: [[[b8; 32]; 2]; SLICES],
269    /// Merkle top of the batch in the front (heap layout).
270    pub tree: [[b8; 32]; TREE_NODES],
271    /// Batch root.
272    pub root: [b8; 32],
273    /// Signed envelope bytes.
274    pub commitment: [b8; 64],
275}
276
277/// The shared certificate unit.
278///
279/// One BLAKE3 lane for the Merkle top, the Ed25519 signer, two 32-entry
280/// rings (records and signatures) and the control/data registers.
281#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
282pub struct CertUnit {
283    /// Pipelined BLAKE3 compression for the Merkle top; results come back
284    /// with the heap index as tag.
285    lane: InlineCompressionPipeline,
286    /// The Ed25519 certificate signer (vendored RHDL core, see
287    /// [`crate::ed25519`]).
288    signer: CertificateSigner,
289    /// Per-batch Merkle top and envelope, indexed by `batch mod 32`.
290    records: SyncBRAM<CertRecord, 5>,
291    /// Per-batch Ed25519 signature, indexed by `tag mod 32`.
292    signatures: SyncBRAM<[b8; 64], 5>,
293    /// Phases, counters, done bits (reset to idle).
294    control: DFF<CertControl>,
295    /// Queued roots and the front's tree (no reset).
296    data: DFF<CertData>,
297}
298
299impl Default for CertUnit {
300    /// Idle front, nothing requested or distributed, key not loaded.
301    fn default() -> Self {
302        Self {
303            lane: InlineCompressionPipeline,
304            signer: CertificateSigner::default(),
305            records: SyncBRAM::default(),
306            signatures: SyncBRAM::default(),
307            control: DFF::new(CertControl {
308                phase: Phase::Idle,
309                queued: [b2(0); SLICES],
310                level: b4(0),
311                issued: b4(0),
312                returned: b4(0),
313                key_requested: false,
314                front: b32(0),
315                dist: b32(0),
316                done: [false; IN_FLIGHT],
317                stage: b2(0),
318                delivered: [false; SLICES],
319                busy: b64(0),
320                fault: false,
321            }),
322            data: DFF::resetless(),
323        }
324    }
325}
326
327/// Input: subtree roots, slice acknowledgements and configuration. Output:
328/// one certificate offer per slice, plus status.
329impl SynchronousIO for CertUnit {
330    type I = CertInput;
331    type O = CertOutput;
332    type Kernel = cert_unit_kernel;
333}
334
335/// Little-endian bytes of eight 64-bit words.
336///
337/// Turns the Ed25519 core's word-oriented envelope and signature into the
338/// 64 bytes that go into the frame. Combinational.
339#[allow(clippy::needless_range_loop)]
340#[kernel]
341pub fn words64_to_bytes_kernel(words: [b64; 8]) -> [b8; 64] {
342    let mut bytes = [b8(0); 64];
343    for w in 0..8 {
344        for b in 0..8 {
345            bytes[8 * w + b] = (words[w] >> (8 * b as u128)).resize();
346        }
347    }
348    bytes
349}
350
351/// A single-block BLAKE3 root hash of `left ‖ right`.
352///
353/// Builds the lane request for one Merkle-top node: IV key, 64-byte block,
354/// counter 0, single-block root flags. `tag` (the heap index) comes back
355/// with the result; `valid` says whether the request is real. Combinational.
356#[allow(clippy::needless_range_loop)]
357#[kernel]
358pub fn pair_request_kernel(
359    left: [b8; 32],
360    right: [b8; 32],
361    tag: b32,
362    valid: bool,
363) -> CompressionInput {
364    let mut bytes = [b8(0); 64];
365    for b in 0..32 {
366        bytes[b] = left[b];
367        bytes[32 + b] = right[b];
368    }
369    CompressionInput {
370        cv: IV_BITS,
371        block: words_from_bytes_kernel(bytes),
372        counter_low: b32(0),
373        counter_high: b32(0),
374        block_len: b32(64),
375        flags: FLAGS_SINGLE_BLOCK,
376        tag,
377        valid,
378    }
379}
380
381/// The certificate unit's wiring.
382///
383/// Four independent machines share this kernel, connected only through
384/// counters and the two RAMs:
385///
386/// 1. **root queues**: up to two roots per slice;
387/// 2. **Merkle front** (`phase`): Idle → Hashing (one level at a time, one
388///    compression per cycle) → Sign; one batch at a time;
389/// 3. **Ed25519 issue and completion**: request with `tag = front`, file
390///    results by tag, check every tag;
391/// 4. **distribution** (`stage`): wait for the oldest batch's signature,
392///    read both RAMs, offer to all slices until each has taken it.
393///
394/// Timing: the RAMs answer one cycle after their address; the lane and the
395/// Ed25519 core return results many cycles later, tagged.
396#[allow(clippy::too_many_lines, clippy::needless_range_loop)]
397#[kernel]
398pub fn cert_unit_kernel(
399    clock_reset: ClockReset,
400    input: CertInput,
401    q: CertUnitQ,
402) -> (CertOutput, CertUnitD) {
403    let resetting = clock_reset.reset.any();
404    // Next-state copies of the registers.
405    let mut c = q.control;
406    let mut data = q.data;
407    let cfg = input.config;
408
409    // ---------------------------------------------------- root queues
410    // Append each arriving root to its slice's 2-entry queue; a third root
411    // would overwrite one still needed, so it is a fault instead.
412    let mut queued = q.control.queued;
413    for s in 0..SLICES {
414        if input.roots[s].valid {
415            if queued[s] == b2(0) {
416                data.roots[s][0] = input.roots[s].root;
417                queued[s] = b2(1);
418            } else if queued[s] == b2(1) {
419                data.roots[s][1] = input.roots[s].root;
420                queued[s] = b2(2);
421            } else {
422                c.fault = true;
423            }
424        }
425    }
426    // Every slice has delivered its root of the next batch.
427    let mut all_ready = true;
428    for s in 0..SLICES {
429        if q.control.queued[s] == b2(0) {
430            all_ready = false;
431        }
432    }
433
434    // ---------------------------------------------------- Merkle top
435    // The lane's current result: tag = heap index, first 32 bytes = node.
436    let result = q.lane;
437    let r_index: b4 = result.tag.resize();
438    let r_bytes = bytes_from_words_kernel(result.words);
439    let mut r_node = [b8(0); 32];
440    for b in 0..32 {
441        r_node[b] = r_bytes[b];
442    }
443    let phase = q.control.phase;
444    // Batches requested from Ed25519 but not yet distributed.
445    let outstanding = q.control.front - q.control.dist;
446    if phase != Phase::Idle || outstanding != b32(0) {
447        c.busy = q.control.busy + b64(1);
448    }
449    // Node `n = level + issued` of the current level reads its two children.
450    let n = q.control.level + q.control.issued;
451    // Children 2n and 2n + 1 (constant-index loop for RHDL).
452    let mut left = q.data.tree[0];
453    let mut right = q.data.tree[0];
454    for k in 1..SLICES {
455        if n == b4(k as u128) {
456            left = q.data.tree[2 * k];
457            right = q.data.tree[2 * k + 1];
458        }
459    }
460    // The lane request for node n; only sent if `valid` is set below.
461    let mut lane = pair_request_kernel(left, right, n.resize(), false);
462    // Set when this batch's roots are no longer needed (request issued).
463    let mut pop_roots = false;
464    if phase == Phase::Idle {
465        if all_ready && cfg.run {
466            // Copy the oldest root of every slice into the heap's leaves.
467            for s in 0..SLICES {
468                data.tree[SLICES + s] = q.data.roots[s][0];
469            }
470            c.phase = Phase::Hashing;
471            c.level = BOTTOM_LEVEL;
472            c.issued = b4(0);
473            c.returned = b4(0);
474        }
475    } else if phase == Phase::Hashing {
476        // Issue one compression per cycle until the whole level is issued.
477        if q.control.issued < q.control.level {
478            lane.valid = true;
479            c.issued = q.control.issued + b4(1);
480        }
481        // Store each result at its heap index.
482        if result.valid {
483            for k in 1..SLICES {
484                if r_index == b4(k as u128) {
485                    data.tree[k] = r_node;
486                }
487            }
488            c.returned = q.control.returned + b4(1);
489            // Level complete: either it was the root, or move one level up.
490            if q.control.returned + b4(1) == q.control.level {
491                if q.control.level == b4(1) {
492                    // tree[1] is the batch root.
493                    data.root = r_node;
494                    let batch_id = cfg.first_batch + q.control.front.resize::<64>();
495                    data.commitment = words64_to_bytes_kernel(batch_commitment_words(
496                        cfg.epoch, batch_id, r_node,
497                    ));
498                    c.phase = Phase::Sign;
499                } else {
500                    c.level = q.control.level >> 1;
501                    c.issued = b4(0);
502                    c.returned = b4(0);
503                }
504            }
505        }
506    }
507
508    // ---------------------------------------------------- Ed25519 issue
509    // The core keeps ~32 requests in flight and may finish them out of
510    // order, so the front never waits for a signature: it only needs a free
511    // ring entry (fewer than IN_FLIGHT batches undistributed).
512    let signed = q.signer;
513    // Load the secret key once, the first time the core is ready for it.
514    let load_key = cfg.run && !q.control.key_requested && signed.load_key_ready;
515    if load_key {
516        c.key_requested = true;
517    }
518    // A ring entry is free for one more batch.
519    let ring_free = outstanding < b32(MAX_OUTSTANDING as u128);
520    // Offer the signing request; `issued` is the handshake completing.
521    let requesting = phase == Phase::Sign && signed.key_loaded && ring_free && !resetting;
522    let issued = requesting && signed.ready;
523    // Ring entry of the batch in the front: `front mod 32`.
524    let front_entry: b5 = q.control.front.resize();
525    let record = CertRecord {
526        tree: q.data.tree,
527        commitment: q.data.commitment,
528    };
529    if issued {
530        // The record is written at `front_entry` (below); the front is free
531        // for the next batch.
532        c.phase = Phase::Idle;
533        c.front = q.control.front + b32(1);
534        pop_roots = true;
535    }
536    if signed.internal_error {
537        c.fault = true;
538    }
539
540    // ---------------------------------------------------- Ed25519 completion
541    // Accept a tag only if it names a requested, not yet completed batch.
542    // Ring entry the returned signature belongs to.
543    let done_entry: b5 = signed.tag.resize();
544    // Distance from the oldest undistributed batch (wraps, so "too old" and
545    // "never requested" both come out ≥ outstanding).
546    let age: b16 = signed.tag - q.control.dist.resize::<16>();
547    // The entry's done bit is already set: a duplicate result.
548    let mut already = false;
549    for e in 0..IN_FLIGHT {
550        if done_entry == b5(e as u128) && q.control.done[e] {
551            already = true;
552        }
553    }
554    let completion_ok = age < outstanding.resize::<16>() && !already;
555    let completing = signed.valid && completion_ok && !resetting;
556    if signed.valid && !completion_ok {
557        c.fault = true;
558    }
559    if completing {
560        for e in 0..IN_FLIGHT {
561            if done_entry == b5(e as u128) {
562                c.done[e] = true;
563            }
564        }
565    }
566
567    // ---------------------------------------------------- distribution
568    // Strictly in batch order. Both RAMs are always read at `dist mod 32`;
569    // their registered outputs stay stable while that entry is offered.
570    let dist_entry: b5 = q.control.dist.resize();
571    // The oldest undistributed batch has its signature.
572    let mut head_done = false;
573    for e in 0..IN_FLIGHT {
574        if dist_entry == b5(e as u128) && q.control.done[e] {
575            head_done = true;
576        }
577    }
578    let stage = q.control.stage;
579    if stage == b2(0) && head_done {
580        c.stage = b2(1);
581    } else if stage == b2(1) {
582        // RAM outputs are valid from the next cycle; nobody has it yet.
583        c.stage = b2(2);
584        c.delivered = [false; SLICES];
585    }
586    let offering = stage == b2(2);
587    // The record of batch `dist` (tree + envelope) from the records RAM.
588    let head = q.records;
589    // The same certificate for every slice; only `valid` and `top` differ.
590    let mut certificates = [Certificate {
591        valid: false,
592        batch: q.control.dist,
593        commitment: head.commitment,
594        signature: q.signatures,
595        top: [[b8(0); 32]; TOP_LEVELS],
596    }; SLICES];
597    for s in 0..SLICES {
598        certificates[s].valid = offering && !q.control.delivered[s] && !resetting;
599        // Sibling at top level t: the other child of slice s's ancestor.
600        for t in 0..TOP_LEVELS {
601            certificates[s].top[t] = head.tree[((SLICES + s) >> t) ^ 1];
602        }
603    }
604    if offering {
605        // Track who has taken it; finish when every slice has.
606        let mut all = true;
607        for s in 0..SLICES {
608            let got = q.control.delivered[s] || input.taken[s];
609            c.delivered[s] = got;
610            if !got {
611                all = false;
612            }
613        }
614        if all {
615            // Batch distributed: free its ring entry for a new batch.
616            c.stage = b2(0);
617            c.dist = q.control.dist + b32(1);
618            for e in 0..IN_FLIGHT {
619                if dist_entry == b5(e as u128) {
620                    c.done[e] = false;
621                }
622            }
623        }
624    }
625    // Drop each slice's oldest root (the one just requested).
626    if pop_roots {
627        for s in 0..SLICES {
628            data.roots[s][0] = data.roots[s][1];
629            queued[s] = queued[s] - b2(1);
630        }
631    }
632    c.queued = queued;
633
634    // Absolute batch id of the batch in the front.
635    let batch_id = cfg.first_batch + q.control.front.resize::<64>();
636    let d = CertUnitD {
637        lane,
638        // Signing request for the front batch, tagged with its relative
639        // batch number.
640        signer: CertificateSignerInput {
641            load_key_valid: load_key,
642            key_seed: cfg.key_seed,
643            clear_key: false,
644            valid: requesting,
645            epoch: cfg.epoch,
646            batch_id,
647            root: q.data.root,
648            tag: q.control.front.resize(),
649        },
650        // Write the front's record on issue; read the distribution head.
651        records: RamIn::<CertRecord, 5> {
652            read_addr: dist_entry,
653            write: RamWrite::<CertRecord, 5> {
654                addr: front_entry,
655                value: record,
656                enable: issued,
657            },
658        },
659        // File each accepted signature by its tag; read the distribution head.
660        signatures: RamIn::<[b8; 64], 5> {
661            read_addr: dist_entry,
662            write: RamWrite::<[b8; 64], 5> {
663                addr: done_entry,
664                value: words64_to_bytes_kernel(signed.signature.words),
665                enable: completing,
666            },
667        },
668        control: c,
669        data,
670    };
671    (
672        CertOutput {
673            certificates,
674            public_key: signed.public_key,
675            key_loaded: signed.key_loaded,
676            certified: q.control.dist,
677            roots_popped: q.control.front,
678            busy_cycles: q.control.busy,
679            fault: q.control.fault,
680        },
681        d,
682    )
683}