1use rhdl::prelude::*;
16use rhdl_fpga::core::dff::DFF;
17
18use crate::{
19 field_parallel::{
20 FieldElement51, FieldMulIn, FieldMulPipe, field_add_kernel, field_neg_kernel,
21 field_sub_kernel,
22 },
23 multicomb::ExtendedPoint,
24 scalar::ScalarWords,
25};
26
27const CONTEXTS: usize = 20;
28const FREE: u128 = 0;
29const READY: u128 = 1;
30const WAIT: u128 = 2;
31
32#[derive(Copy, Clone, Debug, Default, PartialEq, Digital)]
34#[doc(hidden)]
35pub enum VariablePhase {
36 #[default]
38 DoubleFirst,
39 DoubleSecond,
41 AddFirst,
43 AddSecond,
45}
46
47#[derive(Copy, Clone, Debug, PartialEq, Digital)]
49pub struct VariableBaseIn {
50 pub valid: bool,
52 pub point: ExtendedPoint,
54 pub t2d: FieldElement51,
56 pub scalar: ScalarWords,
58 pub tag: b16,
60}
61
62#[derive(Copy, Clone, Debug, PartialEq, Digital)]
64pub struct VariableBaseOut {
65 pub ready: bool,
67 pub valid: bool,
69 pub point: ExtendedPoint,
71 pub tag: b16,
73 pub occupancy: b5,
75}
76
77#[derive(Copy, Clone, Debug, PartialEq, Digital)]
78#[doc(hidden)]
79pub struct VariableContext {
80 state: b2,
81 phase: VariablePhase,
82 bit: b8,
83 scalar: ScalarWords,
84 tag: b16,
85 base: ExtendedPoint,
86 base_t2d: FieldElement51,
87 accumulator: ExtendedPoint,
88 e: FieldElement51,
89 f: FieldElement51,
90 g: FieldElement51,
91 h: FieldElement51,
92}
93
94fn empty_context() -> VariableContext {
95 let zero = FieldElement51::zero();
96 let identity = ExtendedPoint::identity();
97 VariableContext {
98 state: b2(FREE),
99 phase: VariablePhase::DoubleFirst,
100 bit: b8(0),
101 scalar: ScalarWords { words: [b64(0); 4] },
102 tag: b16(0),
103 base: identity,
104 base_t2d: zero,
105 accumulator: identity,
106 e: zero,
107 f: zero,
108 g: zero,
109 h: zero,
110 }
111}
112
113#[derive(Copy, Clone, Debug, PartialEq, Digital)]
114#[doc(hidden)]
115pub struct VariableControl {
116 contexts: [VariableContext; CONTEXTS],
117 issue_valid: [bool; 17],
118 issue_context: [b5; 17],
119 round_robin: b5,
120}
121
122fn reset_control() -> VariableControl {
123 VariableControl {
124 contexts: [empty_context(); CONTEXTS],
125 issue_valid: [false; 17],
126 issue_context: [b5(0); 17],
127 round_robin: b5(0),
128 }
129}
130
131#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
133pub struct VariableBaseMul {
134 control: DFF<VariableControl>,
135 mul0: FieldMulPipe,
136 mul1: FieldMulPipe,
137 mul2: FieldMulPipe,
138 mul3: FieldMulPipe,
139}
140
141impl Default for VariableBaseMul {
142 fn default() -> Self {
143 Self {
144 control: DFF::new(reset_control()),
145 mul0: FieldMulPipe::default(),
146 mul1: FieldMulPipe::default(),
147 mul2: FieldMulPipe::default(),
148 mul3: FieldMulPipe::default(),
149 }
150 }
151}
152
153impl SynchronousIO for VariableBaseMul {
154 type I = VariableBaseIn;
155 type O = VariableBaseOut;
156 type Kernel = variable_base_kernel;
157}
158
159#[kernel]
160fn zero() -> FieldElement51 {
161 FieldElement51 { limbs: [b51(0); 5] }
162}
163
164#[kernel]
165fn one() -> FieldElement51 {
166 FieldElement51 {
167 limbs: [b51(1), b51(0), b51(0), b51(0), b51(0)],
168 }
169}
170
171#[kernel]
172fn identity() -> ExtendedPoint {
173 ExtendedPoint {
174 x: zero(),
175 y: one(),
176 z: one(),
177 t: zero(),
178 }
179}
180
181#[kernel]
182fn empty_context_kernel(_unused: bool) -> VariableContext {
183 VariableContext {
184 state: b2(FREE),
185 phase: VariablePhase::DoubleFirst,
186 bit: b8(0),
187 scalar: ScalarWords { words: [b64(0); 4] },
188 tag: b16(0),
189 base: identity(),
190 base_t2d: zero(),
191 accumulator: identity(),
192 e: zero(),
193 f: zero(),
194 g: zero(),
195 h: zero(),
196 }
197}
198
199#[kernel]
200fn wrap20(value: b5) -> b5 {
201 if value >= b5(20) {
202 value - b5(20)
203 } else {
204 value
205 }
206}
207
208#[kernel]
209fn scalar_bit(scalar: ScalarWords, bit: b8) -> bool {
210 let word = (bit >> 6).resize::<2>();
211 let shift = bit.resize::<6>();
212 ((scalar.words[word] >> shift) & b64(1)) != b64(0)
213}
214
215#[kernel]
217pub fn variable_base_kernel(
218 _cr: ClockReset,
219 i: VariableBaseIn,
220 q: VariableBaseMulQ,
221) -> (VariableBaseOut, VariableBaseMulD) {
222 let r = q.control;
223 let mut n = r;
224 let zero = zero();
225 let mut output = VariableBaseOut {
226 ready: false,
227 valid: false,
228 point: identity(),
229 tag: b16(0),
230 occupancy: b5(0),
231 };
232
233 let mut free_found = false;
234 let mut free_context = b5(0);
235 for index in 0..CONTEXTS {
236 if r.contexts[index].state != b2(FREE) {
237 output.occupancy += b5(1);
238 } else if !free_found {
239 free_found = true;
240 free_context = b5(index as u128);
241 }
242 }
243 output.ready = free_found;
244
245 let mut issue_found = false;
246 let mut issue_context = b5(0);
247 for offset in 0..CONTEXTS {
248 let candidate = wrap20(r.round_robin + b5(offset as u128));
249 if !issue_found && r.contexts[candidate].state == b2(READY) {
250 issue_found = true;
251 issue_context = candidate;
252 }
253 }
254
255 let context = r.contexts[issue_context];
256 let mut a = [zero; 4];
257 let mut b = [zero; 4];
258 if issue_found {
259 match context.phase {
260 VariablePhase::DoubleFirst => {
261 let sum = field_add_kernel(context.accumulator.x, context.accumulator.y);
262 a = [
263 context.accumulator.x,
264 context.accumulator.y,
265 context.accumulator.z,
266 sum,
267 ];
268 b = a;
269 }
270 VariablePhase::AddFirst => {
271 a[0] = field_sub_kernel(context.accumulator.y, context.accumulator.x);
272 b[0] = field_sub_kernel(context.base.y, context.base.x);
273 a[1] = field_add_kernel(context.accumulator.y, context.accumulator.x);
274 b[1] = field_add_kernel(context.base.y, context.base.x);
275 a[2] = context.accumulator.t;
276 b[2] = context.base_t2d;
277 a[3] = zero;
278 b[3] = zero;
279 }
280 _ => {
281 a = [context.e, context.g, context.e, context.f];
282 b = [context.f, context.h, context.h, context.g];
283 }
284 }
285 n.contexts[issue_context].state = b2(WAIT);
286 n.round_robin = wrap20(issue_context + b5(1));
287 }
288
289 n.issue_valid[0] = issue_found;
290 n.issue_context[0] = issue_context;
291 for stage in 1..17 {
292 n.issue_valid[stage] = r.issue_valid[stage - 1];
293 n.issue_context[stage] = r.issue_context[stage - 1];
294 }
295
296 if r.issue_valid[16] && q.mul0.valid && q.mul1.valid && q.mul2.valid && q.mul3.valid {
297 let result_context = r.issue_context[16];
298 let current = r.contexts[result_context];
299 let mut updated = current;
300 updated.state = b2(READY);
301 match current.phase {
302 VariablePhase::DoubleFirst => {
303 let d = field_neg_kernel(q.mul0.result);
304 let c = field_add_kernel(q.mul2.result, q.mul2.result);
305 let g = field_add_kernel(d, q.mul1.result);
306 updated.e = field_sub_kernel(
307 field_sub_kernel(q.mul3.result, q.mul0.result),
308 q.mul1.result,
309 );
310 updated.f = field_sub_kernel(g, c);
311 updated.g = g;
312 updated.h = field_sub_kernel(d, q.mul1.result);
313 updated.phase = VariablePhase::DoubleSecond;
314 }
315 VariablePhase::AddFirst => {
316 let d = field_add_kernel(current.accumulator.z, current.accumulator.z);
317 updated.e = field_sub_kernel(q.mul1.result, q.mul0.result);
318 updated.f = field_sub_kernel(d, q.mul2.result);
319 updated.g = field_add_kernel(d, q.mul2.result);
320 updated.h = field_add_kernel(q.mul1.result, q.mul0.result);
321 updated.phase = VariablePhase::AddSecond;
322 }
323 VariablePhase::DoubleSecond => {
324 updated.accumulator = ExtendedPoint {
325 x: q.mul0.result,
326 y: q.mul1.result,
327 z: q.mul3.result,
328 t: q.mul2.result,
329 };
330 updated.phase = VariablePhase::AddFirst;
331 }
332 VariablePhase::AddSecond => {
333 let added = ExtendedPoint {
334 x: q.mul0.result,
335 y: q.mul1.result,
336 z: q.mul3.result,
337 t: q.mul2.result,
338 };
339 if scalar_bit(current.scalar, current.bit) {
340 updated.accumulator = added;
341 }
342 if current.bit == b8(0) {
343 output.valid = true;
344 output.point = if scalar_bit(current.scalar, current.bit) {
345 added
346 } else {
347 current.accumulator
348 };
349 output.tag = current.tag;
350 updated = empty_context_kernel(false);
351 } else {
352 updated.bit = current.bit - b8(1);
353 updated.phase = VariablePhase::DoubleFirst;
354 }
355 }
356 }
357 n.contexts[result_context] = updated;
358 }
359
360 if i.valid && free_found {
361 n.contexts[free_context] = VariableContext {
362 state: b2(READY),
363 phase: VariablePhase::DoubleFirst,
364 bit: b8(255),
365 scalar: i.scalar,
366 tag: i.tag,
367 base: i.point,
368 base_t2d: i.t2d,
369 accumulator: identity(),
370 e: zero,
371 f: zero,
372 g: zero,
373 h: zero,
374 };
375 }
376
377 let mut d = VariableBaseMulD::dont_care();
378 d.control = n;
379 d.mul0 = FieldMulIn {
380 valid: issue_found,
381 a: a[0],
382 b: b[0],
383 };
384 d.mul1 = FieldMulIn {
385 valid: issue_found,
386 a: a[1],
387 b: b[1],
388 };
389 d.mul2 = FieldMulIn {
390 valid: issue_found,
391 a: a[2],
392 b: b[2],
393 };
394 d.mul3 = FieldMulIn {
395 valid: issue_found,
396 a: a[3],
397 b: b[3],
398 };
399 (output, d)
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use crate::field_parallel::{field_from_words_kernel, field_to_words_kernel};
406 use crate::point_compress::CompressedPoint;
407 use crate::point_decode::{PointDecodeIn, PointDecodePipe};
408 use curve25519_dalek::{
409 constants::ED25519_BASEPOINT_POINT, edwards::CompressedEdwardsY, scalar::Scalar,
410 };
411 use num_bigint::BigUint;
412 use std::iter;
413
414 fn field(words: [u64; 4]) -> FieldElement51 {
415 field_from_words_kernel(words.map(|word| b64(word as u128)))
416 }
417
418 fn basepoint() -> (ExtendedPoint, FieldElement51) {
419 (
420 ExtendedPoint {
421 x: field([
422 0xc9562d608f25d51a,
423 0x692cc7609525a7b2,
424 0xc0a4e231fdd6dc5c,
425 0x216936d3cd6e53fe,
426 ]),
427 y: field([
428 0x6666666666666658,
429 0x6666666666666666,
430 0x6666666666666666,
431 0x6666666666666666,
432 ]),
433 z: FieldElement51::one(),
434 t: field([
435 0x6dde8ab3a5b7dda3,
436 0x20f09f80775152f5,
437 0x66ea4e8e64abe37d,
438 0x67875f0fd78b7665,
439 ]),
440 },
441 field([
442 0xabc91205877aaa68,
443 0x26d9e823ccaac49e,
444 0x5a1b7dcbdd43598c,
445 0x6f117b689f0c65a8,
446 ]),
447 )
448 }
449
450 fn scalar_words(value: Scalar) -> ScalarWords {
451 let bytes = value.to_bytes();
452 ScalarWords {
453 words: std::array::from_fn(|index| {
454 b64(u64::from_le_bytes(bytes[index * 8..index * 8 + 8].try_into().unwrap()) as u128)
455 }),
456 }
457 }
458
459 fn compress(point: ExtendedPoint) -> [u8; 32] {
460 let p = (BigUint::from(1u8) << 255) - BigUint::from(19u8);
461 let value = |field: FieldElement51| {
462 field_to_words_kernel(field)
463 .iter()
464 .enumerate()
465 .fold(BigUint::from(0u8), |sum, (index, word)| {
466 sum + (BigUint::from(word.raw()) << (64 * index))
467 })
468 };
469 let inverse = value(point.z).modpow(&(&p - BigUint::from(2u8)), &p);
470 let x = (value(point.x) * &inverse) % &p;
471 let y = (value(point.y) * inverse) % &p;
472 let mut bytes = y.to_bytes_le();
473 bytes.resize(32, 0);
474 bytes[31] |= ((&x & BigUint::from(1u8))
475 .to_u64_digits()
476 .first()
477 .copied()
478 .unwrap_or(0) as u8)
479 << 7;
480 bytes.try_into().unwrap()
481 }
482
483 #[test]
484 fn twenty_interleaved_scalars_match_dalek() {
485 std::thread::Builder::new()
486 .name("variable-base-sim".into())
487 .stack_size(256 * 1024 * 1024)
488 .spawn(|| {
489 let (point, t2d) = basepoint();
490 let scalars: Vec<_> = (1u64..=20).map(Scalar::from).collect();
491 let requests = scalars
492 .iter()
493 .enumerate()
494 .map(|(index, scalar)| VariableBaseIn {
495 valid: true,
496 point,
497 t2d,
498 scalar: scalar_words(*scalar),
499 tag: b16(index as u128),
500 });
501 let idle = VariableBaseIn {
502 valid: false,
503 point: ExtendedPoint::identity(),
504 t2d: FieldElement51::zero(),
505 scalar: scalar_words(Scalar::ZERO),
506 tag: b16(0),
507 };
508 let outputs: Vec<_> = VariableBaseMul::default()
509 .run(
510 iter::once(idle)
511 .chain(requests)
512 .chain(iter::repeat_n(idle, 24_000))
513 .with_reset(1)
514 .clock_pos_edge(100),
515 )
516 .synchronous_sample()
517 .enumerate()
518 .filter_map(|(cycle, sample)| {
519 sample.output.valid.then_some((cycle, sample.output))
520 })
521 .collect();
522 assert_eq!(outputs.len(), scalars.len());
523 for (_, output) in &outputs {
524 let scalar = scalars[output.tag.raw() as usize];
525 assert_eq!(
526 compress(output.point),
527 (ED25519_BASEPOINT_POINT * scalar).compress().to_bytes()
528 );
529 }
530 let span = outputs.last().unwrap().0 - outputs.first().unwrap().0;
531 let interval = span as f64 / (outputs.len() - 1) as f64;
532 println!("variable-base sustained interval={interval:.3} cycles");
533 assert!(interval <= 1_050.0);
534 })
535 .unwrap()
536 .join()
537 .unwrap();
538 }
539
540 #[test]
541 fn decoded_arbitrary_point_multiplies_like_dalek() {
542 let source_scalar = Scalar::from(0x1234_5678_u64);
543 let source_point = ED25519_BASEPOINT_POINT * source_scalar;
544 let encoded = source_point.compress().to_bytes();
545 let decode_idle = PointDecodeIn {
546 valid: false,
547 encoded: CompressedPoint::zero(),
548 tag: b16(0),
549 };
550 let decode_request = PointDecodeIn {
551 valid: true,
552 encoded: CompressedPoint {
553 words: std::array::from_fn(|index| {
554 b64(
555 u64::from_le_bytes(encoded[index * 8..index * 8 + 8].try_into().unwrap())
556 as u128,
557 )
558 }),
559 },
560 tag: b16(9),
561 };
562 let decoded = PointDecodePipe::default()
563 .run(
564 iter::once(decode_idle)
565 .chain(iter::once(decode_request))
566 .chain(iter::repeat_n(decode_idle, 12_000))
567 .with_reset(1)
568 .clock_pos_edge(100),
569 )
570 .synchronous_sample()
571 .map(|sample| sample.output)
572 .find(|output| output.valid)
573 .unwrap();
574 assert!(decoded.point_valid);
575
576 let scalar = Scalar::from(0xfeed_beef_u64);
577 let request = VariableBaseIn {
578 valid: true,
579 point: decoded.point,
580 t2d: decoded.t2d,
581 scalar: scalar_words(scalar),
582 tag: b16(10),
583 };
584 let idle = VariableBaseIn {
585 valid: false,
586 ..request
587 };
588 let output = VariableBaseMul::default()
589 .run(
590 iter::once(idle)
591 .chain(iter::once(request))
592 .chain(iter::repeat_n(idle, 20_000))
593 .with_reset(1)
594 .clock_pos_edge(100),
595 )
596 .synchronous_sample()
597 .map(|sample| sample.output)
598 .find(|output| output.valid)
599 .unwrap();
600 assert_eq!(
601 CompressedEdwardsY(compress(output.point)),
602 (source_point * scalar).compress()
603 );
604 }
605
606 #[test]
607 fn compiles_to_native_rhdl_netlist() -> miette::Result<()> {
608 unsafe { std::env::set_var("RHDL_SKIP_IVERILOG_CHECK", "1") };
609 VariableBaseMul::default()
610 .descriptor(ScopedName::top())?
611 .hdl()?;
612 Ok(())
613 }
614}