Skip to main content

rhdl_ed25519_fast_core/
sign.rs

1//! Cached-key, fixed-64-byte Ed25519 signing pipeline.
2//!
3//! The pipeline holds 128 independent requests and schedules their three
4//! SHA-512 blocks, scalar reductions, fixed-base multiplications, paired point
5//! compression, and scalar multiply-add operations independently. Messages and
6//! intermediate values live in inferred BRAM; no DRAM, HBM, or host prehash is
7//! part of the cryptographic 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    field_parallel::FieldElement51,
17    multicomb::{ExtendedPoint, MultiCombMulIn},
18    point_compress::{CompressedPoint, PointCompressPairIn},
19    protocol::{SignRequest, Words512},
20    scalar::ScalarWords,
21    scalar_parallel::{
22        ScalarReducePipe, ScalarReducePipeIn, ScalarWideMulIn, ScalarWideMulPipe,
23        scalar_product_addend_kernel,
24    },
25    sha512_parallel::{Sha512PoolIn, Sha512PoolRequest},
26};
27
28#[cfg(feature = "wide-sign")]
29use crate::parallel::{
30    DualMultiComb as MultiCombMul, DualPointCompress as PointCompressPair,
31    Sha512Pool8 as Sha512Pool,
32};
33#[cfg(not(feature = "wide-sign"))]
34use crate::{
35    multicomb::MultiCombMul, point_compress::PointCompressPair, sha512_parallel::Sha512Pool,
36};
37
38#[derive(Copy, Clone, Debug, PartialEq, Digital)]
39#[doc(hidden)]
40pub struct HashBlockInput {
41    kind: b2,
42    seed: ScalarWords,
43    prefix: ScalarWords,
44    public_key: ScalarWords,
45    encoded_r: CompressedPoint,
46    message: Words512,
47}
48
49const SIGN_FREE: u128 = 0;
50const SIGN_NONCE_HASH: u128 = 1;
51const SIGN_NONCE_WAIT: u128 = 2;
52const SIGN_POINT: u128 = 3;
53const SIGN_POINT_WAIT: u128 = 4;
54const SIGN_COMPRESS_WAIT: u128 = 5;
55const SIGN_CHALLENGE0_HASH: u128 = 6;
56const SIGN_CHALLENGE0_WAIT: u128 = 7;
57const SIGN_CHALLENGE1_HASH: u128 = 8;
58const SIGN_CHALLENGE1_WAIT: u128 = 9;
59const SIGN_MULADD: u128 = 10;
60const SIGN_MULADD_WAIT: u128 = 11;
61
62const HASH_NONCE: u128 = 0;
63const HASH_CHALLENGE0: u128 = 1;
64const HASH_CHALLENGE1: u128 = 2;
65const HASH_SEED: u128 = 3;
66
67/// Public input of [`CachedSignCore`].
68#[derive(Copy, Clone, Debug, PartialEq, Digital)]
69pub struct CachedSignIn {
70    /// Loads and expands a key seed when [`CachedSignOut::load_key_ready`] is high.
71    pub load_key_valid: bool,
72    /// Secret 32-byte Ed25519 seed.
73    pub key_seed: ScalarWords,
74    /// Explicitly clears the cached expanded key.
75    pub clear_key: bool,
76    /// Marks [`Self::request`] valid.
77    pub valid: bool,
78    /// Fixed-64-byte signing request.
79    pub request: SignRequest,
80}
81
82/// Public output of [`CachedSignCore`].
83#[derive(Copy, Clone, Debug, PartialEq, Digital)]
84pub struct CachedSignOut {
85    /// High when a seed can be accepted.
86    pub load_key_ready: bool,
87    /// High after hardware key expansion and public-key derivation complete.
88    pub key_loaded: bool,
89    /// Cached compressed public key.
90    pub public_key: ScalarWords,
91    /// High when a signing request can be accepted.
92    pub ready: bool,
93    /// High for one cycle when [`Self::signature`] and [`Self::tag`] are valid.
94    pub valid: bool,
95    /// `R || S` signature, packed in little-endian words.
96    pub signature: Words512,
97    /// Delayed request tag.
98    pub tag: b16,
99    /// Number of occupied signing contexts.
100    pub occupancy: b8,
101    /// Sticky scheduler overflow or invariant failure.
102    pub internal_error: bool,
103}
104
105#[derive(Copy, Clone, Debug, PartialEq, Digital)]
106#[doc(hidden)]
107pub struct HashFetch {
108    valid: bool,
109    kind: b2,
110    context: b7,
111}
112
113#[derive(Copy, Clone, Debug, PartialEq, Digital)]
114#[doc(hidden)]
115pub struct HashIssue {
116    valid: bool,
117    state: [b64; 8],
118    block: [b64; 16],
119    tag: b9,
120}
121
122#[derive(Copy, Clone, Debug, PartialEq, Digital)]
123#[doc(hidden)]
124pub struct MulFetch {
125    valid: bool,
126    context: b7,
127}
128
129#[derive(Copy, Clone, Debug, PartialEq, Digital)]
130#[doc(hidden)]
131pub struct PointFetch {
132    valid: bool,
133    is_key: bool,
134    context: b7,
135}
136
137#[derive(Copy, Clone, Debug, PartialEq, Digital)]
138#[doc(hidden)]
139pub struct PointIssue {
140    valid: bool,
141    is_key: bool,
142    context: b7,
143    scalar: ScalarWords,
144}
145
146#[derive(Copy, Clone, Debug, PartialEq, Digital)]
147#[doc(hidden)]
148pub struct OutputPending {
149    valid: bool,
150    context: b7,
151    scalar: ScalarWords,
152}
153
154#[derive(Copy, Clone, Debug, PartialEq, Digital)]
155#[doc(hidden)]
156pub struct SignMulStage {
157    valid: bool,
158    wide: Words512,
159    tag: b16,
160}
161
162#[derive(Copy, Clone, Debug, PartialEq, Digital)]
163#[doc(hidden)]
164pub struct SignControl {
165    key_loaded: bool,
166    key_busy: bool,
167    key_hash_pending: bool,
168    key_point_pending: bool,
169    key_seed: ScalarWords,
170    key_prefix: ScalarWords,
171    key_secret: ScalarWords,
172    public_key: ScalarWords,
173    states: [b4; 128],
174    tags: [b16; 128],
175    free_fifo: [b7; 128],
176    free_read: b7,
177    free_write: b7,
178    free_count: b8,
179    hash_rr: b7,
180    point_rr: b7,
181    mul_rr: b7,
182    hash_fetch: HashFetch,
183    hash_issue: HashIssue,
184    point_fetch: PointFetch,
185    point_issue: PointIssue,
186    mul_fetch: MulFetch,
187    output_pending: OutputPending,
188    internal_error: bool,
189}
190
191fn reset_control() -> SignControl {
192    let mut free_fifo = [b7(0); 128];
193    for (index, entry) in free_fifo.iter_mut().enumerate() {
194        *entry = b7(index as u128);
195    }
196    SignControl {
197        key_loaded: false,
198        key_busy: false,
199        key_hash_pending: false,
200        key_point_pending: false,
201        key_seed: ScalarWords { words: [b64(0); 4] },
202        key_prefix: ScalarWords { words: [b64(0); 4] },
203        key_secret: ScalarWords { words: [b64(0); 4] },
204        public_key: ScalarWords { words: [b64(0); 4] },
205        states: [b4(SIGN_FREE); 128],
206        tags: [b16(0); 128],
207        free_fifo,
208        free_read: b7(0),
209        free_write: b7(0),
210        free_count: b8(128),
211        hash_rr: b7(0),
212        point_rr: b7(0),
213        mul_rr: b7(0),
214        hash_fetch: HashFetch {
215            valid: false,
216            kind: b2(0),
217            context: b7(0),
218        },
219        hash_issue: HashIssue {
220            valid: false,
221            state: [b64(0); 8],
222            block: [b64(0); 16],
223            tag: b9(0),
224        },
225        point_fetch: PointFetch {
226            valid: false,
227            is_key: false,
228            context: b7(0),
229        },
230        point_issue: PointIssue {
231            valid: false,
232            is_key: false,
233            context: b7(0),
234            scalar: ScalarWords { words: [b64(0); 4] },
235        },
236        mul_fetch: MulFetch {
237            valid: false,
238            context: b7(0),
239        },
240        output_pending: OutputPending {
241            valid: false,
242            context: b7(0),
243            scalar: ScalarWords { words: [b64(0); 4] },
244        },
245        internal_error: false,
246    }
247}
248
249#[derive(Copy, Clone, Debug, PartialEq, Digital)]
250#[doc(hidden)]
251pub struct SignMemoryIn {
252    message: RamIn<Words512, 7>,
253    chain: RamIn<Words512, 7>,
254    nonce_point: RamIn<ScalarWords, 7>,
255    nonce_mul: RamIn<ScalarWords, 7>,
256    challenge: RamIn<ScalarWords, 7>,
257}
258
259#[derive(Copy, Clone, Debug, PartialEq, Digital)]
260#[doc(hidden)]
261pub struct SignMemoryOut {
262    message: Words512,
263    chain: Words512,
264    nonce_point: ScalarWords,
265    nonce_mul: ScalarWords,
266    challenge: ScalarWords,
267}
268
269/// On-chip signing context memories. The duplicated encoded-R memory provides
270/// independent challenge-hash and result-retirement read ports.
271#[derive(Clone, Debug, Synchronous, SynchronousDQ, Default)]
272#[doc(hidden)]
273pub struct SignMemory {
274    message: SyncBRAM<Words512, 7>,
275    chain: SyncBRAM<Words512, 7>,
276    nonce_point: SyncBRAM<ScalarWords, 7>,
277    nonce_mul: SyncBRAM<ScalarWords, 7>,
278    challenge: SyncBRAM<ScalarWords, 7>,
279}
280
281impl SynchronousIO for SignMemory {
282    type I = SignMemoryIn;
283    type O = SignMemoryOut;
284    type Kernel = sign_memory_kernel;
285}
286
287#[kernel]
288#[doc(hidden)]
289pub fn sign_memory_kernel(
290    _cr: ClockReset,
291    i: SignMemoryIn,
292    q: SignMemoryQ,
293) -> (SignMemoryOut, SignMemoryD) {
294    let output = SignMemoryOut {
295        message: q.message,
296        chain: q.chain,
297        nonce_point: q.nonce_point,
298        nonce_mul: q.nonce_mul,
299        challenge: q.challenge,
300    };
301    let mut d = SignMemoryD::dont_care();
302    d.message = i.message;
303    d.chain = i.chain;
304    d.nonce_point = i.nonce_point;
305    d.nonce_mul = i.nonce_mul;
306    d.challenge = i.challenge;
307    (output, d)
308}
309
310#[derive(Copy, Clone, Debug, PartialEq, Digital)]
311#[doc(hidden)]
312pub struct EncodedRWrite {
313    enable: bool,
314    context: b7,
315    value: CompressedPoint,
316}
317
318#[derive(Copy, Clone, Debug, PartialEq, Digital)]
319#[doc(hidden)]
320pub struct EncodedRStoreIn {
321    read_hash: b7,
322    read_result: b7,
323    write0: EncodedRWrite,
324    write1: EncodedRWrite,
325}
326
327#[derive(Copy, Clone, Debug, PartialEq, Digital)]
328#[doc(hidden)]
329pub struct EncodedRStoreOut {
330    hash: CompressedPoint,
331    result: CompressedPoint,
332}
333
334#[derive(Copy, Clone, Debug, PartialEq, Digital)]
335#[doc(hidden)]
336pub struct EncodedRBanks {
337    bank0: [CompressedPoint; 16],
338    bank1: [CompressedPoint; 16],
339    bank2: [CompressedPoint; 16],
340    bank3: [CompressedPoint; 16],
341    bank4: [CompressedPoint; 16],
342    bank5: [CompressedPoint; 16],
343    bank6: [CompressedPoint; 16],
344    bank7: [CompressedPoint; 16],
345}
346
347/// Two-write register file with two synchronous read ports for compressed points.
348///
349/// Eight physical banks keep each resettable RHDL array small. Compression
350/// produces two points per cycle, so two logical write ports are required to
351/// preserve the one-pair-per-cycle retirement burst.
352#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
353#[doc(hidden)]
354pub struct EncodedRStore {
355    read_data: DFF<[CompressedPoint; 2]>,
356    bank0: DFF<[CompressedPoint; 16]>,
357    bank1: DFF<[CompressedPoint; 16]>,
358    bank2: DFF<[CompressedPoint; 16]>,
359    bank3: DFF<[CompressedPoint; 16]>,
360    bank4: DFF<[CompressedPoint; 16]>,
361    bank5: DFF<[CompressedPoint; 16]>,
362    bank6: DFF<[CompressedPoint; 16]>,
363    bank7: DFF<[CompressedPoint; 16]>,
364}
365
366impl Default for EncodedRStore {
367    fn default() -> Self {
368        let zero = CompressedPoint { words: [b64(0); 4] };
369        Self {
370            read_data: DFF::new([zero; 2]),
371            bank0: DFF::new([zero; 16]),
372            bank1: DFF::new([zero; 16]),
373            bank2: DFF::new([zero; 16]),
374            bank3: DFF::new([zero; 16]),
375            bank4: DFF::new([zero; 16]),
376            bank5: DFF::new([zero; 16]),
377            bank6: DFF::new([zero; 16]),
378            bank7: DFF::new([zero; 16]),
379        }
380    }
381}
382
383impl SynchronousIO for EncodedRStore {
384    type I = EncodedRStoreIn;
385    type O = EncodedRStoreOut;
386    type Kernel = encoded_r_store_kernel;
387}
388
389#[kernel]
390fn encoded_r_read(address: b7, banks: EncodedRBanks) -> CompressedPoint {
391    let index = address.resize::<4>();
392    match (address >> 4).resize::<3>().raw() {
393        0 => banks.bank0[index],
394        1 => banks.bank1[index],
395        2 => banks.bank2[index],
396        3 => banks.bank3[index],
397        4 => banks.bank4[index],
398        5 => banks.bank5[index],
399        6 => banks.bank6[index],
400        _ => banks.bank7[index],
401    }
402}
403
404#[kernel]
405#[doc(hidden)]
406pub fn encoded_r_store_kernel(
407    _cr: ClockReset,
408    i: EncodedRStoreIn,
409    q: EncodedRStoreQ,
410) -> (EncodedRStoreOut, EncodedRStoreD) {
411    let mut bank0 = q.bank0;
412    let mut bank1 = q.bank1;
413    let mut bank2 = q.bank2;
414    let mut bank3 = q.bank3;
415    let mut bank4 = q.bank4;
416    let mut bank5 = q.bank5;
417    let mut bank6 = q.bank6;
418    let mut bank7 = q.bank7;
419    if i.write0.enable {
420        let index = i.write0.context.resize::<4>();
421        match (i.write0.context >> 4).resize::<3>().raw() {
422            0 => bank0[index] = i.write0.value,
423            1 => bank1[index] = i.write0.value,
424            2 => bank2[index] = i.write0.value,
425            3 => bank3[index] = i.write0.value,
426            4 => bank4[index] = i.write0.value,
427            5 => bank5[index] = i.write0.value,
428            6 => bank6[index] = i.write0.value,
429            _ => bank7[index] = i.write0.value,
430        }
431    }
432    if i.write1.enable {
433        let index = i.write1.context.resize::<4>();
434        match (i.write1.context >> 4).resize::<3>().raw() {
435            0 => bank0[index] = i.write1.value,
436            1 => bank1[index] = i.write1.value,
437            2 => bank2[index] = i.write1.value,
438            3 => bank3[index] = i.write1.value,
439            4 => bank4[index] = i.write1.value,
440            5 => bank5[index] = i.write1.value,
441            6 => bank6[index] = i.write1.value,
442            _ => bank7[index] = i.write1.value,
443        }
444    }
445    let banks = EncodedRBanks {
446        bank0: q.bank0,
447        bank1: q.bank1,
448        bank2: q.bank2,
449        bank3: q.bank3,
450        bank4: q.bank4,
451        bank5: q.bank5,
452        bank6: q.bank6,
453        bank7: q.bank7,
454    };
455    let output = EncodedRStoreOut {
456        hash: q.read_data[0],
457        result: q.read_data[1],
458    };
459    let mut d = EncodedRStoreD::dont_care();
460    // Match the one-cycle read contract of the adjacent SyncBRAM stores.
461    d.read_data = [
462        encoded_r_read(i.read_hash, banks),
463        encoded_r_read(i.read_result, banks),
464    ];
465    d.bank0 = bank0;
466    d.bank1 = bank1;
467    d.bank2 = bank2;
468    d.bank3 = bank3;
469    d.bank4 = bank4;
470    d.bank5 = bank5;
471    d.bank6 = bank6;
472    d.bank7 = bank7;
473    (output, d)
474}
475
476#[derive(Copy, Clone, Debug, PartialEq, Digital)]
477#[doc(hidden)]
478pub struct TaggedPoint {
479    context: b7,
480    point: ExtendedPoint,
481}
482
483#[derive(Copy, Clone, Debug, PartialEq, Digital)]
484#[doc(hidden)]
485pub struct PointPairFifoIn {
486    push: bool,
487    value: TaggedPoint,
488    pop: bool,
489}
490
491#[derive(Copy, Clone, Debug, PartialEq, Digital)]
492#[doc(hidden)]
493pub struct PointPairFifoOut {
494    valid: bool,
495    second_valid: bool,
496    first: TaggedPoint,
497    second: TaggedPoint,
498    count: b6,
499    overflow: bool,
500}
501
502#[derive(Copy, Clone, Debug, PartialEq, Digital)]
503#[doc(hidden)]
504pub struct PointPairFifoControl {
505    read_pair: b4,
506    write_point: b5,
507    count: b6,
508    read_pending: bool,
509    head_valid: bool,
510    readable: b2,
511    overflow: bool,
512}
513
514/// Thirty-two-point even/odd BRAM queue with a two-point dequeue interface.
515#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
516#[doc(hidden)]
517pub struct PointPairFifo {
518    control: DFF<PointPairFifoControl>,
519    even: SyncBRAM<TaggedPoint, 4>,
520    odd: SyncBRAM<TaggedPoint, 4>,
521}
522
523impl Default for PointPairFifo {
524    fn default() -> Self {
525        Self {
526            control: DFF::new(PointPairFifoControl {
527                read_pair: b4(0),
528                write_point: b5(0),
529                count: b6(0),
530                read_pending: false,
531                head_valid: false,
532                readable: b2(0),
533                overflow: false,
534            }),
535            even: SyncBRAM::default(),
536            odd: SyncBRAM::default(),
537        }
538    }
539}
540
541impl SynchronousIO for PointPairFifo {
542    type I = PointPairFifoIn;
543    type O = PointPairFifoOut;
544    type Kernel = point_pair_fifo_kernel;
545}
546
547#[kernel]
548#[doc(hidden)]
549pub fn point_pair_fifo_kernel(
550    _cr: ClockReset,
551    i: PointPairFifoIn,
552    q: PointPairFifoQ,
553) -> (PointPairFifoOut, PointPairFifoD) {
554    let r = q.control;
555    let mut n = r;
556    let zero = FieldElement51 { limbs: [b51(0); 5] };
557    let empty = TaggedPoint {
558        context: b7(0),
559        point: ExtendedPoint {
560            x: zero,
561            y: zero,
562            z: zero,
563            t: zero,
564        },
565    };
566    let pop = i.pop && r.head_valid;
567    let pop_two = pop && r.count >= b6(2);
568    if r.read_pending {
569        n.read_pending = false;
570        n.head_valid = true;
571    }
572    if pop {
573        n.head_valid = false;
574        n.read_pair = r.read_pair + b4(1);
575        if !pop_two {
576            n.write_point = ((r.read_pair + b4(1)).resize::<5>()) << 1;
577        }
578    }
579    let count_after_pop = if pop_two {
580        r.count - b6(2)
581    } else if pop {
582        r.count - b6(1)
583    } else {
584        r.count
585    };
586    // SyncBRAM returns pre-write contents. A just-enqueued second point is
587    // counted as occupied immediately, but cannot be read until the next edge.
588    n.readable = if count_after_pop >= b6(2) {
589        b2(2)
590    } else {
591        count_after_pop.resize::<2>()
592    };
593    if i.push {
594        if count_after_pop < b6(32) {
595            n.write_point = r.write_point + b5(1);
596        } else {
597            n.overflow = true;
598        }
599    }
600    n.count = count_after_pop
601        + if i.push && count_after_pop < b6(32) {
602            b6(1)
603        } else {
604            b6(0)
605        };
606    if !n.head_valid && !n.read_pending && n.count >= b6(1) {
607        n.read_pending = true;
608    }
609    let write = i.push && count_after_pop < b6(32);
610    let even_write = RamWrite::<TaggedPoint, 4> {
611        addr: (r.write_point >> 1).resize::<4>(),
612        value: i.value,
613        enable: write && (r.write_point & b5(1)) == b5(0),
614    };
615    let odd_write = RamWrite::<TaggedPoint, 4> {
616        addr: (r.write_point >> 1).resize::<4>(),
617        value: i.value,
618        enable: write && (r.write_point & b5(1)) != b5(0),
619    };
620    let output = PointPairFifoOut {
621        valid: r.head_valid && r.readable != b2(0),
622        second_valid: r.head_valid && r.readable == b2(2),
623        first: if r.head_valid { q.even } else { empty },
624        second: if r.head_valid { q.odd } else { empty },
625        count: r.count,
626        overflow: r.overflow,
627    };
628    let mut d = PointPairFifoD::dont_care();
629    d.control = n;
630    d.even = RamIn::<TaggedPoint, 4> {
631        read_addr: n.read_pair,
632        write: even_write,
633    };
634    d.odd = RamIn::<TaggedPoint, 4> {
635        read_addr: n.read_pair,
636        write: odd_write,
637    };
638    (output, d)
639}
640
641/// Native-RHDL cached-key signing core for exactly 64-byte messages.
642#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
643pub struct CachedSignCore {
644    control: DFF<SignControl>,
645    memory: SignMemory,
646    encoded_r: EncodedRStore,
647    sha: Sha512Pool,
648    hash_reducer: ScalarReducePipe,
649    point: MultiCombMul,
650    point_fifo: PointPairFifo,
651    compressor: PointCompressPair,
652    mul_multiplier: ScalarWideMulPipe,
653    mul_add: DFF<SignMulStage>,
654    mul_reducer: ScalarReducePipe,
655}
656
657impl Default for CachedSignCore {
658    fn default() -> Self {
659        Self {
660            control: DFF::new(reset_control()),
661            memory: SignMemory::default(),
662            encoded_r: EncodedRStore::default(),
663            sha: Sha512Pool::default(),
664            hash_reducer: ScalarReducePipe::default(),
665            point: MultiCombMul::default(),
666            point_fifo: PointPairFifo::default(),
667            compressor: PointCompressPair::default(),
668            mul_multiplier: ScalarWideMulPipe::default(),
669            mul_add: DFF::new(SignMulStage {
670                valid: false,
671                wide: Words512 { words: [b64(0); 8] },
672                tag: b16(0),
673            }),
674            mul_reducer: ScalarReducePipe::default(),
675        }
676    }
677}
678
679impl SynchronousIO for CachedSignCore {
680    type I = CachedSignIn;
681    type O = CachedSignOut;
682    type Kernel = cached_sign_kernel;
683}
684
685#[kernel]
686fn next_context(value: b7) -> b7 {
687    value + b7(1)
688}
689
690#[kernel]
691fn scan_context(start: b7, offset: b8) -> b7 {
692    (start.resize::<8>() + offset).resize::<7>()
693}
694
695#[kernel]
696pub(crate) fn reverse_bytes(value: b64) -> b64 {
697    let mut result = b64(0);
698    for byte in 0..8 {
699        result |= ((value >> (byte * 8) as u128).resize::<8>()).resize::<64>()
700            << ((7 - byte) * 8) as u128;
701    }
702    result
703}
704
705#[kernel]
706pub(crate) fn sha_iv() -> [b64; 8] {
707    [
708        b64(0x6a09e667f3bcc908),
709        b64(0xbb67ae8584caa73b),
710        b64(0x3c6ef372fe94f82b),
711        b64(0xa54ff53a5f1d36f1),
712        b64(0x510e527fade682d1),
713        b64(0x9b05688c2b3e6c1f),
714        b64(0x1f83d9abfb41bd6b),
715        b64(0x5be0cd19137e2179),
716    ]
717}
718
719#[kernel]
720pub(crate) fn words256(value: ScalarWords) -> [b64; 4] {
721    [
722        reverse_bytes(value.words[0]),
723        reverse_bytes(value.words[1]),
724        reverse_bytes(value.words[2]),
725        reverse_bytes(value.words[3]),
726    ]
727}
728
729#[kernel]
730fn build_hash_block(input: HashBlockInput) -> [b64; 16] {
731    let mut block = [b64(0); 16];
732    if input.kind == b2(HASH_SEED) {
733        let seed_words = words256(input.seed);
734        for index in 0..4 {
735            block[index] = seed_words[index];
736        }
737        block[4] = b64(0x8000000000000000);
738        block[15] = b64(0x100);
739    } else if input.kind == b2(HASH_NONCE) {
740        let prefix_words = words256(input.prefix);
741        for index in 0..4 {
742            block[index] = prefix_words[index];
743        }
744        for index in 0..8 {
745            block[index + 4] = reverse_bytes(input.message.words[index]);
746        }
747        block[12] = b64(0x8000000000000000);
748        block[15] = b64(0x300);
749    } else if input.kind == b2(HASH_CHALLENGE0) {
750        for index in 0..4 {
751            block[index] = reverse_bytes(input.encoded_r.words[index]);
752            block[index + 4] = reverse_bytes(input.public_key.words[index]);
753        }
754        for index in 0..8 {
755            block[index + 8] = reverse_bytes(input.message.words[index]);
756        }
757    } else {
758        block[0] = b64(0x8000000000000000);
759        block[15] = b64(0x400);
760    }
761    block
762}
763
764#[kernel]
765pub(crate) fn digest_words(state: [b64; 8]) -> Words512 {
766    let mut words = [b64(0); 8];
767    for index in 0..8 {
768        words[index] = reverse_bytes(state[index]);
769    }
770    Words512 { words }
771}
772
773#[kernel]
774pub(crate) fn secret_from_digest(state: [b64; 8]) -> ScalarWords {
775    let digest = digest_words(state);
776    let mut words = [
777        digest.words[0],
778        digest.words[1],
779        digest.words[2],
780        digest.words[3],
781    ];
782    words[0] &= b64(0xfffffffffffffff8);
783    words[3] = (words[3] & b64(0x3fffffffffffffff)) | b64(0x4000000000000000);
784    ScalarWords { words }
785}
786
787#[kernel]
788pub(crate) fn prefix_from_digest(state: [b64; 8]) -> ScalarWords {
789    let digest = digest_words(state);
790    ScalarWords {
791        words: [
792            digest.words[4],
793            digest.words[5],
794            digest.words[6],
795            digest.words[7],
796        ],
797    }
798}
799
800#[kernel]
801fn compressed_as_scalar(value: CompressedPoint) -> ScalarWords {
802    ScalarWords { words: value.words }
803}
804
805#[kernel]
806fn scalar_as_compressed(value: ScalarWords) -> CompressedPoint {
807    CompressedPoint { words: value.words }
808}
809
810/// State transition for [`CachedSignCore`].
811#[kernel]
812pub fn cached_sign_kernel(
813    _cr: ClockReset,
814    i: CachedSignIn,
815    q: CachedSignCoreQ,
816) -> (CachedSignOut, CachedSignCoreD) {
817    let r = q.control;
818    let mut n = r;
819    let zero_scalar = ScalarWords { words: [b64(0); 4] };
820    let zero_words = Words512 { words: [b64(0); 8] };
821    let zero_field = FieldElement51 { limbs: [b51(0); 5] };
822    let one_field = FieldElement51 {
823        limbs: [b51(1), b51(0), b51(0), b51(0), b51(0)],
824    };
825    let identity = ExtendedPoint {
826        x: zero_field,
827        y: one_field,
828        z: one_field,
829        t: zero_field,
830    };
831    let mut output = CachedSignOut {
832        load_key_ready: !r.key_loaded && !r.key_busy,
833        key_loaded: r.key_loaded,
834        public_key: r.public_key,
835        ready: r.key_loaded && r.free_count != b8(0),
836        valid: false,
837        signature: zero_words,
838        tag: b16(0),
839        occupancy: b8(128) - r.free_count,
840        internal_error: r.internal_error
841            || q.point_fifo.overflow
842            || q.point.error
843            || q.compressor.error,
844    };
845
846    let no_message_write = RamWrite::<Words512, 7> {
847        addr: b7(0),
848        value: zero_words,
849        enable: false,
850    };
851    let no_scalar_write = RamWrite::<ScalarWords, 7> {
852        addr: b7(0),
853        value: zero_scalar,
854        enable: false,
855    };
856    let no_r_write = EncodedRWrite {
857        enable: false,
858        context: b7(0),
859        value: CompressedPoint { words: [b64(0); 4] },
860    };
861    let mut message_write = no_message_write;
862    let mut r_write0 = no_r_write;
863    let mut r_write1 = no_r_write;
864    let mut chain_write = no_message_write;
865    let mut nonce_write = no_scalar_write;
866    let mut challenge_write = no_scalar_write;
867
868    if i.clear_key {
869        n.key_loaded = false;
870        n.key_busy = false;
871        n.key_hash_pending = false;
872        n.key_point_pending = false;
873        n.key_seed = zero_scalar;
874        n.key_prefix = zero_scalar;
875        n.key_secret = zero_scalar;
876        n.public_key = zero_scalar;
877    }
878    if i.load_key_valid && !r.key_loaded && !r.key_busy {
879        n.key_busy = true;
880        n.key_hash_pending = true;
881        n.key_seed = i.key_seed;
882    }
883
884    let accept = i.valid && r.key_loaded && r.free_count != b8(0);
885    if accept {
886        let context = r.free_fifo[r.free_read];
887        n.states[context] = b4(SIGN_NONCE_HASH);
888        n.tags[context] = i.request.tag;
889        n.free_read = next_context(r.free_read);
890        n.free_count = r.free_count - b8(1);
891        message_write = RamWrite::<Words512, 7> {
892            addr: context,
893            value: i.request.message,
894            enable: true,
895        };
896    }
897
898    let issue_accept = r.hash_issue.valid && q.sha.request0_ready;
899    if issue_accept {
900        n.hash_issue.valid = false;
901    }
902    let issue_available = !r.hash_issue.valid || issue_accept;
903    if r.hash_fetch.valid && issue_available {
904        let kind = r.hash_fetch.kind;
905        n.hash_issue = HashIssue {
906            valid: true,
907            state: if kind == b2(HASH_CHALLENGE1) {
908                q.memory.chain.words
909            } else {
910                sha_iv()
911            },
912            block: build_hash_block(HashBlockInput {
913                kind,
914                seed: r.key_seed,
915                prefix: r.key_prefix,
916                public_key: r.public_key,
917                encoded_r: q.encoded_r.hash,
918                message: q.memory.message,
919            }),
920            tag: (kind.resize::<9>() << 7) | r.hash_fetch.context.resize::<9>(),
921        };
922        n.hash_fetch.valid = false;
923    }
924
925    let fetch_available = !r.hash_fetch.valid || issue_available;
926    let mut hash_found = false;
927    let mut hash_kind = b2(HASH_NONCE);
928    let mut hash_context = b7(0);
929    if fetch_available {
930        if r.key_hash_pending {
931            hash_found = true;
932            hash_kind = b2(HASH_SEED);
933            n.key_hash_pending = false;
934        } else {
935            for offset in 0..8 {
936                let context = scan_context(r.hash_rr, b8(offset as u128));
937                let state = r.states[context];
938                if !hash_found
939                    && (state == b4(SIGN_NONCE_HASH)
940                        || state == b4(SIGN_CHALLENGE0_HASH)
941                        || state == b4(SIGN_CHALLENGE1_HASH))
942                {
943                    hash_found = true;
944                    hash_context = context;
945                    hash_kind = if state == b4(SIGN_NONCE_HASH) {
946                        b2(HASH_NONCE)
947                    } else if state == b4(SIGN_CHALLENGE0_HASH) {
948                        b2(HASH_CHALLENGE0)
949                    } else {
950                        b2(HASH_CHALLENGE1)
951                    };
952                    n.states[context] = if state == b4(SIGN_NONCE_HASH) {
953                        b4(SIGN_NONCE_WAIT)
954                    } else if state == b4(SIGN_CHALLENGE0_HASH) {
955                        b4(SIGN_CHALLENGE0_WAIT)
956                    } else {
957                        b4(SIGN_CHALLENGE1_WAIT)
958                    };
959                }
960            }
961            n.hash_rr = scan_context(r.hash_rr, b8(8));
962        }
963        if hash_found {
964            n.hash_fetch = HashFetch {
965                valid: true,
966                kind: hash_kind,
967                context: hash_context,
968            };
969        }
970    }
971
972    let sha_result = q.sha.result0;
973    let sha_kind = (sha_result.tag >> 7).resize::<2>();
974    let sha_context = sha_result.tag.resize::<7>();
975    let reduce_input = ScalarReducePipeIn {
976        valid: sha_result.valid && (sha_kind == b2(HASH_NONCE) || sha_kind == b2(HASH_CHALLENGE1)),
977        wide: digest_words(sha_result.state),
978        tag: sha_result.tag.resize::<16>(),
979    };
980    if sha_result.valid && sha_kind == b2(HASH_SEED) {
981        n.key_secret = secret_from_digest(sha_result.state);
982        n.key_prefix = prefix_from_digest(sha_result.state);
983        n.key_point_pending = true;
984    } else if sha_result.valid && sha_kind == b2(HASH_CHALLENGE0) {
985        chain_write = RamWrite::<Words512, 7> {
986            addr: sha_context,
987            value: Words512 {
988                words: sha_result.state,
989            },
990            enable: true,
991        };
992        n.states[sha_context] = b4(SIGN_CHALLENGE1_HASH);
993    }
994
995    if q.hash_reducer.valid {
996        let kind = (q.hash_reducer.tag >> 7).resize::<2>();
997        let context = q.hash_reducer.tag.resize::<7>();
998        if kind == b2(HASH_NONCE) {
999            nonce_write = RamWrite::<ScalarWords, 7> {
1000                addr: context,
1001                value: q.hash_reducer.scalar,
1002                enable: true,
1003            };
1004            n.states[context] = b4(SIGN_POINT);
1005        } else {
1006            challenge_write = RamWrite::<ScalarWords, 7> {
1007                addr: context,
1008                value: q.hash_reducer.scalar,
1009                enable: true,
1010            };
1011            n.states[context] = b4(SIGN_MULADD);
1012        }
1013    }
1014
1015    let point_accept = r.point_issue.valid && q.point.ready;
1016    if point_accept {
1017        n.point_issue.valid = false;
1018    }
1019    let point_issue_available = !r.point_issue.valid || point_accept;
1020    if r.point_fetch.valid && point_issue_available {
1021        n.point_issue = PointIssue {
1022            valid: true,
1023            is_key: r.point_fetch.is_key,
1024            context: r.point_fetch.context,
1025            scalar: if r.point_fetch.is_key {
1026                r.key_secret
1027            } else {
1028                q.memory.nonce_point
1029            },
1030        };
1031        n.point_fetch.valid = false;
1032    }
1033    let point_fetch_available = !r.point_fetch.valid || point_issue_available;
1034    let mut point_found = false;
1035    let mut point_context = b7(0);
1036    let mut point_is_key = false;
1037    if point_fetch_available {
1038        if r.key_point_pending {
1039            point_found = true;
1040            point_is_key = true;
1041            n.key_point_pending = false;
1042        } else {
1043            for offset in 0..8 {
1044                let context = scan_context(r.point_rr, b8(offset as u128));
1045                if !point_found && r.states[context] == b4(SIGN_POINT) {
1046                    point_found = true;
1047                    point_context = context;
1048                    n.states[context] = b4(SIGN_POINT_WAIT);
1049                }
1050            }
1051            n.point_rr = scan_context(r.point_rr, b8(8));
1052        }
1053        if point_found {
1054            n.point_fetch = PointFetch {
1055                valid: true,
1056                is_key: point_is_key,
1057                context: point_context,
1058            };
1059        }
1060    }
1061    let point_input = MultiCombMulIn {
1062        valid: r.point_issue.valid,
1063        scalar: r.point_issue.scalar,
1064        tag: (if r.point_issue.is_key {
1065            b16(1) << 15
1066        } else {
1067            b16(0)
1068        }) | r.point_issue.context.resize::<16>(),
1069    };
1070
1071    let point_is_key_result = q.point.valid && (q.point.tag & (b16(1) << 15)) != b16(0);
1072    let point_push = q.point.valid && !point_is_key_result;
1073    if point_push {
1074        let context = q.point.tag.resize::<7>();
1075        n.states[context] = b4(SIGN_COMPRESS_WAIT);
1076    }
1077    let mut point_work_pending = r.point_fetch.valid || r.point_issue.valid || point_push;
1078    for context in 0..128 {
1079        if r.states[context] == b4(SIGN_POINT) || r.states[context] == b4(SIGN_POINT_WAIT) {
1080            point_work_pending = true;
1081        }
1082    }
1083    if q.point.occupancy != b5(0) {
1084        point_work_pending = true;
1085    }
1086    let single_tail = q.point_fifo.valid && q.point_fifo.count == b6(1) && !point_work_pending;
1087    let pair_ready =
1088        q.point_fifo.valid && q.compressor.ready && (q.point_fifo.second_valid || single_tail);
1089    let key_compress = point_is_key_result && q.compressor.ready;
1090    if point_is_key_result && !q.compressor.ready {
1091        n.internal_error = true;
1092    }
1093    let compressor_input = PointCompressPairIn {
1094        valid: key_compress || pair_ready,
1095        first: if key_compress {
1096            q.point.point
1097        } else {
1098            q.point_fifo.first.point
1099        },
1100        second: if key_compress || single_tail {
1101            identity
1102        } else {
1103            q.point_fifo.second.point
1104        },
1105        tag: if key_compress {
1106            b16(1) << 15
1107        } else {
1108            q.point_fifo.first.context.resize::<16>()
1109                | (q.point_fifo.second.context.resize::<16>() << 7)
1110                | if single_tail { b16(1) << 14 } else { b16(0) }
1111        },
1112    };
1113
1114    if q.compressor.valid {
1115        if (q.compressor.tag & (b16(1) << 15)) != b16(0) {
1116            n.public_key = compressed_as_scalar(q.compressor.first);
1117            n.key_loaded = true;
1118            n.key_busy = false;
1119        } else {
1120            let single = (q.compressor.tag & (b16(1) << 14)) != b16(0);
1121            let first_context = q.compressor.tag.resize::<7>();
1122            let second_context = (q.compressor.tag >> 7).resize::<7>();
1123            r_write0 = EncodedRWrite {
1124                enable: true,
1125                context: first_context,
1126                value: q.compressor.first,
1127            };
1128            r_write1 = EncodedRWrite {
1129                enable: !single,
1130                context: second_context,
1131                value: q.compressor.second,
1132            };
1133            n.states[first_context] = b4(SIGN_CHALLENGE0_HASH);
1134            if !single {
1135                n.states[second_context] = b4(SIGN_CHALLENGE0_HASH);
1136            }
1137        }
1138    }
1139
1140    let mut mul_found = false;
1141    let mut mul_context = b7(0);
1142    if !r.mul_fetch.valid {
1143        for offset in 0..8 {
1144            let context = scan_context(r.mul_rr, b8(offset as u128));
1145            if !mul_found && r.states[context] == b4(SIGN_MULADD) {
1146                mul_found = true;
1147                mul_context = context;
1148                n.states[context] = b4(SIGN_MULADD_WAIT);
1149            }
1150        }
1151        n.mul_rr = scan_context(r.mul_rr, b8(8));
1152        if mul_found {
1153            n.mul_fetch = MulFetch {
1154                valid: true,
1155                context: mul_context,
1156            };
1157        }
1158    }
1159    let muladd_input = ScalarWideMulIn {
1160        valid: r.mul_fetch.valid,
1161        x: q.memory.challenge,
1162        y: r.key_secret,
1163        addend: q.memory.nonce_mul,
1164        tag: r.mul_fetch.context.resize::<16>(),
1165    };
1166    if r.mul_fetch.valid {
1167        n.mul_fetch.valid = false;
1168    }
1169
1170    let scalar_result_pending = q.mul_reducer.valid;
1171    if scalar_result_pending {
1172        n.output_pending = OutputPending {
1173            valid: true,
1174            context: q.mul_reducer.tag.resize::<7>(),
1175            scalar: q.mul_reducer.scalar,
1176        };
1177    }
1178    let signature_retire = r.output_pending.valid;
1179    if signature_retire {
1180        let context = r.output_pending.context;
1181        output.valid = true;
1182        output.tag = r.tags[context];
1183        output.signature = Words512 {
1184            words: [
1185                q.encoded_r.result.words[0],
1186                q.encoded_r.result.words[1],
1187                q.encoded_r.result.words[2],
1188                q.encoded_r.result.words[3],
1189                r.output_pending.scalar.words[0],
1190                r.output_pending.scalar.words[1],
1191                r.output_pending.scalar.words[2],
1192                r.output_pending.scalar.words[3],
1193            ],
1194        };
1195        if !scalar_result_pending {
1196            n.output_pending.valid = false;
1197        }
1198        n.states[context] = b4(SIGN_FREE);
1199        n.free_fifo[r.free_write] = context;
1200        n.free_write = next_context(r.free_write);
1201        n.free_count = n.free_count + b8(1);
1202    }
1203
1204    let hash_read_context = if hash_found {
1205        hash_context
1206    } else {
1207        r.hash_fetch.context
1208    };
1209    let scalar_read_context = if mul_found {
1210        mul_context
1211    } else {
1212        r.mul_fetch.context
1213    };
1214    let point_read_context = if point_found {
1215        point_context
1216    } else {
1217        r.point_fetch.context
1218    };
1219    let result_read_context = if scalar_result_pending {
1220        q.mul_reducer.tag.resize::<7>()
1221    } else {
1222        r.output_pending.context
1223    };
1224    let mut d = CachedSignCoreD::dont_care();
1225    d.control = n;
1226    d.memory = SignMemoryIn {
1227        message: RamIn::<Words512, 7> {
1228            read_addr: hash_read_context,
1229            write: message_write,
1230        },
1231        chain: RamIn::<Words512, 7> {
1232            read_addr: hash_read_context,
1233            write: chain_write,
1234        },
1235        nonce_point: RamIn::<ScalarWords, 7> {
1236            read_addr: point_read_context,
1237            write: nonce_write,
1238        },
1239        nonce_mul: RamIn::<ScalarWords, 7> {
1240            read_addr: scalar_read_context,
1241            write: nonce_write,
1242        },
1243        challenge: RamIn::<ScalarWords, 7> {
1244            read_addr: scalar_read_context,
1245            write: challenge_write,
1246        },
1247    };
1248    d.encoded_r = EncodedRStoreIn {
1249        read_hash: hash_read_context,
1250        read_result: result_read_context,
1251        write0: r_write0,
1252        write1: r_write1,
1253    };
1254    d.sha = Sha512PoolIn {
1255        request0: Sha512PoolRequest {
1256            valid: r.hash_issue.valid,
1257            tag: r.hash_issue.tag,
1258            state: r.hash_issue.state,
1259            block: r.hash_issue.block,
1260        },
1261        request1: Sha512PoolRequest {
1262            valid: false,
1263            tag: b9(0),
1264            state: [b64(0); 8],
1265            block: [b64(0); 16],
1266        },
1267    };
1268    d.hash_reducer = reduce_input;
1269    d.point = point_input;
1270    d.point_fifo = PointPairFifoIn {
1271        push: point_push,
1272        value: TaggedPoint {
1273            context: q.point.tag.resize::<7>(),
1274            point: q.point.point,
1275        },
1276        pop: pair_ready,
1277    };
1278    d.compressor = compressor_input;
1279    d.mul_multiplier = muladd_input;
1280    d.mul_add = SignMulStage {
1281        valid: q.mul_multiplier.valid,
1282        wide: scalar_product_addend_kernel(q.mul_multiplier.product, q.mul_multiplier.addend),
1283        tag: q.mul_multiplier.tag,
1284    };
1285    d.mul_reducer = ScalarReducePipeIn {
1286        valid: q.mul_add.valid,
1287        wide: q.mul_add.wide,
1288        tag: q.mul_add.tag,
1289    };
1290    (output, d)
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295    use super::*;
1296    use crate::simulation::CycleSim;
1297    use ed25519_dalek::{Signer, SigningKey};
1298
1299    #[test]
1300    fn compressed_point_reads_are_registered() {
1301        let zero = CompressedPoint::zero();
1302        let mut i = EncodedRStoreIn {
1303            read_hash: b7(0),
1304            read_result: b7(1),
1305            write0: EncodedRWrite {
1306                enable: false,
1307                context: b7(0),
1308                value: zero,
1309            },
1310            write1: EncodedRWrite {
1311                enable: false,
1312                context: b7(0),
1313                value: zero,
1314            },
1315        };
1316        let mut sim = CycleSim::new(EncodedRStore::default());
1317        i.write0 = EncodedRWrite {
1318            enable: true,
1319            context: b7(0),
1320            value: CompressedPoint {
1321                words: [b64(11); 4],
1322            },
1323        };
1324        i.write1 = EncodedRWrite {
1325            enable: true,
1326            context: b7(1),
1327            value: CompressedPoint {
1328                words: [b64(22); 4],
1329            },
1330        };
1331        sim.tick(i);
1332        i.write0.enable = false;
1333        i.write1.enable = false;
1334        let output = sim.tick(i);
1335        assert_eq!(output.hash.words, [b64(11); 4]);
1336        assert_eq!(output.result.words, [b64(22); 4]);
1337        // A combinational address change must not alter output before an edge.
1338        i.read_hash = b7(1);
1339        i.read_result = b7(0);
1340        let q = EncodedRStoreQ {
1341            read_data: [output.hash, output.result],
1342            bank0: [zero; 16],
1343            bank1: [zero; 16],
1344            bank2: [zero; 16],
1345            bank3: [zero; 16],
1346            bank4: [zero; 16],
1347            bank5: [zero; 16],
1348            bank6: [zero; 16],
1349            bank7: [zero; 16],
1350        };
1351        let (held, _) = encoded_r_store_kernel(ClockReset::dont_care(), i, q);
1352        assert_eq!(held, output);
1353        let output = sim.tick(i);
1354        assert_eq!(output.hash.words, [b64(22); 4]);
1355        assert_eq!(output.result.words, [b64(11); 4]);
1356    }
1357
1358    #[test]
1359    fn all_128_compressed_point_contexts_are_distinct() {
1360        let mut sim = CycleSim::new(EncodedRStore::default());
1361        let mut input = EncodedRStoreIn::dont_care();
1362        for index in 0..64 {
1363            input.write0 = EncodedRWrite {
1364                enable: true,
1365                context: b7(index),
1366                value: CompressedPoint {
1367                    words: [b64(index + 1); 4],
1368                },
1369            };
1370            input.write1 = EncodedRWrite {
1371                enable: true,
1372                context: b7(index + 64),
1373                value: CompressedPoint {
1374                    words: [b64(index + 65); 4],
1375                },
1376            };
1377            sim.tick(input);
1378        }
1379        input.write0.enable = false;
1380        input.write1.enable = false;
1381        for index in 0..128 {
1382            input.read_hash = b7(index);
1383            input.read_result = b7(127 - index);
1384            let output = sim.tick(input);
1385            assert_eq!(output.hash.words, [b64(index + 1); 4]);
1386            assert_eq!(output.result.words, [b64(128 - index); 4]);
1387        }
1388    }
1389
1390    #[test]
1391    fn point_pairs_never_read_a_just_written_bram_word() {
1392        let mut sim = CycleSim::new(PointPairFifo::default());
1393        let empty = TaggedPoint {
1394            context: b7(0),
1395            point: ExtendedPoint::identity(),
1396        };
1397        let mut output = sim.tick(PointPairFifoIn {
1398            push: false,
1399            value: empty,
1400            pop: false,
1401        });
1402        let mut seen = Vec::new();
1403        for cycle in 0..100 {
1404            let pop = output.valid && output.second_valid;
1405            if pop {
1406                seen.push(output.first.context.raw());
1407                seen.push(output.second.context.raw());
1408                assert_eq!(
1409                    output.first.point.x.limbs[0].raw(),
1410                    output.first.context.raw() + 1
1411                );
1412                assert_eq!(
1413                    output.second.point.x.limbs[0].raw(),
1414                    output.second.context.raw() + 1
1415                );
1416            }
1417            let mut value = empty;
1418            value.context = b7(cycle % 64);
1419            value.point.x.limbs[0] = b51(cycle + 1);
1420            output = sim.tick(PointPairFifoIn {
1421                push: cycle < 20,
1422                value,
1423                pop,
1424            });
1425            assert!(!output.overflow);
1426        }
1427        assert_eq!(seen, (0..20).collect::<Vec<_>>());
1428    }
1429
1430    fn scalar_words(bytes: [u8; 32]) -> ScalarWords {
1431        ScalarWords {
1432            words: std::array::from_fn(|index| {
1433                b64(u64::from_le_bytes(bytes[index * 8..index * 8 + 8].try_into().unwrap()) as u128)
1434            }),
1435        }
1436    }
1437
1438    fn words512(bytes: [u8; 64]) -> Words512 {
1439        Words512 {
1440            words: std::array::from_fn(|index| {
1441                b64(u64::from_le_bytes(bytes[index * 8..index * 8 + 8].try_into().unwrap()) as u128)
1442            }),
1443        }
1444    }
1445
1446    fn bytes512(words: Words512) -> [u8; 64] {
1447        let mut bytes = [0u8; 64];
1448        for (index, word) in words.words.iter().enumerate() {
1449            bytes[index * 8..index * 8 + 8].copy_from_slice(&(word.raw() as u64).to_le_bytes());
1450        }
1451        bytes
1452    }
1453
1454    fn idle() -> CachedSignIn {
1455        CachedSignIn {
1456            load_key_valid: false,
1457            key_seed: ScalarWords { words: [b64(0); 4] },
1458            clear_key: false,
1459            valid: false,
1460            request: SignRequest {
1461                message: Words512 { words: [b64(0); 8] },
1462                tag: b16(0),
1463            },
1464        }
1465    }
1466
1467    #[test]
1468    fn cached_signing_matches_dalek() {
1469        std::thread::Builder::new()
1470            .name("cached-sign-sim".into())
1471            .stack_size(256 * 1024 * 1024)
1472            .spawn(cached_signing_body)
1473            .unwrap()
1474            .join()
1475            .unwrap();
1476    }
1477
1478    fn cached_signing_body() {
1479        let seed = std::array::from_fn(|index| (index as u8).wrapping_mul(23));
1480        let message = std::array::from_fn(|index| (index as u8).wrapping_mul(31).rotate_left(1));
1481        let key = SigningKey::from_bytes(&seed);
1482        let expected_key = key.verifying_key().to_bytes();
1483        let expected_signature = key.sign(&message).to_bytes();
1484        let mut load_sent = false;
1485        let mut sign_sent = false;
1486        let mut sim = CycleSim::new(CachedSignCore::default());
1487        let mut output = sim.tick(idle());
1488        for _ in 0..40_000 {
1489            let mut input = idle();
1490            if !load_sent && output.load_key_ready {
1491                input.load_key_valid = true;
1492                input.key_seed = scalar_words(seed);
1493                load_sent = true;
1494            } else if !sign_sent && output.key_loaded && output.ready {
1495                assert_eq!(output.public_key, scalar_words(expected_key));
1496                input.valid = true;
1497                input.request = SignRequest {
1498                    message: words512(message),
1499                    tag: b16(0x51a7),
1500                };
1501                sign_sent = true;
1502            }
1503            output = sim.tick(input);
1504            assert!(!output.internal_error);
1505            if output.valid {
1506                assert_eq!(output.tag, b16(0x51a7));
1507                assert_eq!(bytes512(output.signature), expected_signature);
1508                println!("sign completion at cycle {}", sim.cycles());
1509                return;
1510            }
1511        }
1512        panic!(
1513            "signer timeout: loaded={} occupancy={}",
1514            output.key_loaded,
1515            output.occupancy.raw()
1516        );
1517    }
1518
1519    #[test]
1520    #[ignore = "sustained experiment: run with --profile sim under tools/bounded.sh"]
1521    fn sustained_signing_matches_dalek() {
1522        std::thread::Builder::new().stack_size(256 * 1024 * 1024)
1523            .spawn(|| {
1524                let count = std::env::var("ED25519_BENCH_COUNT").ok().and_then(|s|s.parse::<usize>().ok()).unwrap_or(384);
1525                assert!((384..=4096).contains(&count));
1526                let seed=[0x51;32];
1527                let key=SigningKey::from_bytes(&seed);
1528                let messages:Vec<[u8;64]>=(0..count).map(|tag| {
1529                    let mut message=std::array::from_fn(|i|(tag as u8).wrapping_add((i as u8).wrapping_mul(17)));
1530                    message[..4].copy_from_slice(&(tag as u32).to_le_bytes());
1531                    message
1532                }).collect();
1533                let expected:Vec<_>=messages.iter().map(|m|key.sign(m).to_bytes()).collect();
1534                let mut seen=vec![false;count];
1535                let mut sim=CycleSim::new(CachedSignCore::default());
1536                let mut output=sim.tick(idle());
1537                let mut loaded=false; let mut accepted=0; let mut completed=0;
1538                let mut first=0; let mut last=0;
1539                let mut full_context_cycles=0; let mut loaded_cycles=0;
1540                let mut accepted_at=Vec::new(); let mut completed_at=Vec::new();
1541                for _ in 0..count*1000+30_000 {
1542                    let mut input=idle();
1543                    if !loaded && output.load_key_ready { input.load_key_valid=true;input.key_seed=scalar_words(seed);loaded=true; }
1544                    if output.ready && accepted<count {
1545                        input.valid=true;input.request=SignRequest{message:words512(messages[accepted]),tag:b16(accepted as u128)};
1546                        accepted+=1; accepted_at.push(sim.cycles()+1);
1547                    }
1548                    output=sim.tick(input);
1549                    if output.key_loaded && accepted<count { loaded_cycles+=1; if !output.ready {full_context_cycles+=1;} }
1550                    assert!(!output.internal_error,"internal error at {}",sim.cycles());
1551                    if output.valid {
1552                        let tag=output.tag.raw() as usize;
1553                        assert!(tag<count && !seen[tag],"unexpected/duplicate tag {tag}");
1554                        assert_eq!(bytes512(output.signature),expected[tag],"signature {tag}");
1555                        seen[tag]=true;completed+=1;completed_at.push(sim.cycles());
1556                        if completed==128 { first=sim.cycles(); }
1557                        if completed==count-128 { last=sim.cycles(); }
1558                        if completed%64==0 { println!("completed {completed}/{count} at cycle {}",sim.cycles()); }
1559                        if completed==count { break; }
1560                    }
1561                }
1562                assert_eq!((accepted,completed),(count,count));
1563                let interval=(last-first) as f64/(count-256) as f64;
1564                let topology=if cfg!(feature="wide-sign") {"wide"} else {"compact"};
1565                println!("topology={topology} cycles/sign={interval:.3} projection_at_205mhz={:.0} sig/s (NOT measured hardware clock)",205e6/interval);
1566                std::fs::create_dir_all("reports/bench").unwrap();
1567                std::fs::write(format!("reports/bench/native-{topology}.json"),format!(
1568                    "{{\"topology\":\"{topology}\",\"contexts\":128,\"accepted\":{accepted},\"completed\":{completed},\"warmup_completions\":128,\"drain_excluded\":128,\"interval_start\":{first},\"interval_end\":{last},\"sign_ii\":{interval},\"assumed_mhz\":205,\"achieved_mhz\":null,\"all_signatures_match_dalek\":true,\"loaded_offer_cycles\":{loaded_cycles},\"full_context_cycles\":{full_context_cycles},\"accept_cycles\":{accepted_at:?},\"completion_cycles\":{completed_at:?}}}\n"
1569                )).unwrap();
1570            }).unwrap().join().unwrap();
1571    }
1572
1573    #[test]
1574    #[ignore = "explicit frontend gate for the 128-context scheduler"]
1575    fn signing_control_compiles() -> miette::Result<()> {
1576        rhdl::core::compile_design::<cached_sign_kernel>(rhdl::core::CompilationMode::Synchronous)?;
1577        rhdl::core::compile_design::<encoded_r_store_kernel>(
1578            rhdl::core::CompilationMode::Synchronous,
1579        )?;
1580        Ok(())
1581    }
1582
1583    #[test]
1584    #[ignore = "large descriptor: use bounded explicit export after leaf tests"]
1585    fn compiles_to_native_rhdl_netlist() -> miette::Result<()> {
1586        unsafe { std::env::set_var("RHDL_SKIP_IVERILOG_CHECK", "1") };
1587        CachedSignCore::default()
1588            .descriptor(ScopedName::top())?
1589            .hdl()?;
1590        Ok(())
1591    }
1592}