Skip to main content

rhdl_ed25519_fast_core/
verify.rs

1//! High-throughput strict Ed25519 verification for 64-byte messages.
2//!
3//! [`StrictVerifyCore`] evaluates the complete Ed25519 equation in hardware:
4//! SHA-512 challenge hashing, reduction modulo `l`, strict point decoding,
5//! fixed-base `[S]B`, variable-base `[k]A`, and the projective equality check.
6//! Intermediate points are joined by a 64-entry on-chip context store.  No
7//! prehashing, external DRAM, HBM, or host arithmetic is part of the path.
8
9use rhdl::prelude::*;
10use rhdl_fpga::core::{
11    dff::DFF,
12    ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
13};
14
15use crate::{
16    equation::{EquationIn, EquationPipe},
17    field_parallel::FieldElement51,
18    multicomb::{ExtendedPoint, MultiCombMul, MultiCombMulIn},
19    point_compress::CompressedPoint,
20    point_decode::{PointDecodeIn, PointDecodePipe},
21    protocol::{VerifyRequest, Words512},
22    scalar::{ScalarWords, scalar_ge_kernel, scalar_l_kernel},
23    scalar_parallel::{ScalarReducePipe, ScalarReducePipeIn},
24    sha512_parallel::{Sha512Pool, Sha512PoolIn, Sha512PoolRequest},
25    variable_base::{VariableBaseIn, VariableBaseMul},
26};
27
28/// Verification completed successfully.
29pub const VERIFY_OK: u128 = 0;
30/// Public-key encoding is not canonical or not on the curve.
31pub const VERIFY_BAD_PUBLIC_KEY: u128 = 1;
32/// Signature `R` encoding is not canonical or not on the curve.
33pub const VERIFY_BAD_R: u128 = 2;
34/// Public key is a low-order point.
35pub const VERIFY_SMALL_PUBLIC_KEY: u128 = 3;
36/// Signature `R` is a low-order point.
37pub const VERIFY_SMALL_R: u128 = 4;
38/// Signature scalar `S` is not canonical.
39pub const VERIFY_NONCANONICAL_S: u128 = 5;
40/// The Ed25519 group equation did not hold.
41pub const VERIFY_EQUATION_FAILURE: u128 = 6;
42
43/// Input channel of [`StrictVerifyCore`].
44#[derive(Copy, Clone, Debug, PartialEq, Digital)]
45pub struct StrictVerifyIn {
46    /// Marks [`Self::request`] valid when [`StrictVerifyOut::ready`] is high.
47    pub valid: bool,
48    /// Strict verification request for exactly 64 message bytes.
49    pub request: VerifyRequest,
50}
51
52/// Output and flow control of [`StrictVerifyCore`].
53#[derive(Copy, Clone, Debug, PartialEq, Digital)]
54pub struct StrictVerifyOut {
55    /// High when the core can accept a request.
56    pub ready: bool,
57    /// High for one cycle when the decision fields are valid.
58    pub valid: bool,
59    /// Strict verification result.
60    pub ok: bool,
61    /// Stable error code; zero means success.
62    pub error: b8,
63    /// Delayed request tag.
64    pub tag: b16,
65    /// Number of occupied top-level verification contexts.
66    pub occupancy: b7,
67}
68
69#[derive(Copy, Clone, Debug, PartialEq, Digital)]
70#[doc(hidden)]
71pub struct VerifyStatus {
72    used: bool,
73    tag: b16,
74    canonical_s: bool,
75    a_ready: bool,
76    a_valid: bool,
77    a_small: bool,
78    r_ready: bool,
79    r_valid: bool,
80    r_small: bool,
81    fixed_ready: bool,
82    hash1_pending: bool,
83    challenge_ready: bool,
84    variable_issued: bool,
85    variable_ready: bool,
86    variable_lane: bool,
87    equation_issued: bool,
88}
89
90fn empty_status() -> VerifyStatus {
91    VerifyStatus {
92        used: false,
93        tag: b16(0),
94        canonical_s: false,
95        a_ready: false,
96        a_valid: false,
97        a_small: false,
98        r_ready: false,
99        r_valid: false,
100        r_small: false,
101        fixed_ready: false,
102        hash1_pending: false,
103        challenge_ready: false,
104        variable_issued: false,
105        variable_ready: false,
106        variable_lane: false,
107        equation_issued: false,
108    }
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Digital)]
112#[doc(hidden)]
113pub struct Fetch {
114    valid: bool,
115    context: b6,
116}
117
118#[derive(Copy, Clone, Debug, PartialEq, Digital)]
119#[doc(hidden)]
120pub struct VariableFetch {
121    valid: bool,
122    context: b6,
123    lane: bool,
124}
125
126#[derive(Copy, Clone, Debug, PartialEq, Digital)]
127#[doc(hidden)]
128pub struct HashIssue {
129    valid: bool,
130    context: b6,
131    state: [b64; 8],
132}
133
134#[derive(Copy, Clone, Debug, PartialEq, Digital)]
135#[doc(hidden)]
136pub struct VerifyControl {
137    status: [VerifyStatus; 64],
138    hash_fetch: Fetch,
139    hash_issue: HashIssue,
140    variable_fetch: VariableFetch,
141    equation_fetch: Fetch,
142    hash_rr: b6,
143    variable_rr: b6,
144    equation_rr: b6,
145    variable_lane_rr: bool,
146}
147
148fn reset_control() -> VerifyControl {
149    VerifyControl {
150        status: [empty_status(); 64],
151        hash_fetch: Fetch {
152            valid: false,
153            context: b6(0),
154        },
155        hash_issue: HashIssue {
156            valid: false,
157            context: b6(0),
158            state: [b64(0); 8],
159        },
160        variable_fetch: VariableFetch {
161            valid: false,
162            context: b6(0),
163            lane: false,
164        },
165        equation_fetch: Fetch {
166            valid: false,
167            context: b6(0),
168        },
169        hash_rr: b6(0),
170        variable_rr: b6(0),
171        equation_rr: b6(0),
172        variable_lane_rr: false,
173    }
174}
175
176/// Point plus affine `2*d*T` retained after strict decoding.
177#[derive(Copy, Clone, Debug, PartialEq, Digital)]
178#[doc(hidden)]
179pub struct DecodedPoint {
180    point: ExtendedPoint,
181    t2d: FieldElement51,
182}
183
184#[derive(Copy, Clone, Debug, PartialEq, Digital)]
185#[doc(hidden)]
186pub struct VerifyMemoryIn {
187    chain: RamIn<Words512, 6>,
188    a: RamIn<DecodedPoint, 6>,
189    r: RamIn<DecodedPoint, 6>,
190    fixed: RamIn<ExtendedPoint, 6>,
191    challenge: RamIn<ScalarWords, 6>,
192    variable0: RamIn<ExtendedPoint, 6>,
193    variable1: RamIn<ExtendedPoint, 6>,
194}
195
196#[derive(Copy, Clone, Debug, PartialEq, Digital)]
197#[doc(hidden)]
198pub struct VerifyMemoryOut {
199    chain: Words512,
200    a: DecodedPoint,
201    r: DecodedPoint,
202    fixed: ExtendedPoint,
203    challenge: ScalarWords,
204    variable0: ExtendedPoint,
205    variable1: ExtendedPoint,
206}
207
208/// Independent BRAM banks for the verifier's wide join values.
209#[derive(Clone, Debug, Synchronous, SynchronousDQ, Default)]
210#[doc(hidden)]
211pub struct VerifyMemory {
212    chain: SyncBRAM<Words512, 6>,
213    a: SyncBRAM<DecodedPoint, 6>,
214    r: SyncBRAM<DecodedPoint, 6>,
215    fixed: SyncBRAM<ExtendedPoint, 6>,
216    challenge: SyncBRAM<ScalarWords, 6>,
217    variable0: SyncBRAM<ExtendedPoint, 6>,
218    variable1: SyncBRAM<ExtendedPoint, 6>,
219}
220
221impl SynchronousIO for VerifyMemory {
222    type I = VerifyMemoryIn;
223    type O = VerifyMemoryOut;
224    type Kernel = verify_memory_kernel;
225}
226
227/// Wiring transition for [`VerifyMemory`].
228#[kernel]
229#[doc(hidden)]
230pub fn verify_memory_kernel(
231    _cr: ClockReset,
232    i: VerifyMemoryIn,
233    q: VerifyMemoryQ,
234) -> (VerifyMemoryOut, VerifyMemoryD) {
235    let output = VerifyMemoryOut {
236        chain: q.chain,
237        a: q.a,
238        r: q.r,
239        fixed: q.fixed,
240        challenge: q.challenge,
241        variable0: q.variable0,
242        variable1: q.variable1,
243    };
244    let mut d = VerifyMemoryD::dont_care();
245    d.chain = i.chain;
246    d.a = i.a;
247    d.r = i.r;
248    d.fixed = i.fixed;
249    d.challenge = i.challenge;
250    d.variable0 = i.variable0;
251    d.variable1 = i.variable1;
252    (output, d)
253}
254
255/// Entire strict verifier, expressed as one synthesizable RHDL module graph.
256#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
257pub struct StrictVerifyCore {
258    control: DFF<VerifyControl>,
259    memory: VerifyMemory,
260    decode_a: PointDecodePipe,
261    decode_r: PointDecodePipe,
262    sha: Sha512Pool,
263    reducer: ScalarReducePipe,
264    fixed: MultiCombMul,
265    variable0: VariableBaseMul,
266    variable1: VariableBaseMul,
267    equation: EquationPipe,
268}
269
270impl Default for StrictVerifyCore {
271    fn default() -> Self {
272        Self {
273            control: DFF::new(reset_control()),
274            memory: VerifyMemory::default(),
275            decode_a: PointDecodePipe::default(),
276            decode_r: PointDecodePipe::default(),
277            sha: Sha512Pool::default(),
278            reducer: ScalarReducePipe::default(),
279            fixed: MultiCombMul::default(),
280            variable0: VariableBaseMul::default(),
281            variable1: VariableBaseMul::default(),
282            equation: EquationPipe::default(),
283        }
284    }
285}
286
287impl SynchronousIO for StrictVerifyCore {
288    type I = StrictVerifyIn;
289    type O = StrictVerifyOut;
290    type Kernel = strict_verify_kernel;
291}
292
293#[kernel]
294fn zero_field() -> FieldElement51 {
295    FieldElement51 { limbs: [b51(0); 5] }
296}
297
298#[kernel]
299fn one_field() -> FieldElement51 {
300    FieldElement51 {
301        limbs: [b51(1), b51(0), b51(0), b51(0), b51(0)],
302    }
303}
304
305#[kernel]
306fn identity() -> ExtendedPoint {
307    ExtendedPoint {
308        x: zero_field(),
309        y: one_field(),
310        z: one_field(),
311        t: zero_field(),
312    }
313}
314
315#[kernel]
316fn empty_status_kernel(_unused: bool) -> VerifyStatus {
317    VerifyStatus {
318        used: false,
319        tag: b16(0),
320        canonical_s: false,
321        a_ready: false,
322        a_valid: false,
323        a_small: false,
324        r_ready: false,
325        r_valid: false,
326        r_small: false,
327        fixed_ready: false,
328        hash1_pending: false,
329        challenge_ready: false,
330        variable_issued: false,
331        variable_ready: false,
332        variable_lane: false,
333        equation_issued: false,
334    }
335}
336
337#[kernel]
338fn reverse_bytes(value: b64) -> b64 {
339    let mut result = b64(0);
340    for byte in 0..8 {
341        result |= ((value >> (byte * 8) as u128).resize::<8>()).resize::<64>()
342            << ((7 - byte) * 8) as u128;
343    }
344    result
345}
346
347#[kernel]
348fn sha_iv() -> [b64; 8] {
349    [
350        b64(0x6a09e667f3bcc908),
351        b64(0xbb67ae8584caa73b),
352        b64(0x3c6ef372fe94f82b),
353        b64(0xa54ff53a5f1d36f1),
354        b64(0x510e527fade682d1),
355        b64(0x9b05688c2b3e6c1f),
356        b64(0x1f83d9abfb41bd6b),
357        b64(0x5be0cd19137e2179),
358    ]
359}
360
361#[kernel]
362fn challenge_block0(request: VerifyRequest) -> [b64; 16] {
363    let mut block = [b64(0); 16];
364    for index in 0..4 {
365        block[index] = reverse_bytes(request.signature.words[index]);
366        block[index + 4] = reverse_bytes(request.public_key.words[index]);
367    }
368    for index in 0..8 {
369        block[index + 8] = reverse_bytes(request.message.words[index]);
370    }
371    block
372}
373
374#[kernel]
375fn challenge_block1() -> [b64; 16] {
376    let mut block = [b64(0); 16];
377    block[0] = b64(0x8000000000000000);
378    block[15] = b64(0x400);
379    block
380}
381
382#[kernel]
383fn digest_words(state: [b64; 8]) -> Words512 {
384    let mut words = [b64(0); 8];
385    for index in 0..8 {
386        words[index] = reverse_bytes(state[index]);
387    }
388    Words512 { words }
389}
390
391#[kernel]
392fn request_r(request: VerifyRequest) -> CompressedPoint {
393    CompressedPoint {
394        words: [
395            request.signature.words[0],
396            request.signature.words[1],
397            request.signature.words[2],
398            request.signature.words[3],
399        ],
400    }
401}
402
403#[kernel]
404fn request_s(request: VerifyRequest) -> ScalarWords {
405    ScalarWords {
406        words: [
407            request.signature.words[4],
408            request.signature.words[5],
409            request.signature.words[6],
410            request.signature.words[7],
411        ],
412    }
413}
414
415#[kernel]
416fn wrap64(value: b6) -> b6 {
417    value
418}
419
420#[kernel]
421fn no_point_write() -> RamWrite<ExtendedPoint, 6> {
422    RamWrite::<ExtendedPoint, 6> {
423        addr: b6(0),
424        value: identity(),
425        enable: false,
426    }
427}
428
429/// State transition for [`StrictVerifyCore`].
430#[kernel]
431pub fn strict_verify_kernel(
432    _cr: ClockReset,
433    i: StrictVerifyIn,
434    q: StrictVerifyCoreQ,
435) -> (StrictVerifyOut, StrictVerifyCoreD) {
436    let r = q.control;
437    let mut n = r;
438    let zero_words = Words512 { words: [b64(0); 8] };
439    let zero_scalar = ScalarWords { words: [b64(0); 4] };
440    let empty_decoded = DecodedPoint {
441        point: identity(),
442        t2d: zero_field(),
443    };
444    let mut output = StrictVerifyOut {
445        ready: false,
446        valid: false,
447        ok: false,
448        error: b8(0),
449        tag: b16(0),
450        occupancy: b7(0),
451    };
452
453    let mut free_found = false;
454    let mut free_context = b6(0);
455    for context in 0..64 {
456        if r.status[context].used {
457            output.occupancy += b7(1);
458        } else if !free_found {
459            free_found = true;
460            free_context = b6(context as u128);
461        }
462    }
463    let accept_resources =
464        free_found && q.decode_a.ready && q.decode_r.ready && q.fixed.ready && q.sha.request1_ready;
465    output.ready = accept_resources;
466    let accept = i.valid && accept_resources;
467
468    let mut chain_write = RamWrite::<Words512, 6> {
469        addr: b6(0),
470        value: zero_words,
471        enable: false,
472    };
473    let mut a_write = RamWrite::<DecodedPoint, 6> {
474        addr: b6(0),
475        value: empty_decoded,
476        enable: false,
477    };
478    let mut r_write = a_write;
479    let mut fixed_write = no_point_write();
480    let mut challenge_write = RamWrite::<ScalarWords, 6> {
481        addr: b6(0),
482        value: zero_scalar,
483        enable: false,
484    };
485    let mut variable0_write = no_point_write();
486    let mut variable1_write = no_point_write();
487
488    if accept {
489        let scalar = request_s(i.request);
490        n.status[free_context] = VerifyStatus {
491            used: true,
492            tag: i.request.tag,
493            canonical_s: !scalar_ge_kernel(scalar, scalar_l_kernel(false)),
494            ..empty_status_kernel(false)
495        };
496    }
497
498    if q.decode_a.valid {
499        let context = q.decode_a.tag.resize::<6>();
500        n.status[context].a_ready = true;
501        n.status[context].a_valid = q.decode_a.point_valid;
502        n.status[context].a_small = q.decode_a.small_order;
503        a_write = RamWrite::<DecodedPoint, 6> {
504            addr: context,
505            value: DecodedPoint {
506                point: q.decode_a.point,
507                t2d: q.decode_a.t2d,
508            },
509            enable: true,
510        };
511    }
512    if q.decode_r.valid {
513        let context = q.decode_r.tag.resize::<6>();
514        n.status[context].r_ready = true;
515        n.status[context].r_valid = q.decode_r.point_valid;
516        n.status[context].r_small = q.decode_r.small_order;
517        r_write = RamWrite::<DecodedPoint, 6> {
518            addr: context,
519            value: DecodedPoint {
520                point: q.decode_r.point,
521                t2d: q.decode_r.t2d,
522            },
523            enable: true,
524        };
525    }
526    if q.fixed.valid {
527        let context = q.fixed.tag.resize::<6>();
528        n.status[context].fixed_ready = true;
529        fixed_write = RamWrite::<ExtendedPoint, 6> {
530            addr: context,
531            value: q.fixed.point,
532            enable: true,
533        };
534    }
535
536    let sha_results = [q.sha.result0, q.sha.result1];
537    let mut reducer_input = ScalarReducePipeIn {
538        valid: false,
539        wide: zero_words,
540        tag: b16(0),
541    };
542    for result_index in 0..2 {
543        let result = sha_results[result_index];
544        if result.valid {
545            let context = result.tag.resize::<6>();
546            let second = (result.tag & b9(0x40)) != b9(0);
547            if second {
548                reducer_input = ScalarReducePipeIn {
549                    valid: true,
550                    wide: digest_words(result.state),
551                    tag: context.resize::<16>(),
552                };
553            } else {
554                n.status[context].hash1_pending = true;
555                chain_write = RamWrite::<Words512, 6> {
556                    addr: context,
557                    value: Words512 {
558                        words: result.state,
559                    },
560                    enable: true,
561                };
562            }
563        }
564    }
565    if q.reducer.valid {
566        let context = q.reducer.tag.resize::<6>();
567        n.status[context].challenge_ready = true;
568        challenge_write = RamWrite::<ScalarWords, 6> {
569            addr: context,
570            value: q.reducer.scalar,
571            enable: true,
572        };
573    }
574
575    let hash_issue_accepted = r.hash_issue.valid && q.sha.request0_ready;
576    if hash_issue_accepted {
577        n.hash_issue.valid = false;
578    }
579    if r.hash_fetch.valid && (!r.hash_issue.valid || hash_issue_accepted) {
580        n.hash_issue = HashIssue {
581            valid: true,
582            context: r.hash_fetch.context,
583            state: q.memory.chain.words,
584        };
585        n.hash_fetch.valid = false;
586    }
587    if !n.hash_fetch.valid {
588        let mut found = false;
589        let mut selected = b6(0);
590        for offset in 0..64 {
591            let candidate = wrap64(r.hash_rr + b6(offset as u128));
592            if !found && r.status[candidate].used && r.status[candidate].hash1_pending {
593                found = true;
594                selected = candidate;
595            }
596        }
597        if found {
598            n.status[selected].hash1_pending = false;
599            n.hash_fetch = Fetch {
600                valid: true,
601                context: selected,
602            };
603            n.hash_rr = selected + b6(1);
604        }
605    }
606
607    let lane0_ready = q.variable0.ready;
608    let lane1_ready = q.variable1.ready;
609    let variable_input_valid = r.variable_fetch.valid;
610    let variable_consumed = variable_input_valid
611        && if r.variable_fetch.lane {
612            lane1_ready
613        } else {
614            lane0_ready
615        };
616    if variable_consumed {
617        n.variable_fetch.valid = false;
618    }
619    if !n.variable_fetch.valid {
620        let mut found = false;
621        let mut selected = b6(0);
622        for offset in 0..64 {
623            let candidate = wrap64(r.variable_rr + b6(offset as u128));
624            let status = r.status[candidate];
625            if !found
626                && status.used
627                && status.a_ready
628                && status.challenge_ready
629                && !status.variable_issued
630            {
631                found = true;
632                selected = candidate;
633            }
634        }
635        if found && (lane0_ready || lane1_ready) {
636            let lane = if lane0_ready && lane1_ready {
637                r.variable_lane_rr
638            } else {
639                lane1_ready
640            };
641            n.status[selected].variable_issued = true;
642            n.status[selected].variable_lane = lane;
643            n.variable_fetch = VariableFetch {
644                valid: true,
645                context: selected,
646                lane,
647            };
648            n.variable_rr = selected + b6(1);
649            n.variable_lane_rr = !lane;
650        }
651    }
652    if q.variable0.valid {
653        let context = q.variable0.tag.resize::<6>();
654        n.status[context].variable_ready = true;
655        variable0_write = RamWrite::<ExtendedPoint, 6> {
656            addr: context,
657            value: q.variable0.point,
658            enable: true,
659        };
660    }
661    if q.variable1.valid {
662        let context = q.variable1.tag.resize::<6>();
663        n.status[context].variable_ready = true;
664        variable1_write = RamWrite::<ExtendedPoint, 6> {
665            addr: context,
666            value: q.variable1.point,
667            enable: true,
668        };
669    }
670
671    let equation_consumed = r.equation_fetch.valid && q.equation.ready;
672    if equation_consumed {
673        n.equation_fetch.valid = false;
674    }
675    if !n.equation_fetch.valid {
676        let mut found = false;
677        let mut selected = b6(0);
678        for offset in 0..64 {
679            let candidate = wrap64(r.equation_rr + b6(offset as u128));
680            let status = r.status[candidate];
681            if !found
682                && status.used
683                && status.r_ready
684                && status.fixed_ready
685                && status.variable_ready
686                && !status.equation_issued
687            {
688                found = true;
689                selected = candidate;
690            }
691        }
692        if found && q.equation.ready {
693            n.status[selected].equation_issued = true;
694            n.equation_fetch = Fetch {
695                valid: true,
696                context: selected,
697            };
698            n.equation_rr = selected + b6(1);
699        }
700    }
701
702    if q.equation.valid {
703        let context = q.equation.tag.resize::<6>();
704        let status = r.status[context];
705        let error = if !status.a_valid {
706            b8(VERIFY_BAD_PUBLIC_KEY)
707        } else if !status.r_valid {
708            b8(VERIFY_BAD_R)
709        } else if status.a_small {
710            b8(VERIFY_SMALL_PUBLIC_KEY)
711        } else if status.r_small {
712            b8(VERIFY_SMALL_R)
713        } else if !status.canonical_s {
714            b8(VERIFY_NONCANONICAL_S)
715        } else if !q.equation.equal {
716            b8(VERIFY_EQUATION_FAILURE)
717        } else {
718            b8(VERIFY_OK)
719        };
720        output.valid = true;
721        output.ok = error == b8(VERIFY_OK);
722        output.error = error;
723        output.tag = status.tag;
724        n.status[context] = empty_status_kernel(false);
725    }
726
727    let hash_read = if n.hash_fetch.valid {
728        n.hash_fetch.context
729    } else {
730        r.hash_issue.context
731    };
732    let variable_read = if n.variable_fetch.valid {
733        n.variable_fetch.context
734    } else {
735        r.variable_fetch.context
736    };
737    let equation_read = if n.equation_fetch.valid {
738        n.equation_fetch.context
739    } else {
740        r.equation_fetch.context
741    };
742
743    let mut d = StrictVerifyCoreD::dont_care();
744    d.control = n;
745    d.memory = VerifyMemoryIn {
746        chain: RamIn::<Words512, 6> {
747            read_addr: hash_read,
748            write: chain_write,
749        },
750        a: RamIn::<DecodedPoint, 6> {
751            read_addr: variable_read,
752            write: a_write,
753        },
754        r: RamIn::<DecodedPoint, 6> {
755            read_addr: equation_read,
756            write: r_write,
757        },
758        fixed: RamIn::<ExtendedPoint, 6> {
759            read_addr: equation_read,
760            write: fixed_write,
761        },
762        challenge: RamIn::<ScalarWords, 6> {
763            read_addr: variable_read,
764            write: challenge_write,
765        },
766        variable0: RamIn::<ExtendedPoint, 6> {
767            read_addr: equation_read,
768            write: variable0_write,
769        },
770        variable1: RamIn::<ExtendedPoint, 6> {
771            read_addr: equation_read,
772            write: variable1_write,
773        },
774    };
775    d.decode_a = PointDecodeIn {
776        valid: accept,
777        encoded: CompressedPoint {
778            words: i.request.public_key.words,
779        },
780        tag: free_context.resize::<16>(),
781    };
782    d.decode_r = PointDecodeIn {
783        valid: accept,
784        encoded: request_r(i.request),
785        tag: free_context.resize::<16>(),
786    };
787    d.fixed = MultiCombMulIn {
788        valid: accept,
789        scalar: request_s(i.request),
790        tag: free_context.resize::<16>(),
791    };
792    d.sha = Sha512PoolIn {
793        request0: Sha512PoolRequest {
794            valid: r.hash_issue.valid,
795            tag: b9(0x40) | r.hash_issue.context.resize::<9>(),
796            state: r.hash_issue.state,
797            block: challenge_block1(),
798        },
799        request1: Sha512PoolRequest {
800            valid: accept,
801            tag: free_context.resize::<9>(),
802            state: sha_iv(),
803            block: challenge_block0(i.request),
804        },
805    };
806    d.reducer = reducer_input;
807    let variable_request = VariableBaseIn {
808        valid: variable_input_valid,
809        point: q.memory.a.point,
810        t2d: q.memory.a.t2d,
811        scalar: q.memory.challenge,
812        tag: r.variable_fetch.context.resize::<16>(),
813    };
814    d.variable0 = VariableBaseIn {
815        valid: variable_request.valid && !r.variable_fetch.lane,
816        ..variable_request
817    };
818    d.variable1 = VariableBaseIn {
819        valid: variable_request.valid && r.variable_fetch.lane,
820        ..variable_request
821    };
822    d.equation = EquationIn {
823        valid: r.equation_fetch.valid,
824        variable: if r.status[r.equation_fetch.context].variable_lane {
825            q.memory.variable1
826        } else {
827            q.memory.variable0
828        },
829        r: q.memory.r.point,
830        r_t2d: q.memory.r.t2d,
831        fixed: q.memory.fixed,
832        tag: r.equation_fetch.context.resize::<16>(),
833    };
834    (output, d)
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840    use crate::sha512_parallel::{Sha512Compress, Sha512CompressIn};
841    use curve25519_dalek::scalar::Scalar;
842    use ed25519_dalek::{Signer, SigningKey};
843    use rhdl::core::sim::ResetOrData;
844    use sha2::{Digest, Sha512};
845    use std::iter;
846
847    fn scalar_words(bytes: [u8; 32]) -> ScalarWords {
848        ScalarWords {
849            words: std::array::from_fn(|index| {
850                b64(u64::from_le_bytes(bytes[index * 8..index * 8 + 8].try_into().unwrap()) as u128)
851            }),
852        }
853    }
854
855    fn words512(bytes: [u8; 64]) -> Words512 {
856        Words512 {
857            words: std::array::from_fn(|index| {
858                b64(u64::from_le_bytes(bytes[index * 8..index * 8 + 8].try_into().unwrap()) as u128)
859            }),
860        }
861    }
862
863    fn idle() -> StrictVerifyIn {
864        StrictVerifyIn {
865            valid: false,
866            request: crate::protocol::zero_verify_request(),
867        }
868    }
869
870    fn compress_block(state: [b64; 8], block: [b64; 16]) -> [b64; 8] {
871        let idle = Sha512CompressIn {
872            start: false,
873            state: [b64(0); 8],
874            block: [b64(0); 16],
875        };
876        Sha512Compress::default()
877            .run(
878                iter::once(idle)
879                    .chain(iter::once(Sha512CompressIn {
880                        start: true,
881                        state,
882                        block,
883                    }))
884                    .chain(iter::repeat_n(idle, 250))
885                    .with_reset(1)
886                    .clock_pos_edge(100),
887            )
888            .synchronous_sample()
889            .map(|sample| sample.output)
890            .find(|output| output.valid)
891            .unwrap()
892            .state
893    }
894
895    #[test]
896    fn challenge_hash_and_reduction_match_dalek() {
897        let seed = [0x31; 32];
898        let message = [0x79; 64];
899        let key = SigningKey::from_bytes(&seed);
900        let signature = key.sign(&message).to_bytes();
901        let request = VerifyRequest {
902            message: words512(message),
903            public_key: scalar_words(key.verifying_key().to_bytes()),
904            signature: words512(signature),
905            tag: b16(0),
906        };
907        let first = compress_block(sha_iv(), challenge_block0(request));
908        let second = compress_block(first, challenge_block1());
909        let wide = digest_words(second);
910        let idle = ScalarReducePipeIn {
911            valid: false,
912            wide: Words512 { words: [b64(0); 8] },
913            tag: b16(0),
914        };
915        let reduced = ScalarReducePipe::default()
916            .run(
917                iter::once(idle)
918                    .chain(iter::once(ScalarReducePipeIn {
919                        valid: true,
920                        wide,
921                        tag: b16(7),
922                    }))
923                    .chain(iter::repeat_n(idle, 100))
924                    .with_reset(1)
925                    .clock_pos_edge(100),
926            )
927            .synchronous_sample()
928            .map(|sample| sample.output)
929            .find(|output| output.valid)
930            .unwrap();
931        let mut hasher = Sha512::new();
932        hasher.update(&signature[..32]);
933        hasher.update(key.verifying_key().to_bytes());
934        hasher.update(message);
935        let digest: [u8; 64] = hasher.finalize().into();
936        let expected = Scalar::from_bytes_mod_order_wide(&digest).to_bytes();
937        let actual = std::array::from_fn(|index| {
938            ((reduced.scalar.words[index / 8].raw() >> ((index % 8) * 8)) & 0xff) as u8
939        });
940        assert_eq!(actual, expected);
941    }
942
943    #[test]
944    fn accepts_valid_and_rejects_modified_message() {
945        std::thread::Builder::new()
946            .name("strict-verify-sim".into())
947            .stack_size(512 * 1024 * 1024)
948            .spawn(|| {
949                let seed = std::array::from_fn(|index| (index as u8).wrapping_mul(19));
950                let message = std::array::from_fn(|index| (index as u8).wrapping_mul(37));
951                let key = SigningKey::from_bytes(&seed);
952                let signature = key.sign(&message).to_bytes();
953                let mut modified = message;
954                modified[17] ^= 0x80;
955                let valid = StrictVerifyIn {
956                    valid: true,
957                    request: VerifyRequest {
958                        message: words512(message),
959                        public_key: scalar_words(key.verifying_key().to_bytes()),
960                        signature: words512(signature),
961                        tag: b16(0x101),
962                    },
963                };
964                let invalid = StrictVerifyIn {
965                    valid: true,
966                    request: VerifyRequest {
967                        message: words512(modified),
968                        tag: b16(0x102),
969                        ..valid.request
970                    },
971                };
972                let requests = [valid, invalid];
973                let mut need_reset = true;
974                let mut request_index = 0usize;
975                let mut result_count = 0usize;
976                let outputs: Vec<_> = StrictVerifyCore::default()
977                    .run_fn(
978                        move |output| {
979                            if result_count == requests.len() {
980                                return None;
981                            }
982                            if need_reset {
983                                need_reset = false;
984                                return Some(ResetOrData::Reset);
985                            }
986                            if output.valid {
987                                result_count += 1;
988                            }
989                            let mut input = idle();
990                            if request_index < requests.len() && output.ready {
991                                input = requests[request_index];
992                                request_index += 1;
993                            }
994                            Some(ResetOrData::Data(input))
995                        },
996                        100,
997                    )
998                    .take_while(|sample| sample.time < 4_000_000)
999                    .synchronous_sample()
1000                    .map(|sample| sample.output)
1001                    .filter(|output| output.valid)
1002                    .collect();
1003                assert_eq!(outputs.len(), 2);
1004                let accepted = outputs
1005                    .iter()
1006                    .find(|output| output.tag == b16(0x101))
1007                    .unwrap();
1008                assert!(
1009                    accepted.ok,
1010                    "valid signature error={}",
1011                    accepted.error.raw()
1012                );
1013                let rejected = outputs
1014                    .iter()
1015                    .find(|output| output.tag == b16(0x102))
1016                    .unwrap();
1017                assert!(!rejected.ok);
1018                assert_eq!(rejected.error, b8(VERIFY_EQUATION_FAILURE));
1019            })
1020            .unwrap()
1021            .join()
1022            .unwrap();
1023    }
1024}