Skip to main content

rhdl_ed25519_fast_core/
variable_base.rs

1//! Constant-pattern variable-base scalar multiplication.
2//!
3//! [`VariableBaseMul`] computes `[scalar]P` for a decoded affine point.  Every
4//! one of the 256 scalar bits executes one complete Edwards doubling and one
5//! complete mixed addition; a final multiplexer selects the bit result.  The
6//! operation count and memory access pattern therefore do not reveal scalar
7//! bits.
8//!
9//! Four radix-`2^51` multipliers execute each formula round together.  Twenty
10//! independent contexts hide their 17-cycle latency, allowing one four-product
11//! round to issue each clock.  A lane consumes 1,024 issue clocks per scalar;
12//! the strict verifier uses two lanes for a sustained request interval near 512
13//! clocks.
14
15use 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/// Internal formula phase for one scalar context.
33#[derive(Copy, Clone, Debug, Default, PartialEq, Digital)]
34#[doc(hidden)]
35pub enum VariablePhase {
36    /// First four products of a doubling.
37    #[default]
38    DoubleFirst,
39    /// Four coordinate products completing a doubling.
40    DoubleSecond,
41    /// First three products of a mixed addition.
42    AddFirst,
43    /// Four coordinate products completing a mixed addition.
44    AddSecond,
45}
46
47/// Tagged variable-base multiplication request.
48#[derive(Copy, Clone, Debug, PartialEq, Digital)]
49pub struct VariableBaseIn {
50    /// Marks this request valid when [`VariableBaseOut::ready`] is high.
51    pub valid: bool,
52    /// Decoded affine point `(X, Y, 1, X*Y)`.
53    pub point: ExtendedPoint,
54    /// Precomputed affine `2*d*T` returned by the point decoder.
55    pub t2d: FieldElement51,
56    /// Canonical scalar in little-endian words.
57    pub scalar: ScalarWords,
58    /// Caller-defined transaction tag.
59    pub tag: b16,
60}
61
62/// Result and flow control from [`VariableBaseMul`].
63#[derive(Copy, Clone, Debug, PartialEq, Digital)]
64pub struct VariableBaseOut {
65    /// High when at least one context is available.
66    pub ready: bool,
67    /// High for one cycle when [`Self::point`] is valid.
68    pub valid: bool,
69    /// Projective extended result `[scalar]P`.
70    pub point: ExtendedPoint,
71    /// Delayed caller tag.
72    pub tag: b16,
73    /// Number of occupied scalar contexts.
74    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/// Twenty-context, four-multiplier variable-point scalar engine.
132#[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/// State transition for [`VariableBaseMul`].
216#[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}