1use 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#[derive(Copy, Clone, Debug, PartialEq, Digital)]
69pub struct CachedSignIn {
70 pub load_key_valid: bool,
72 pub key_seed: ScalarWords,
74 pub clear_key: bool,
76 pub valid: bool,
78 pub request: SignRequest,
80}
81
82#[derive(Copy, Clone, Debug, PartialEq, Digital)]
84pub struct CachedSignOut {
85 pub load_key_ready: bool,
87 pub key_loaded: bool,
89 pub public_key: ScalarWords,
91 pub ready: bool,
93 pub valid: bool,
95 pub signature: Words512,
97 pub tag: b16,
99 pub occupancy: b8,
101 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#[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#[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 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#[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 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#[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#[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 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}