Skip to main content

rhdl_dsig_scalable_core/verify/
datapath.rs

1//! One verify core: records in, verdict bytes out, in order.
2//!
3//! # What it joins
4//!
5//! ```text
6//!               beats (26 per record)
7//!                    │
8//!               ┌────▼─────┐ header rows, check row ┌────────────┐
9//!               │  loader  ├───────────────────────▶│   hasher   │◀── rows ──┐
10//!               │          │ loaded                 │ (BLAKE3)   │── digits ─┤
11//!               │          ├── nodes, pk_nonce ────────────────────────▶ ┌───┴────┐
12//!               │          │                        └─────┬──────┘       │ chains │
13//!               │          ├── commitment, signature ─▶ ┌─┴─────┐ done   └────────┘
14//!               └──────────┘                            │ certs │◀── Ed25519 answers
15//!                                                       └───┬───┘
16//!      hash retirement (oldest first): done? ──▶ verdict queue (4,096) ──▶ certificate settled? ──▶ verdict byte
17//! ```
18//!
19//! * The **loader** takes the record's 26 beats one by one and scatters them:
20//!   header rows and the check row to the [hasher](super::hasher), nodes and
21//!   `pk_nonce` to the [chain block](super::chains), the commitment and
22//!   signature to the [certificate cache](super::certs).
23//! * A record's **context** is its number mod 64; a record may start loading
24//!   only while fewer than 64 records are in flight.
25//! * **Retirement** takes records strictly in order. The oldest leaves when
26//!   the hasher has both comparisons and its certificate answer is known;
27//!   its verdict byte goes out and its context is cleared everywhere.
28//!
29//! # Where each field of a record goes
30//!
31//! | Beat | Bytes used | Goes to |
32//! |---|---|---|
33//! | 0 | header 0..16, root 32..64 | format check; check row; cache (commitment) |
34//! | 1 | `R ‖ S` | cache (signature) |
35//! | 2 | index, `pk_hash`, `pk_nonce`, nonce 0..14 | format check; check row; chain block |
36//! | 3 | nonce 14..16, sibling 0 | header row 1 (`sib0 ‖ pk_hash`) |
37//! | 4, 5, 6 | siblings 1..6 | header rows 2, 3, 4 |
38//! | 6..25 | chain nodes | node banks (`c mod 6`), up to four per beat |
39//! | 25 | message | header row 0 (`pk_hash ‖ nonce ‖ message`); record loaded |
40
41use crate::verify::certs::{CertRequest, CertResult, CertsInput, ENTRIES, PENDING, VERIFIED, VerifyCerts};
42use crate::verify::chains::{
43    CONTEXTS, ChainsInput, LANES, NodeWrite, NonceWrite, Retire, VerifyChains,
44};
45use crate::verify::hasher::{CheckWrite, HeaderWrite, HasherInput, Loaded, VerifyHasher};
46use crate::verify::{RECORD_BEATS, VERDICT_CERT, VERDICT_CHAINS, VERDICT_FORMAT, VERDICT_MERKLE, VERDICT_WRITTEN};
47use crate::{CHAINS, ELEMENT_BYTES};
48use rhdl::prelude::*;
49use rhdl_fpga::core::{
50    dff::DFF,
51    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
52};
53
54/// Verdicts that may wait for their certificate check (`2^12`): enough for
55/// 143,000 cycles of Ed25519 latency at 35 cycles per record (the check
56/// takes 52,000–83,000 cycles, Ed25519 project, native simulation).
57pub const PENDING_VERDICTS: usize = 4096;
58/// Room kept free in the verdict queue (a write may be in flight).
59const QUEUE_LIMIT: b13 = b13((PENDING_VERDICTS - 2) as u128);
60/// Queue entry bit: the certificate answer was still pending.
61const Q_PENDING: b16 = b16(1 << 4);
62/// Queue entry bit: the certificate is known bad.
63const Q_CERT_BAD: b16 = b16(1 << 3);
64/// Node banks: chain `c` is in bank `c mod 6`.
65const NODE_BANKS: usize = 2 * LANES;
66/// Byte offset of chain node 0 in a record.
67const NODES_AT: usize = 418;
68/// The last beat of a record.
69const LAST_BEAT: usize = RECORD_BEATS - 1;
70/// The beat with the message's first 16 bytes (record 1642..1658).
71const MESSAGE_BEAT: usize = 25;
72/// Profile 2: the message continues into a 27th beat, hashed as a second
73/// digest block (see [`super::hasher`]).
74const TWO_BLOCK: bool = crate::MESSAGE_BYTES == 64;
75
76/// One beat of the record stream.
77#[derive(Clone, Copy, Debug, PartialEq, Digital)]
78pub struct Beat {
79    /// A beat is presented.
80    pub valid: bool,
81    /// Its 64 bytes.
82    pub data: [b8; 64],
83}
84
85impl Default for Beat {
86    /// No beat.
87    fn default() -> Self {
88        Self {
89            valid: false,
90            data: [b8(0); 64],
91        }
92    }
93}
94
95/// One verdict leaving the core.
96#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
97pub struct Verdict {
98    /// A verdict is presented.
99    pub valid: bool,
100    /// The verdict byte (see [`crate::verify`]).
101    pub byte: b8,
102}
103
104/// Inputs of a verify core.
105#[derive(Clone, Copy, Debug, PartialEq, Digital)]
106pub struct CoreInput {
107    /// The record stream.
108    pub beat: Beat,
109    /// The signer key slot of the record whose beat 0 is presented (0 when
110    /// one key is trusted; see [`super::certs`]).
111    pub slot: b4,
112    /// Reject records whose certificate epoch is not [`Self::expected_epoch`]
113    /// (with FORMAT): a frame from another session of the same signer
114    /// cannot verify. `false` in the batch verifier (no sessions).
115    pub check_epoch: bool,
116    /// The epoch the record's signer is using this session (with beat 0).
117    pub expected_epoch: b64,
118    /// A certificate to look up ahead of its records (see
119    /// [`super::certs`], warm lookups); `valid: false` when unused.
120    pub warm: CertWarm,
121    /// The verdict consumer can take a verdict this cycle.
122    pub verdict_ready: bool,
123    /// The Ed25519 core took the presented certificate request.
124    pub cert_taken: bool,
125    /// An Ed25519 answer for this core.
126    pub cert_result: CertResult,
127}
128
129/// A certificate announced ahead of its records.
130#[derive(Clone, Copy, Debug, PartialEq, Digital)]
131pub struct CertWarm {
132    /// Presented (held until `warm_taken`).
133    pub valid: bool,
134    /// The signer's key slot.
135    pub slot: b4,
136    /// The 64-byte commitment.
137    pub commitment: [b8; 64],
138    /// The Ed25519 signature.
139    pub signature: [b8; 64],
140}
141
142impl CertWarm {
143    /// No certificate.
144    #[must_use]
145    pub fn none() -> Self {
146        Self {
147            valid: false,
148            slot: b4(0),
149            commitment: [b8(0); 64],
150            signature: [b8(0); 64],
151        }
152    }
153}
154
155/// Outputs of a verify core.
156#[derive(Clone, Copy, Debug, PartialEq, Digital)]
157pub struct CoreOutput {
158    /// The core takes the presented beat this cycle.
159    pub beat_ready: bool,
160    /// The core took the presented warm certificate this cycle.
161    pub warm_taken: bool,
162    /// The next verdict, in record order.
163    pub verdict: Verdict,
164    /// A certificate check for the Ed25519 core.
165    pub cert_request: CertRequest,
166    /// Records retired.
167    pub retired: b32,
168    /// Retired records that were valid.
169    pub valid: b32,
170    /// Certificate checks issued.
171    pub cert_checks: b32,
172    /// Certificate checks that failed.
173    pub cert_fails: b32,
174    /// The chain lanes are loaded.
175    pub ready: bool,
176    /// Activity this cycle, for utilization counters.
177    pub activity: Activity,
178}
179
180/// What a core's shared resources did this cycle.
181#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
182pub struct Activity {
183    /// A compression entered the BLAKE3 lane.
184    pub blake3: bool,
185    /// A chain step entered each chain lane.
186    pub lanes: [bool; LANES],
187    /// The loader took a beat.
188    pub beat: bool,
189    /// A beat was offered but the loader could not take it.
190    pub beat_stall: bool,
191}
192
193/// Where each node is written: `(has, window offset, k / 2)` per beat and bank.
194///
195/// The window is the previous beat followed by the current one, so it
196/// starts at record byte `64·(beat − 1)`. Node `c` (record bytes
197/// `418 + 18c ..+ 18`) is written in the beat where it ends.
198pub const NODE_MAP: [[(bool, usize, usize); NODE_BANKS]; RECORD_BEATS] = node_map();
199
200/// Computes [`NODE_MAP`] and checks that no beat writes two nodes to one bank.
201const fn node_map() -> [[(bool, usize, usize); NODE_BANKS]; RECORD_BEATS] {
202    let mut map = [[(false, 0, 0); NODE_BANKS]; RECORD_BEATS];
203    let mut chain = 0;
204    while chain < CHAINS {
205        let start = NODES_AT + ELEMENT_BYTES * chain;
206        let beat = (start + ELEMENT_BYTES - 1) / 64;
207        let bank = chain % NODE_BANKS;
208        assert!(!map[beat][bank].0, "two nodes of one beat share a bank");
209        map[beat][bank] = (true, start + 64 - 64 * beat, chain / NODE_BANKS);
210        chain += 1;
211    }
212    map
213}
214
215/// [`NODE_MAP`]'s first column.
216const NODE_HAS: [[bool; NODE_BANKS]; RECORD_BEATS] = node_columns().0;
217/// [`NODE_MAP`]'s second column.
218const NODE_OFFSET: [[usize; NODE_BANKS]; RECORD_BEATS] = node_columns().1;
219/// [`NODE_MAP`]'s third column, as RHDL bits.
220const NODE_SLOT: [[b4; NODE_BANKS]; RECORD_BEATS] = node_columns().2;
221
222/// Splits [`NODE_MAP`] into plain tables kernels can index.
223#[allow(clippy::type_complexity)]
224const fn node_columns() -> ([[bool; NODE_BANKS]; RECORD_BEATS], [[usize; NODE_BANKS]; RECORD_BEATS], [[b4; NODE_BANKS]; RECORD_BEATS]) {
225    let mut has = [[false; NODE_BANKS]; RECORD_BEATS];
226    let mut offset = [[0; NODE_BANKS]; RECORD_BEATS];
227    let mut slot = [[b4(0); NODE_BANKS]; RECORD_BEATS];
228    let mut b = 0;
229    while b < RECORD_BEATS {
230        let mut m = 0;
231        while m < NODE_BANKS {
232            has[b][m] = NODE_MAP[b][m].0;
233            offset[b][m] = NODE_MAP[b][m].1;
234            slot[b][m] = b4(NODE_MAP[b][m].2 as u128);
235            m += 1;
236        }
237        b += 1;
238    }
239    (has, offset, slot)
240}
241
242/// [`VERDICT_WRITTEN`] as RHDL bits.
243const BIT_WRITTEN: b8 = b8(VERDICT_WRITTEN as u128);
244/// [`VERDICT_FORMAT`] as RHDL bits.
245const BIT_FORMAT: b8 = b8(VERDICT_FORMAT as u128);
246/// [`VERDICT_MERKLE`] as RHDL bits.
247const BIT_MERKLE: b8 = b8(VERDICT_MERKLE as u128);
248/// [`VERDICT_CHAINS`] as RHDL bits.
249const BIT_CHAINS: b8 = b8(VERDICT_CHAINS as u128);
250/// [`VERDICT_CERT`] as RHDL bits.
251const BIT_CERT: b8 = b8(VERDICT_CERT as u128);
252
253/// The 16 header bytes a well-formed commitment starts with.
254const HEADER: [b8; 16] = header_bits();
255
256/// [`super::commitment_header`] as RHDL bytes.
257const fn header_bits() -> [b8; 16] {
258    let domain = *b"DSIGFP01";
259    let mut header = [b8(0); 16];
260    let mut i = 0;
261    while i < 8 {
262        header[i] = b8(domain[i] as u128);
263        i += 1;
264    }
265    header[8] = b8(crate::certificate::PROFILE_ID as u128);
266    header[12] = b8(crate::BATCH_KEYS as u128);
267    header
268}
269
270/// Resettable core state.
271#[derive(Clone, Copy, Debug, PartialEq, Digital)]
272#[doc(hidden)]
273pub struct CoreControl {
274    /// Beat number within the current record (0..26).
275    beat: b5,
276    /// A warm certificate's commitment went to the cache last cycle; its
277    /// signature goes this cycle.
278    warm_second: bool,
279    /// Records whose loading has begun.
280    started: b32,
281    /// Records whose hash work is retired (their contexts are free).
282    retired: b32,
283    /// Records queued for their verdict (equals `retired`).
284    queued: b32,
285    /// Queue entries read out.
286    fetched: b32,
287    /// Verdicts produced.
288    finalized: b32,
289    /// Valid verdicts produced.
290    valid: b32,
291    /// A queue read is in flight.
292    reading: bool,
293    /// The head register holds the oldest queued verdict.
294    head_valid: bool,
295    /// That queue entry.
296    head: b16,
297    /// Per context: the format check failed.
298    format_bad: [bool; CONTEXTS],
299    /// Per context: the certificate answer has arrived.
300    cert_known: [bool; CONTEXTS],
301    /// Per context: the answer waits on an entry.
302    cert_pending: [bool; CONTEXTS],
303    /// Per context: that entry.
304    cert_entry: [b6; CONTEXTS],
305    /// Per context: the certificate is valid (when not pending).
306    cert_ok: [bool; CONTEXTS],
307    /// Header bytes 0..16 matched in beat 0.
308    header_ok: bool,
309    /// Contexts in use, `started − retired`, kept as its own small counter
310    /// so that `room` and `in_flight` need no 32-bit compare (the compare
311    /// sat on the routed kernel's worst paths, verifier build v1).
312    occupancy: b7,
313    /// Verdicts queued and not finalized, `queued − finalized`.
314    backlog: b13,
315    /// The context retired last cycle: the chains and the hasher clear it
316    /// one cycle late, so the retirement decision does not fan out into
317    /// all three rings in the same clock. Nothing touches a freed context
318    /// sooner: its next record needs 26 beats before its first event.
319    freed_valid: bool,
320    freed_ctx: b6,
321    /// `retired` mod 64, one cycle late: the hasher's oldest-first priority
322    /// (a stale priority costs nothing).
323    oldest: b6,
324}
325
326impl Default for CoreControl {
327    /// Nothing loaded, nothing retired.
328    fn default() -> Self {
329        Self {
330            beat: b5(0),
331            warm_second: false,
332            started: b32(0),
333            retired: b32(0),
334            queued: b32(0),
335            fetched: b32(0),
336            finalized: b32(0),
337            valid: b32(0),
338            reading: false,
339            head_valid: false,
340            head: b16(0),
341            format_bad: [false; CONTEXTS],
342            cert_known: [false; CONTEXTS],
343            cert_pending: [false; CONTEXTS],
344            cert_entry: [b6(0); CONTEXTS],
345            cert_ok: [false; CONTEXTS],
346            header_ok: false,
347            occupancy: b7(0),
348            backlog: b13(0),
349            freed_valid: false,
350            freed_ctx: b6(0),
351            oldest: b6(0),
352        }
353    }
354}
355
356/// The loader's resetless registers.
357#[derive(Clone, Copy, Debug, PartialEq, Digital)]
358#[doc(hidden)]
359pub struct LoaderData {
360    /// The previous beat.
361    prev: [b8; 64],
362    /// The record's commitment root.
363    root: [b8; 32],
364    /// The record's `pk_hash`.
365    pk_hash: [b8; 32],
366    /// The record's message nonce.
367    nonce: [b8; 16],
368    /// The key index's low 7 bits.
369    index: b7,
370}
371
372/// One verify core.
373#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
374pub struct VerifyCore<const TABLE_MASK: usize> {
375    /// Three recirculating chain lanes and the row assembler.
376    chains: VerifyChains<TABLE_MASK>,
377    /// The BLAKE3 lane and its scheduler.
378    hasher: VerifyHasher,
379    /// The certificate cache.
380    certs: VerifyCerts,
381    /// Verdicts whose hash work is done, in record order, waiting for their
382    /// certificate check: bits 0..3 format, Merkle, chains, certificate bad;
383    /// bit 4 pending; bits 5..11 the cache entry.
384    pending: SyncBRAM<b16, 12>,
385    /// Counters and per-context flags.
386    control: DFF<CoreControl>,
387    /// Loader registers.
388    data: DFF<LoaderData>,
389}
390
391impl<const TABLE_MASK: usize> Default for VerifyCore<TABLE_MASK> {
392    /// An empty core.
393    fn default() -> Self {
394        Self {
395            chains: VerifyChains::default(),
396            hasher: VerifyHasher::default(),
397            certs: VerifyCerts::default(),
398            pending: SyncBRAM::default(),
399            control: DFF::new(CoreControl::default()),
400            data: DFF::resetless(),
401        }
402    }
403}
404
405impl<const TABLE_MASK: usize> SynchronousIO for VerifyCore<TABLE_MASK> {
406    type I = CoreInput;
407    type O = CoreOutput;
408    type Kernel = verify_core_kernel<TABLE_MASK>;
409}
410
411/// The core's logic: the loader, retirement and the wiring between blocks.
412#[allow(clippy::needless_range_loop, clippy::too_many_lines)]
413#[kernel]
414pub fn verify_core_kernel<const TABLE_MASK: usize>(
415    clock_reset: ClockReset,
416    input: CoreInput,
417    q: VerifyCoreQ<TABLE_MASK>,
418) -> (CoreOutput, VerifyCoreD<TABLE_MASK>) {
419    // No reset gating here: every register this logic feeds resets itself,
420    // and keeping the reset out of the logic cones keeps it off the routed
421    // kernel's failing paths (verifier build v1).
422    let resetting = clock_reset.reset.any();
423    let _ = resetting;
424    let mut c = q.control;
425    let mut data = q.data;
426
427    // ------------------------------------------------ hash retirement
428    // The oldest record leaves its context as soon as its hash work is done
429    // and its certificate lookup has answered (pending or not); its verdict
430    // waits in the queue.
431    let head: b6 = q.control.retired.resize();
432    let mut head_done = false;
433    let mut head_merkle = false;
434    let mut head_chains = false;
435    let mut head_format = false;
436    let mut head_known = false;
437    let mut head_pending = false;
438    let mut head_entry = b6(0);
439    let mut head_ok = false;
440    for x in 0..CONTEXTS {
441        if head == b6(x as u128) {
442            head_done = q.hasher.done[x];
443            head_merkle = q.hasher.merkle_ok[x];
444            head_chains = q.hasher.chains_ok[x];
445            head_format = q.control.format_bad[x];
446            head_known = q.control.cert_known[x];
447            head_pending = q.control.cert_pending[x];
448            head_entry = q.control.cert_entry[x];
449            head_ok = q.control.cert_ok[x];
450        }
451    }
452    let in_flight = q.control.occupancy != b7(0);
453    let queue_room = q.control.backlog < QUEUE_LIMIT;
454    let retire = in_flight && head_done && head_known && queue_room;
455    let mut entry: b16 = head_entry.resize::<16>() << 5;
456    // Bits 0..3 are the verdict's own format, Merkle and chains bits.
457    if head_format {
458        entry = entry | BIT_FORMAT.resize::<16>();
459    }
460    if !head_merkle {
461        entry = entry | BIT_MERKLE.resize::<16>();
462    }
463    if !head_chains {
464        entry = entry | BIT_CHAINS.resize::<16>();
465    }
466    if head_pending {
467        entry = entry | Q_PENDING;
468    } else if !head_ok {
469        entry = entry | Q_CERT_BAD;
470    }
471    if retire {
472        c.retired = q.control.retired + b32(1);
473        c.queued = q.control.queued + b32(1);
474    }
475    let queue_write = RamWrite::<b16, 12> {
476        addr: q.control.queued.resize(),
477        value: entry,
478        enable: retire,
479    };
480
481    // ------------------------------------------------ verdicts, in order
482    // Refill the head register from the queue (one-cycle read).
483    let mut queue_read: b12 = q.control.fetched.resize();
484    if q.control.reading {
485        c.head = q.pending;
486        c.head_valid = true;
487        c.reading = false;
488    } else if !q.control.head_valid && q.control.fetched != q.control.queued {
489        queue_read = q.control.fetched.resize();
490        c.reading = true;
491        c.fetched = q.control.fetched + b32(1);
492    }
493    let waiting = (q.control.head & Q_PENDING) != b16(0);
494    let wait_entry: b6 = (q.control.head >> 5).resize();
495    let mut entry_state = b2(0);
496    for e in 0..ENTRIES {
497        if wait_entry == b6(e as u128) {
498            entry_state = q.certs.state[e];
499        }
500    }
501    let settled = !waiting || entry_state != PENDING;
502    let cert_bad = if waiting { entry_state != VERIFIED } else { (q.control.head & Q_CERT_BAD) != b16(0) };
503    let finalize = q.control.head_valid && settled && input.verdict_ready;
504    let mut byte = BIT_WRITTEN | (q.control.head & b16(7)).resize::<8>();
505    if cert_bad {
506        byte = byte | BIT_CERT;
507    }
508    if finalize {
509        c.head_valid = false;
510        c.finalized = q.control.finalized + b32(1);
511        if byte == BIT_WRITTEN {
512            c.valid = q.control.valid + b32(1);
513        }
514    }
515
516    // ------------------------------------------------ loader
517    let beat = q.control.beat;
518    let ctx: b6 = q.control.started.resize::<6>() - if beat == b5(0) { b6(0) } else { b6(1) };
519    let room = q.control.occupancy < b7(CONTEXTS as u128);
520    let can_start = room && !q.certs.busy && q.chains.ready;
521    let ready = beat != b5(0) || can_start;
522    let take = input.beat.valid && ready;
523    let cur = input.beat.data;
524    // Warm certificates use the cache between records: commitment this
525    // cycle, signature next (the cache is busy then, so no record starts).
526    let warm_first = input.warm.valid && !q.control.warm_second && beat == b5(0) && !input.beat.valid && !q.certs.busy;
527    let warm_second = q.control.warm_second;
528    c.warm_second = warm_first;
529    let mut window = [b8(0); 128];
530    for i in 0..64 {
531        window[i] = q.data.prev[i];
532        window[64 + i] = cur[i];
533    }
534    let mut header = HeaderWrite::default();
535    let mut check = CheckWrite::default();
536    let mut nonce_write = NonceWrite::default();
537    let mut loaded = Loaded::default();
538    let mut nodes = [NodeWrite::default(); NODE_BANKS];
539    if take {
540        data.prev = cur;
541        c.beat = if beat == b5(LAST_BEAT as u128) { b5(0) } else { beat + b5(1) };
542        // The new record's context (beat 0 counts it as started).
543        let this: b6 = if beat == b5(0) { q.control.started.resize() } else { ctx };
544        if beat == b5(0) {
545            c.started = q.control.started + b32(1);
546            let mut same = true;
547            for i in 0..16 {
548                same = same && cur[i] == HEADER[i];
549            }
550            // Commitment bytes 16..24: the signer's epoch (LE).
551            let mut epoch_ok = true;
552            for k in 0..8 {
553                let want: b8 = (input.expected_epoch >> ((8 * k) as u128)).resize();
554                epoch_ok = epoch_ok && cur[16 + k] == want;
555            }
556            c.header_ok = same && (epoch_ok || !input.check_epoch);
557            for i in 0..32 {
558                data.root[i] = cur[32 + i];
559            }
560        }
561        if beat == b5(2) {
562            let index_ok = cur[1] == b8(0) && cur[0] < b8(128);
563            for x in 0..CONTEXTS {
564                if this == b6(x as u128) {
565                    c.format_bad[x] = !(q.control.header_ok && index_ok);
566                }
567            }
568            data.index = cur[0].resize();
569            let mut pk_hash = [b8(0); 32];
570            for i in 0..32 {
571                pk_hash[i] = cur[2 + i];
572            }
573            data.pk_hash = pk_hash;
574            let mut row = [b8(0); 64];
575            for i in 0..32 {
576                row[i] = q.data.root[i];
577                row[32 + i] = cur[2 + i];
578            }
579            check = CheckWrite {
580                enable: true,
581                ctx: this,
582                value: row,
583            };
584            let mut pk_nonce = [b8(0); 16];
585            for i in 0..16 {
586                pk_nonce[i] = cur[34 + i];
587            }
588            nonce_write = NonceWrite {
589                valid: true,
590                ctx: this,
591                value: pk_nonce,
592            };
593            for i in 0..14 {
594                data.nonce[i] = cur[50 + i];
595            }
596        }
597        if beat == b5(3) {
598            data.nonce[14] = cur[0];
599            data.nonce[15] = cur[1];
600            let mut row = [b8(0); 64];
601            for i in 0..32 {
602                row[i] = cur[2 + i];
603                row[32 + i] = q.data.pk_hash[i];
604            }
605            header = HeaderWrite {
606                enable: true,
607                addr: (this.resize::<9>() << 3) | b9(1),
608                value: row,
609            };
610        }
611        if beat == b5(4) || beat == b5(5) || beat == b5(6) {
612            let mut row = [b8(0); 64];
613            for i in 0..64 {
614                row[i] = window[34 + i];
615            }
616            header = HeaderWrite {
617                enable: true,
618                addr: (this.resize::<9>() << 3) | (beat - b5(2)).resize::<9>(),
619                value: row,
620            };
621        }
622        if beat == b5(MESSAGE_BEAT as u128) {
623            let mut row = [b8(0); 64];
624            for i in 0..32 {
625                row[i] = q.data.pk_hash[i];
626            }
627            for i in 0..16 {
628                row[32 + i] = q.data.nonce[i];
629                row[48 + i] = cur[42 + i];
630            }
631            header = HeaderWrite {
632                enable: true,
633                addr: this.resize::<9>() << 3,
634                value: row,
635            };
636            if !TWO_BLOCK {
637                loaded = Loaded {
638                    valid: true,
639                    ctx: this,
640                    index: q.data.index,
641                };
642            }
643        }
644        if TWO_BLOCK && beat == b5(LAST_BEAT as u128) {
645            // Profile 2: message bytes 16..64 (record 1658..1706), zero-padded.
646            let mut row = [b8(0); 64];
647            for i in 0..48 {
648                row[i] = window[58 + i];
649            }
650            header = HeaderWrite {
651                enable: true,
652                addr: (this.resize::<9>() << 3) | b9(5),
653                value: row,
654            };
655            loaded = Loaded {
656                valid: true,
657                ctx: this,
658                index: q.data.index,
659            };
660        }
661        // Nodes that end in this beat.
662        let mut onehot = [false; RECORD_BEATS];
663        for b in 0..RECORD_BEATS {
664            onehot[b] = beat == b5(b as u128);
665        }
666        for m in 0..NODE_BANKS {
667            let mut value = [b8(0); ELEMENT_BYTES];
668            let mut enable = false;
669            let mut slot = b4(0);
670            for b in 0..RECORD_BEATS {
671                if onehot[b] && NODE_HAS[b][m] {
672                    enable = true;
673                    slot = NODE_SLOT[b][m];
674                    for y in 0..ELEMENT_BYTES {
675                        value[y] = window[NODE_OFFSET[b][m] + y];
676                    }
677                }
678            }
679            nodes[m] = NodeWrite {
680                enable,
681                addr: (this.resize::<10>() << 4) | slot.resize::<10>(),
682                value,
683            };
684        }
685    }
686
687    // ------------------------------------------------ certificate answers
688    let answer = q.certs.answer;
689    let mut format_bad = c.format_bad;
690    let mut cert_known = q.control.cert_known;
691    let mut cert_pending = q.control.cert_pending;
692    let mut cert_entry = q.control.cert_entry;
693    let mut cert_ok = q.control.cert_ok;
694    for x in 0..CONTEXTS {
695        let here = b6(x as u128);
696        if answer.valid && answer.ctx == here {
697            cert_known[x] = true;
698            cert_pending[x] = answer.pending;
699            cert_entry[x] = answer.entry;
700            cert_ok[x] = answer.ok;
701        }
702        if retire && head == here {
703            cert_known[x] = false;
704            format_bad[x] = false;
705        }
706    }
707    c.format_bad = format_bad;
708    // Small counters (see `CoreControl`).
709    let start = take && beat == b5(0);
710    c.occupancy = q.control.occupancy + (if start { b7(1) } else { b7(0) }) - (if retire { b7(1) } else { b7(0) });
711    c.backlog = q.control.backlog + (if retire { b13(1) } else { b13(0) }) - (if finalize { b13(1) } else { b13(0) });
712    c.freed_valid = retire;
713    c.freed_ctx = head;
714    c.oldest = head;
715    c.cert_known = cert_known;
716    c.cert_pending = cert_pending;
717    c.cert_entry = cert_entry;
718    c.cert_ok = cert_ok;
719
720    let d = VerifyCoreD::<TABLE_MASK> {
721        chains: ChainsInput {
722            nodes,
723            digits: q.hasher.digits,
724            nonce: nonce_write,
725            retire: Retire {
726                valid: q.control.freed_valid,
727                ctx: q.control.freed_ctx,
728            },
729            row: q.hasher.row,
730        },
731        hasher: HasherInput {
732            header,
733            check,
734            loaded,
735            chains_done: q.chains.done,
736            row: q.chains.row,
737            oldest: q.control.oldest,
738            retire_valid: q.control.freed_valid,
739            retire_ctx: q.control.freed_ctx,
740        },
741        certs: CertsInput {
742            commitment_valid: (take && beat == b5(0)) || warm_first,
743            signature_valid: (take && beat == b5(1)) || warm_second,
744            beat: if warm_first {
745                input.warm.commitment
746            } else if warm_second {
747                input.warm.signature
748            } else {
749                cur
750            },
751            ctx: if beat == b5(0) { q.control.started.resize() } else { ctx },
752            slot: if warm_first { input.warm.slot } else { input.slot },
753            warm: warm_first,
754            taken: input.cert_taken,
755            result: input.cert_result,
756            release_valid: finalize && waiting,
757            release_entry: wait_entry,
758        },
759        pending: RamIn::<b16, 12> {
760            read_addr: queue_read,
761            write: queue_write,
762        },
763        control: c,
764        data,
765    };
766    (
767        CoreOutput {
768            beat_ready: ready,
769            warm_taken: warm_second,
770            verdict: Verdict {
771                valid: finalize,
772                byte,
773            },
774            cert_request: q.certs.request,
775            retired: q.control.finalized,
776            valid: q.control.valid,
777            cert_checks: q.certs.checks,
778            cert_fails: q.certs.fails,
779            ready: q.chains.ready,
780            activity: Activity {
781                blake3: q.hasher.issued,
782                lanes: q.chains.stepped,
783                beat: take,
784                beat_stall: input.beat.valid && !ready,
785            },
786        },
787        d,
788    )
789}