Skip to main content

rhdl_dsig_scalable_core/verify/
hasher.rs

1//! The verifier's hasher: one BLAKE3 lane for every record in flight.
2//!
3//! # The problem
4//!
5//! Besides the chain steps, checking a signature takes 29 BLAKE3
6//! compressions, in three independent strands:
7//!
8//! | Strand | Compressions | Depends on |
9//! |---|---|---|
10//! | A: digest, then Merkle levels 0..6 | 1 + 7, one after the other | the loaded record |
11//! | C0: commitment chunk 0 (blocks 0..15), then the parent | 16 + 1 | all chain ends; the parent also needs C1 |
12//! | C1: commitment chunk 1 (blocks 16..19) | 4 | all chain ends |
13//!
14//! Inside a strand every compression needs the one before it, 33 cycles
15//! earlier. One record alone would leave the lane idle most of the time.
16//!
17//! # The idea: interleave many records
18//!
19//! The lane accepts one compression per clock from *any* record. Every clock
20//! the scheduler looks at all 64 contexts, finds the strands whose next
21//! compression may start, and issues the oldest record's (C0 before C1 before
22//! A within a record). With about 20 records in flight the lane is busy
23//! 29 out of every 35 cycles, which matches the chain lanes' 35-cycle period.
24//!
25//! ```text
26//!   cycle t      t+1               t+2   t+3                  t+4        t+32       t+33
27//!   pick ──▶ read header/CV RAMs, ──▶ ··· ─▶ build the block ─▶ BLAKE3 ─··─▶ result ─▶ CV write,
28//!   (ctx,       ask chains for row               (row arrives)     28 stages             compare,
29//!   strand)                                                                              digits
30//! ```
31//!
32//! # Storage per context
33//!
34//! | RAM | Rows | Written by |
35//! |---|---|---|
36//! | header | 0 digest block `pk_hash ‖ nonce ‖ message`; 1 `sib0 ‖ pk_hash`; 2 `sib1 ‖ sib2`; 3 `sib3 ‖ sib4`; 4 `sib5 ‖ sib6` | the loader |
37//! | check | `root ‖ pk_hash` (the two values results are compared with) | the loader |
38//! | CV m, CV 0, CV 1 | the running chaining value of strands A, C0, C1 | results |
39//!
40//! # Results
41//!
42//! * The digest's 64 output bytes become the 68 digits (low 2 bits of each
43//!   byte, then the base-4 checksum `Σ(3 − d)`), sent to the chain block.
44//! * Merkle level 6's output is compared with the commitment's root.
45//! * The parent's output is compared with `pk_hash`.
46//!
47//! A context is [`HasherOutput::done`] after both comparisons; its two
48//! answers are [`HasherOutput::merkle_ok`] and [`HasherOutput::chains_ok`].
49
50use crate::blake3_core::{
51    CHUNK_END, CHUNK_START, CompressionInput, CompressionOutput, IV, InlineCompressionPipeline, PARENT, ROOT,
52    SINGLE_BLOCK_ROOT,
53};
54use crate::chain_lane::le_words_kernel;
55use crate::verify::chains::{CONTEXTS, DigitsEvent, RowRequest};
56use crate::{CHAINS, MESSAGE_DIGITS};
57use rhdl::prelude::*;
58use rhdl_fpga::core::{
59    dff::DFF,
60    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
61};
62
63/// Strand A: digest then Merkle levels.
64const STRAND_A: b2 = b2(0);
65/// Strand C0: commitment chunk 0 then the parent.
66const STRAND_C0: b2 = b2(1);
67/// Strand C1: commitment chunk 1.
68const STRAND_C1: b2 = b2(2);
69/// Digest compressions: 1 (profile 1, `pk_hash ‖ nonce ‖ message` is one
70/// 64-byte block) or 2 (profile 2: the 64-byte message makes 112 bytes,
71/// header row 0 then the tail in [`TAIL_ROW`]).
72const DIGEST_STEPS: usize = if crate::MESSAGE_BYTES == 64 { 2 } else { 1 };
73/// Profile 2's second digest block is in use.
74const TWO_BLOCK: bool = DIGEST_STEPS == 2;
75/// Strand A's last digest step; its output is the digits.
76const LAST_DIGEST: b5 = b5((DIGEST_STEPS - 1) as u128);
77/// Strand A's first Merkle step.
78const FIRST_MERKLE: b5 = b5(DIGEST_STEPS as u128);
79/// Strand A's Merkle level 6 (the root comparison).
80const LAST_MERKLE: b5 = b5((DIGEST_STEPS + 6) as u128);
81/// Header row holding message bytes 16..64 (profile 2).
82const TAIL_ROW: b3 = b3(5);
83/// Strand A is finished after its digest and 7 Merkle compressions.
84const A_DONE: b4 = b4((DIGEST_STEPS + 7) as u128);
85/// Strand C0's parent step.
86const C0_PARENT: b5 = b5(16);
87/// Strand C0 is finished after 16 blocks and the parent.
88const C0_DONE: b5 = b5(17);
89/// Strand C1 is finished after 4 blocks.
90const C1_DONE: b3 = b3(4);
91
92/// BLAKE3's IV as RHDL words.
93const IV_WORDS: [b32; 8] = iv_words();
94/// `CHUNK_START | CHUNK_END | ROOT`.
95const FLAGS_SINGLE: b32 = b32(SINGLE_BLOCK_ROOT as u128);
96/// `CHUNK_START`.
97const FLAGS_START: b32 = b32(CHUNK_START as u128);
98/// `CHUNK_END`.
99const FLAGS_END: b32 = b32(CHUNK_END as u128);
100/// `CHUNK_END | ROOT`: profile 2's last digest block.
101const FLAGS_END_ROOT: b32 = b32((CHUNK_END | ROOT) as u128);
102/// `PARENT | ROOT`.
103const FLAGS_PARENT: b32 = b32((PARENT | ROOT) as u128);
104
105/// Converts [`IV`] to RHDL words.
106const fn iv_words() -> [b32; 8] {
107    let mut words = [b32(0); 8];
108    let mut i = 0;
109    while i < 8 {
110        words[i] = b32(IV[i] as u128);
111        i += 1;
112    }
113    words
114}
115
116/// One header-RAM row write (`ctx·8 + row`).
117#[derive(Clone, Copy, Debug, PartialEq, Digital)]
118pub struct HeaderWrite {
119    /// Write this cycle.
120    pub enable: bool,
121    /// `ctx·8 + row`, rows 0..=4 (and 5, profile 2's message tail).
122    pub addr: b9,
123    /// The 64-byte row.
124    pub value: [b8; 64],
125}
126
127impl Default for HeaderWrite {
128    /// No write.
129    fn default() -> Self {
130        Self {
131            enable: false,
132            addr: b9(0),
133            value: [b8(0); 64],
134        }
135    }
136}
137
138/// The two values a context's results are compared with.
139#[derive(Clone, Copy, Debug, PartialEq, Digital)]
140pub struct CheckWrite {
141    /// Write this cycle.
142    pub enable: bool,
143    /// The context.
144    pub ctx: b6,
145    /// `root ‖ pk_hash`.
146    pub value: [b8; 64],
147}
148
149impl Default for CheckWrite {
150    /// No write.
151    fn default() -> Self {
152        Self {
153            enable: false,
154            ctx: b6(0),
155            value: [b8(0); 64],
156        }
157    }
158}
159
160/// A record is fully loaded: strand A may start.
161#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
162pub struct Loaded {
163    /// Loaded this cycle.
164    pub valid: bool,
165    /// The record's context.
166    pub ctx: b6,
167    /// The key index's low 7 bits: bit `i` puts the running hash on the
168    /// right at Merkle level `i`.
169    pub index: b7,
170}
171
172/// Inputs of the hasher.
173#[derive(Clone, Copy, Debug, PartialEq, Digital)]
174pub struct HasherInput {
175    /// Header-RAM writes from the loader.
176    pub header: HeaderWrite,
177    /// Check-RAM writes from the loader.
178    pub check: CheckWrite,
179    /// A record finished loading.
180    pub loaded: Loaded,
181    /// Per context: all 68 chain ends are stored.
182    pub chains_done: [bool; CONTEXTS],
183    /// The commitment block the chain block delivers this cycle.
184    pub row: [b8; 64],
185    /// The oldest context in flight (first in line for the lane).
186    pub oldest: b6,
187    /// A context leaves the unit; its state clears.
188    pub retire_valid: bool,
189    /// That context.
190    pub retire_ctx: b6,
191}
192
193/// Outputs of the hasher.
194#[derive(Clone, Copy, Debug, PartialEq, Digital)]
195pub struct HasherOutput {
196    /// The commitment block wanted [`ROW_LATENCY`](crate::verify::chains::ROW_LATENCY) cycles from now.
197    pub row: RowRequest,
198    /// A record's digits, once per record.
199    pub digits: DigitsEvent,
200    /// Both comparisons are made.
201    pub done: [bool; CONTEXTS],
202    /// The Merkle climb reached the commitment's root.
203    pub merkle_ok: [bool; CONTEXTS],
204    /// The chain ends commit to `pk_hash`.
205    pub chains_ok: [bool; CONTEXTS],
206    /// A compression enters the BLAKE3 lane this cycle (activity counter).
207    pub issued: bool,
208}
209
210/// Per-context progress, 25 bits.
211#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
212#[doc(hidden)]
213pub struct Progress {
214    /// The record is loaded.
215    loaded: bool,
216    /// Key index bits.
217    index: b7,
218    /// Strand A's next step (0 digest, 1..=7 Merkle levels 0..6, 8 done).
219    a: b4,
220    /// Strand A has a compression in the lane.
221    a_busy: bool,
222    /// Strand C0's next step (0..=15 blocks, 16 parent, 17 done).
223    c0: b5,
224    /// Strand C0 has a compression in the lane.
225    c0_busy: bool,
226    /// Strand C1's next step (0..=3 blocks 16..19, 4 done).
227    c1: b3,
228    /// Strand C1 has a compression in the lane.
229    c1_busy: bool,
230    /// Merkle root matched.
231    merkle_ok: bool,
232    /// Commitment matched.
233    chains_ok: bool,
234}
235
236/// A picked compression travelling to the lane.
237#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
238#[doc(hidden)]
239pub struct Pick {
240    /// A compression is picked.
241    valid: bool,
242    /// Its context.
243    ctx: b6,
244    /// Its strand.
245    strand: b2,
246    /// Its step within the strand.
247    step: b5,
248    /// The context's key index bits.
249    index: b7,
250}
251
252/// Resettable state.
253#[derive(Clone, Copy, Debug, PartialEq, Digital)]
254#[doc(hidden)]
255pub struct HasherControl {
256    /// Per-context progress.
257    progress: [Progress; CONTEXTS],
258    /// Picked last cycle (RAM reads issued now).
259    s1: Pick,
260    /// RAM data arrives.
261    s2: Pick,
262    /// The row arrives; the block is built.
263    s3: Pick,
264    /// The built block enters the lane.
265    s4_valid: bool,
266    /// A lane result is in `result`.
267    r1_valid: bool,
268    /// Digits are in `digits`.
269    d_valid: bool,
270}
271
272impl Default for HasherControl {
273    /// Nothing loaded, nothing in flight.
274    fn default() -> Self {
275        Self {
276            progress: [Progress::default(); CONTEXTS],
277            s1: Pick::default(),
278            s2: Pick::default(),
279            s3: Pick::default(),
280            s4_valid: false,
281            r1_valid: false,
282            d_valid: false,
283        }
284    }
285}
286
287/// Resetless data registers.
288#[derive(Clone, Copy, Debug, PartialEq, Digital)]
289#[doc(hidden)]
290pub struct HasherData {
291    /// Header row read for the stage-2 pick, held one cycle.
292    header: [b8; 64],
293    /// The three chaining values read for the stage-2 pick.
294    cv_m: [b32; 8],
295    /// Chunk 0's chaining value.
296    cv_0: [b32; 8],
297    /// Chunk 1's chaining value.
298    cv_1: [b32; 8],
299    /// The compression entering the lane.
300    input: CompressionInput,
301    /// The lane result being written back.
302    result: CompressionOutput,
303    /// The digits being sent.
304    digits: [b2; CHAINS],
305    /// Their context.
306    digits_ctx: b6,
307}
308
309/// The hasher: one BLAKE3 lane, its scheduler, and the per-context RAMs.
310#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
311pub struct VerifyHasher {
312    /// The 28-stage BLAKE3 compression pipeline.
313    lane: InlineCompressionPipeline,
314    /// Header rows (`ctx·8 + row`).
315    header: SyncBRAM<[b8; 64], 9>,
316    /// `root ‖ pk_hash` per context.
317    check: SyncBRAM<[b8; 64], 6>,
318    /// Running chaining values of strands A, C0 and C1.
319    cvs: [SyncBRAM<[b32; 8], 6>; 3],
320    /// Progress and pipeline flags.
321    control: DFF<HasherControl>,
322    /// Pipeline data.
323    data: DFF<HasherData>,
324}
325
326impl Default for VerifyHasher {
327    /// An idle hasher.
328    fn default() -> Self {
329        Self {
330            lane: InlineCompressionPipeline,
331            header: SyncBRAM::default(),
332            check: SyncBRAM::default(),
333            cvs: core::array::from_fn(|_| SyncBRAM::default()),
334            control: DFF::new(HasherControl::default()),
335            data: DFF::resetless(),
336        }
337    }
338}
339
340impl SynchronousIO for VerifyHasher {
341    type I = HasherInput;
342    type O = HasherOutput;
343    type Kernel = verify_hasher_kernel;
344}
345
346/// First set bit at or after `start` (wrapping), as `(found, index)`.
347///
348/// Two levels of eight so the logic stays shallow: within each group of
349/// eight the first candidate, then the first group that has one. Bits at or
350/// after `start` are tried before the wrapped-around ones.
351#[allow(clippy::needless_range_loop)]
352#[kernel]
353pub fn oldest_first_kernel(request: [bool; CONTEXTS], start: b6) -> (bool, b6) {
354    let mut late = [false; CONTEXTS];
355    for x in 0..CONTEXTS {
356        late[x] = request[x] && b6(x as u128) >= start;
357    }
358    let (late_found, late_index) = first_set_kernel(late);
359    let (any_found, any_index) = first_set_kernel(request);
360    if late_found {
361        (true, late_index)
362    } else {
363        (any_found, any_index)
364    }
365}
366
367/// Lowest set bit of 64, as `(found, index)`, in two levels of eight.
368#[allow(clippy::needless_range_loop)]
369#[kernel]
370pub fn first_set_kernel(bits: [bool; CONTEXTS]) -> (bool, b6) {
371    let mut group_any = [false; 8];
372    let mut group_low = [b3(0); 8];
373    for g in 0..8 {
374        let mut low = b3(0);
375        let mut any = false;
376        for i in 0..8 {
377            let x = 7 - i;
378            if bits[8 * g + x] {
379                low = b3(x as u128);
380                any = true;
381            }
382        }
383        group_any[g] = any;
384        group_low[g] = low;
385    }
386    let mut found = false;
387    let mut index = b6(0);
388    for i in 0..8 {
389        let g = 7 - i;
390        if group_any[g] {
391            found = true;
392            index = (b6(g as u128) << 3) | group_low[g].resize::<6>();
393        }
394    }
395    (found, index)
396}
397
398/// The 68 digits of a digest output: low 2 bits of bytes 0..64, then the
399/// base-4 checksum `Σ (3 − d)` = `192 − Σ d`, least significant digit first.
400#[allow(clippy::needless_range_loop)]
401#[kernel]
402pub fn digits_kernel(words: [b32; 16]) -> [b2; CHAINS] {
403    let mut digits = [b2(0); CHAINS];
404    for w in 0..16 {
405        digits[4 * w] = words[w].resize();
406        digits[4 * w + 1] = (words[w] >> 8).resize();
407        digits[4 * w + 2] = (words[w] >> 16).resize();
408        digits[4 * w + 3] = (words[w] >> 24).resize();
409    }
410    // Sum of the 64 message digits as a shallow tree.
411    let mut fours = [b4(0); 16];
412    for g in 0..16 {
413        fours[g] = digits[4 * g].resize::<4>()
414            + digits[4 * g + 1].resize::<4>()
415            + digits[4 * g + 2].resize::<4>()
416            + digits[4 * g + 3].resize::<4>();
417    }
418    let mut sixteens = [b6(0); 4];
419    for g in 0..4 {
420        sixteens[g] = fours[4 * g].resize::<6>()
421            + fours[4 * g + 1].resize::<6>()
422            + fours[4 * g + 2].resize::<6>()
423            + fours[4 * g + 3].resize::<6>();
424    }
425    let sum: b8 = sixteens[0].resize::<8>() + sixteens[1].resize::<8>() + sixteens[2].resize::<8>() + sixteens[3].resize::<8>();
426    let checksum: b8 = b8(192) - sum;
427    digits[MESSAGE_DIGITS] = checksum.resize();
428    digits[MESSAGE_DIGITS + 1] = (checksum >> 2).resize();
429    digits[MESSAGE_DIGITS + 2] = (checksum >> 4).resize();
430    digits[MESSAGE_DIGITS + 3] = (checksum >> 6).resize();
431    digits
432}
433
434/// Bit `level` of a 7-bit key index (the side of the running hash).
435#[kernel]
436pub fn index_bit_kernel(index: b7, level: b3) -> bool {
437    let bits = [
438        (index & b7(1)) != b7(0),
439        (index & b7(2)) != b7(0),
440        (index & b7(4)) != b7(0),
441        (index & b7(8)) != b7(0),
442        (index & b7(16)) != b7(0),
443        (index & b7(32)) != b7(0),
444        (index & b7(64)) != b7(0),
445    ];
446    let mut bit = false;
447    for l in 0..7 {
448        if level == b3(l as u128) {
449            bit = bits[l];
450        }
451    }
452    bit
453}
454
455/// The hasher's logic.
456#[allow(clippy::needless_range_loop, clippy::too_many_lines)]
457#[kernel]
458pub fn verify_hasher_kernel(
459    clock_reset: ClockReset,
460    input: HasherInput,
461    q: VerifyHasherQ,
462) -> (HasherOutput, VerifyHasherD) {
463    // No reset gating here: every register this logic feeds resets itself,
464    // and keeping the reset out of the logic cones keeps it off the routed
465    // kernel's failing paths (verifier build v1).
466    let resetting = clock_reset.reset.any();
467    let _ = resetting;
468    let mut c = q.control;
469    let mut data = q.data;
470
471    // ------------------------------------------------ pick (cycle t)
472    // Which strand of each context may issue its next compression.
473    let mut want_a = [false; CONTEXTS];
474    let mut want_c0 = [false; CONTEXTS];
475    let mut want_c1 = [false; CONTEXTS];
476    let mut want = [false; CONTEXTS];
477    for x in 0..CONTEXTS {
478        let p = q.control.progress[x];
479        want_a[x] = p.loaded && !p.a_busy && p.a != A_DONE;
480        want_c1[x] = input.chains_done[x] && p.loaded && !p.c1_busy && p.c1 != C1_DONE;
481        want_c0[x] = input.chains_done[x]
482            && p.loaded
483            && !p.c0_busy
484            && p.c0 != C0_DONE
485            && (p.c0 != C0_PARENT || p.c1 == C1_DONE);
486        want[x] = want_a[x] || want_c0[x] || want_c1[x];
487    }
488    // A digest feeds the chain lanes, the unit's busiest resource, so any
489    // waiting digest goes first (oldest first); then the oldest record.
490    let mut want_digest = [false; CONTEXTS];
491    for x in 0..CONTEXTS {
492        want_digest[x] = want_a[x] && q.control.progress[x].a == b4(0);
493    }
494    let (digest_found, digest_ctx) = oldest_first_kernel(want_digest, input.oldest);
495    let (any_found, any_ctx) = oldest_first_kernel(want, input.oldest);
496    let found = digest_found || any_found;
497    let ctx = if digest_found { digest_ctx } else { any_ctx };
498    let mut pick = Pick {
499        valid: false,
500        ctx: b6(0),
501        strand: STRAND_A,
502        step: b5(0),
503        index: b7(0),
504    };
505    let mut ctx_progress = q.control.progress[0];
506    let mut pick_c0 = false;
507    let mut pick_c1 = false;
508    for x in 0..CONTEXTS {
509        if ctx == b6(x as u128) {
510            ctx_progress = q.control.progress[x];
511            pick_c0 = want_c0[x];
512            pick_c1 = want_c1[x];
513        }
514    }
515    if found {
516        pick.valid = true;
517        pick.ctx = ctx;
518        pick.index = ctx_progress.index;
519        if digest_found {
520            pick.strand = STRAND_A;
521            pick.step = b5(0);
522        } else if pick_c0 {
523            pick.strand = STRAND_C0;
524            pick.step = ctx_progress.c0;
525        } else if pick_c1 {
526            pick.strand = STRAND_C1;
527            pick.step = ctx_progress.c1.resize();
528        } else {
529            pick.strand = STRAND_A;
530            pick.step = ctx_progress.a.resize();
531        }
532    }
533
534    // ------------------------------------------------ stage 1: RAM reads
535    // Header row of strand A's step: 0 digest, then row 1 + (level + 1) / 2.
536    let s1 = q.control.s1;
537    let a_level: b5 = s1.step - FIRST_MERKLE;
538    let header_row: b3 = if s1.step == b5(0) {
539        b3(0)
540    } else if TWO_BLOCK && s1.step == b5(1) {
541        TAIL_ROW
542    } else {
543        b3(1) + ((a_level + b5(1)) >> 1).resize::<3>()
544    };
545    let header_read: b9 = (s1.ctx.resize::<9>() << 3) | header_row.resize::<9>();
546    let row = RowRequest {
547        valid: s1.valid && s1.strand != STRAND_A,
548        ctx: s1.ctx,
549        row: if s1.strand == STRAND_C1 { b5(16) + s1.step } else { s1.step },
550    };
551    c.s2 = s1;
552
553    // ------------------------------------------------ stage 2: hold RAM data
554    data.header = q.header;
555    data.cv_m = q.cvs[0];
556    data.cv_0 = q.cvs[1];
557    data.cv_1 = q.cvs[2];
558    c.s3 = q.control.s2;
559
560    // ------------------------------------------------ stage 3: build the block
561    let s3 = q.control.s3;
562    let header_words = le_words_kernel(q.data.header);
563    let row_words = le_words_kernel(input.row);
564    let mut block = row_words;
565    let mut cv = IV_WORDS;
566    let mut counter_low = b32(0);
567    let mut block_len = b32(64);
568    let mut flags = b32(0);
569    if s3.strand == STRAND_A {
570        flags = FLAGS_SINGLE;
571        if s3.step == b5(0) {
572            // Digest: pk_hash ‖ nonce ‖ message (profile 2: its first block).
573            block = header_words;
574            if TWO_BLOCK {
575                flags = FLAGS_START;
576            }
577        } else if TWO_BLOCK && s3.step == b5(1) {
578            // Profile 2's second digest block: message bytes 16..64.
579            block = header_words;
580            cv = q.data.cv_m;
581            block_len = b32(48);
582            flags = FLAGS_END_ROOT;
583        } else {
584            // Merkle level `step − FIRST_MERKLE`: the running hash beside its sibling.
585            let level: b5 = s3.step - FIRST_MERKLE;
586            let mut sibling = [b32(0); 8];
587            let mut running = q.data.cv_m;
588            for w in 0..8 {
589                // Odd levels and level 0 keep the sibling in the low half.
590                sibling[w] = if level == b5(0) || (level & b5(1)) == b5(1) { header_words[w] } else { header_words[w + 8] };
591                if level == b5(0) {
592                    running[w] = header_words[w + 8];
593                }
594            }
595            let right = index_bit_kernel(s3.index, level.resize());
596            for w in 0..8 {
597                block[w] = if right { sibling[w] } else { running[w] };
598                block[w + 8] = if right { running[w] } else { sibling[w] };
599            }
600        }
601    } else if s3.strand == STRAND_C0 {
602        if s3.step == C0_PARENT {
603            for w in 0..8 {
604                block[w] = q.data.cv_0[w];
605                block[w + 8] = q.data.cv_1[w];
606            }
607            flags = FLAGS_PARENT;
608        } else {
609            if s3.step != b5(0) {
610                cv = q.data.cv_0;
611            } else {
612                flags = FLAGS_START;
613            }
614            if s3.step == b5(15) {
615                flags = flags | FLAGS_END;
616            }
617        }
618    } else {
619        counter_low = b32(1);
620        if s3.step != b5(0) {
621            cv = q.data.cv_1;
622        } else {
623            flags = FLAGS_START;
624        }
625        if s3.step == b5(3) {
626            flags = flags | FLAGS_END;
627            block_len = b32(24);
628        }
629    }
630    data.input = CompressionInput {
631        cv,
632        block,
633        counter_low,
634        counter_high: b32(0),
635        block_len,
636        flags,
637        tag: s3.ctx.resize::<32>() | (s3.strand.resize::<32>() << 6) | (s3.step.resize::<32>() << 8),
638        valid: true,
639    };
640    c.s4_valid = s3.valid;
641    let mut lane_in = q.data.input;
642    lane_in.valid = q.control.s4_valid;
643
644    // ------------------------------------------------ results
645    // R0: the lane's output; ask the check RAM for the context's values.
646    let out = q.lane;
647    let out_ctx: b6 = out.tag.resize();
648    data.result = out;
649    c.r1_valid = out.valid;
650    // R1: write the chaining value back, compare, advance the strand.
651    let r1 = q.data.result;
652    let r1_ctx: b6 = r1.tag.resize();
653    let r1_strand: b2 = (r1.tag >> 6).resize();
654    let r1_step: b5 = (r1.tag >> 8).resize();
655    let mut cv_out = [b32(0); 8];
656    for w in 0..8 {
657        cv_out[w] = r1.words[w];
658    }
659    // The check row is `root ‖ pk_hash`; compare word by word.
660    let check_words = le_words_kernel(q.check);
661    let mut matches_root = true;
662    let mut matches_pk = true;
663    for w in 0..8 {
664        matches_root = matches_root && cv_out[w] == check_words[w];
665        matches_pk = matches_pk && cv_out[w] == check_words[w + 8];
666    }
667    let r1_valid = q.control.r1_valid;
668    let write_m = r1_valid && r1_strand == STRAND_A && r1_step != LAST_DIGEST;
669    let write_0 = r1_valid && r1_strand == STRAND_C0;
670    let write_1 = r1_valid && r1_strand == STRAND_C1;
671    // The digest result becomes digits next cycle.
672    c.d_valid = r1_valid && r1_strand == STRAND_A && r1_step == LAST_DIGEST;
673    data.digits = digits_kernel(r1.words);
674    data.digits_ctx = r1_ctx;
675
676    // ------------------------------------------------ progress
677    let mut progress = q.control.progress;
678    for x in 0..CONTEXTS {
679        let mut p = q.control.progress[x];
680        let here = b6(x as u128);
681        if input.loaded.valid && input.loaded.ctx == here {
682            p.loaded = true;
683            p.index = input.loaded.index;
684        }
685        if found && ctx == here {
686            if digest_found {
687                p.a_busy = true;
688            } else if pick_c0 {
689                p.c0_busy = true;
690            } else if pick_c1 {
691                p.c1_busy = true;
692            } else {
693                p.a_busy = true;
694            }
695        }
696        if r1_valid && r1_ctx == here {
697            if r1_strand == STRAND_A {
698                p.a_busy = false;
699                p.a = p.a + b4(1);
700                if r1_step == LAST_MERKLE {
701                    p.merkle_ok = matches_root;
702                }
703            } else if r1_strand == STRAND_C0 {
704                p.c0_busy = false;
705                p.c0 = p.c0 + b5(1);
706                if r1_step == C0_PARENT {
707                    p.chains_ok = matches_pk;
708                }
709            } else {
710                p.c1_busy = false;
711                p.c1 = p.c1 + b3(1);
712            }
713        }
714        if input.retire_valid && input.retire_ctx == here {
715            p = Progress {
716                loaded: false,
717                index: b7(0),
718                a: b4(0),
719                a_busy: false,
720                c0: b5(0),
721                c0_busy: false,
722                c1: b3(0),
723                c1_busy: false,
724                merkle_ok: false,
725                chains_ok: false,
726            };
727        }
728        progress[x] = p;
729    }
730    c.progress = progress;
731    c.s1 = pick;
732
733    let mut done = [false; CONTEXTS];
734    let mut merkle_ok = [false; CONTEXTS];
735    let mut chains_ok = [false; CONTEXTS];
736    for x in 0..CONTEXTS {
737        let p = q.control.progress[x];
738        done[x] = p.a == A_DONE && p.c0 == C0_DONE;
739        merkle_ok[x] = p.merkle_ok;
740        chains_ok[x] = p.chains_ok;
741    }
742    let d = VerifyHasherD {
743        lane: lane_in,
744        header: RamIn::<[b8; 64], 9> {
745            read_addr: header_read,
746            write: RamWrite::<[b8; 64], 9> {
747                addr: input.header.addr,
748                value: input.header.value,
749                enable: input.header.enable,
750            },
751        },
752        check: RamIn::<[b8; 64], 6> {
753            read_addr: out_ctx,
754            write: RamWrite::<[b8; 64], 6> {
755                addr: input.check.ctx,
756                value: input.check.value,
757                enable: input.check.enable,
758            },
759        },
760        cvs: [
761            RamIn::<[b32; 8], 6> {
762                read_addr: s1.ctx,
763                write: RamWrite::<[b32; 8], 6> {
764                    addr: r1_ctx,
765                    value: cv_out,
766                    enable: write_m,
767                },
768            },
769            RamIn::<[b32; 8], 6> {
770                read_addr: s1.ctx,
771                write: RamWrite::<[b32; 8], 6> {
772                    addr: r1_ctx,
773                    value: cv_out,
774                    enable: write_0,
775                },
776            },
777            RamIn::<[b32; 8], 6> {
778                read_addr: s1.ctx,
779                write: RamWrite::<[b32; 8], 6> {
780                    addr: r1_ctx,
781                    value: cv_out,
782                    enable: write_1,
783                },
784            },
785        ],
786        control: c,
787        data,
788    };
789    (
790        HasherOutput {
791            row,
792            digits: DigitsEvent {
793                valid: q.control.d_valid,
794                ctx: q.data.digits_ctx,
795                digits: q.data.digits,
796            },
797            done,
798            merkle_ok,
799            chains_ok,
800            issued: q.control.s4_valid,
801        },
802        d,
803    )
804}