Skip to main content

rhdl_ed25519_fast_core/
point_decode.rs

1//! Constant-pattern compressed Edwards-point decoding.
2//!
3//! Ed25519 verification begins by recovering the affine `X` coordinate from a
4//! compressed Edwards `Y` coordinate.  [`PointDecodePipe`] runs that square-root
5//! calculation entirely in hardware.  Twenty request contexts share one
6//! initiation-interval-one radix-`2^51` multiplier; while one context waits for
7//! its 17-cycle product, another context issues the next operation.
8//!
9//! The exponentiation schedule is deliberately simple: a fixed binary
10//! square-and-multiply implementation of `(p - 5) / 8`.  Its 516 field products
11//! per point are independent of input data.  A verifier can instantiate one
12//! lane for public keys and one for signature `R` values, accepting a pair every
13//! 516 clocks without external memory.
14
15use rhdl::prelude::*;
16use rhdl_fpga::core::dff::DFF;
17
18use crate::{
19    field_parallel::{
20        FieldElement51, FieldMulIn, FieldMulPipe, field_add_kernel, field_eq_kernel,
21        field_from_words_kernel, field_neg_kernel, field_sub_kernel, field_to_words_kernel,
22    },
23    multicomb::ExtendedPoint,
24    point_compress::CompressedPoint,
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)]
33#[doc(hidden)]
34pub enum DecodeStep {
35    #[default]
36    Y2,
37    Dy2,
38    V2,
39    V3,
40    V6,
41    V7,
42    Uv3,
43    Uv7,
44    ExpSquare,
45    ExpMul,
46    Root,
47    Root2,
48    Check,
49    Correct,
50    T,
51    T2d,
52}
53
54/// One compressed point and its caller-selected transaction tag.
55#[derive(Copy, Clone, Debug, PartialEq, Digital)]
56pub struct PointDecodeIn {
57    /// Marks this request valid when [`PointDecodeOut::ready`] is high.
58    pub valid: bool,
59    /// Compressed Edwards-Y encoding.
60    pub encoded: CompressedPoint,
61    /// Tag returned unchanged with the result.
62    pub tag: b16,
63}
64
65/// Result and flow control from [`PointDecodePipe`].
66#[derive(Copy, Clone, Debug, PartialEq, Digital)]
67pub struct PointDecodeOut {
68    /// High when at least one of the 20 contexts is free.
69    pub ready: bool,
70    /// High for one cycle when the remaining fields are valid.
71    pub valid: bool,
72    /// High only for a canonical encoding of a curve point.
73    pub point_valid: bool,
74    /// High for one of the eight compressed low-order points.
75    pub small_order: bool,
76    /// Recovered extended point `(X, Y, 1, X*Y)`.
77    pub point: ExtendedPoint,
78    /// Affine `2*d*T`, precomputed for mixed additions.
79    pub t2d: FieldElement51,
80    /// Delayed caller tag.
81    pub tag: b16,
82    /// Number of occupied decoder contexts.
83    pub occupancy: b5,
84}
85
86#[derive(Copy, Clone, Debug, PartialEq, Digital)]
87#[doc(hidden)]
88pub struct DecodeContext {
89    state: b2,
90    step: DecodeStep,
91    tag: b16,
92    sign: bool,
93    canonical: bool,
94    small_order: bool,
95    square: bool,
96    flipped: bool,
97    exponent_bit: b8,
98    y: FieldElement51,
99    u: FieldElement51,
100    v: FieldElement51,
101    v2: FieldElement51,
102    v3: FieldElement51,
103    v7: FieldElement51,
104    uv3: FieldElement51,
105    base: FieldElement51,
106    accumulator: FieldElement51,
107    root: FieldElement51,
108    x: FieldElement51,
109    t: FieldElement51,
110}
111
112fn empty_context() -> DecodeContext {
113    let zero = FieldElement51::zero();
114    DecodeContext {
115        state: b2(FREE),
116        step: DecodeStep::Y2,
117        tag: b16(0),
118        sign: false,
119        canonical: false,
120        small_order: false,
121        square: false,
122        flipped: false,
123        exponent_bit: b8(0),
124        y: zero,
125        u: zero,
126        v: zero,
127        v2: zero,
128        v3: zero,
129        v7: zero,
130        uv3: zero,
131        base: zero,
132        accumulator: zero,
133        root: zero,
134        x: zero,
135        t: zero,
136    }
137}
138
139#[derive(Copy, Clone, Debug, PartialEq, Digital)]
140#[doc(hidden)]
141pub struct DecodeControl {
142    contexts: [DecodeContext; CONTEXTS],
143    issue_valid: [bool; 17],
144    issue_context: [b5; 17],
145    round_robin: b5,
146}
147
148fn reset_control() -> DecodeControl {
149    DecodeControl {
150        contexts: [empty_context(); CONTEXTS],
151        issue_valid: [false; 17],
152        issue_context: [b5(0); 17],
153        round_robin: b5(0),
154    }
155}
156
157/// Twenty-context fixed-schedule Edwards point decoder.
158#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
159pub struct PointDecodePipe {
160    control: DFF<DecodeControl>,
161    multiplier: FieldMulPipe,
162}
163
164impl Default for PointDecodePipe {
165    fn default() -> Self {
166        Self {
167            control: DFF::new(reset_control()),
168            multiplier: FieldMulPipe::default(),
169        }
170    }
171}
172
173impl SynchronousIO for PointDecodePipe {
174    type I = PointDecodeIn;
175    type O = PointDecodeOut;
176    type Kernel = point_decode_kernel;
177}
178
179#[kernel]
180fn field_zero() -> FieldElement51 {
181    FieldElement51 { limbs: [b51(0); 5] }
182}
183
184#[kernel]
185fn field_one() -> FieldElement51 {
186    FieldElement51 {
187        limbs: [b51(1), b51(0), b51(0), b51(0), b51(0)],
188    }
189}
190
191#[kernel]
192fn empty_context_kernel(_unused: bool) -> DecodeContext {
193    let zero = field_zero();
194    DecodeContext {
195        state: b2(FREE),
196        step: DecodeStep::Y2,
197        tag: b16(0),
198        sign: false,
199        canonical: false,
200        small_order: false,
201        square: false,
202        flipped: false,
203        exponent_bit: b8(0),
204        y: zero,
205        u: zero,
206        v: zero,
207        v2: zero,
208        v3: zero,
209        v7: zero,
210        uv3: zero,
211        base: zero,
212        accumulator: zero,
213        root: zero,
214        x: zero,
215        t: zero,
216    }
217}
218
219#[kernel]
220fn edwards_d() -> FieldElement51 {
221    field_from_words_kernel([
222        b64(0x75eb4dca135978a3),
223        b64(0x00700a4d4141d8ab),
224        b64(0x8cc740797779e898),
225        b64(0x52036cee2b6ffe73),
226    ])
227}
228
229#[kernel]
230fn sqrt_m1() -> FieldElement51 {
231    field_from_words_kernel([
232        b64(0xc4ee1b274a0ea0b0),
233        b64(0x2f431806ad2fe478),
234        b64(0x2b4d00993dfbd7a7),
235        b64(0x2b8324804fc1df0b),
236    ])
237}
238
239#[kernel]
240fn edwards_d2() -> FieldElement51 {
241    field_from_words_kernel([
242        b64(0xebd69b9426b2f159),
243        b64(0x00e0149a8283b156),
244        b64(0x198e80f2eef3d130),
245        b64(0x2406d9dc56dffce7),
246    ])
247}
248
249#[kernel]
250fn words_equal(a: [b64; 4], b: [b64; 4]) -> bool {
251    a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3]
252}
253
254#[kernel]
255fn canonical_y(words: [b64; 4]) -> bool {
256    let high = words[3] & b64(0x7fff_ffff_ffff_ffff);
257    high < b64(0x7fff_ffff_ffff_ffff)
258        || (high == b64(0x7fff_ffff_ffff_ffff)
259            && words[2] == b64(0xffff_ffff_ffff_ffff)
260            && words[1] == b64(0xffff_ffff_ffff_ffff)
261            && words[0] < b64(0xffff_ffff_ffff_ffed))
262}
263
264#[kernel]
265fn small_order_encoding(words: [b64; 4]) -> bool {
266    words_equal(words, [b64(1), b64(0), b64(0), b64(0)])
267        || words_equal(
268            words,
269            [
270                b64(0x4fd84d3d706a17c7),
271                b64(0x0f67100d760b3cba),
272                b64(0xc6cc392cfa53202a),
273                b64(0x7a03ac9277fdc74e),
274            ],
275        )
276        || words_equal(words, [b64(0), b64(0), b64(0), b64(0x8000_0000_0000_0000)])
277        || words_equal(
278            words,
279            [
280                b64(0xb027b2c28f95e826),
281                b64(0xf098eff289f4c345),
282                b64(0x3933c6d305acdfd5),
283                b64(0x05fc536d880238b1),
284            ],
285        )
286        || words_equal(
287            words,
288            [
289                b64(0xffff_ffff_ffff_ffec),
290                b64(0xffff_ffff_ffff_ffff),
291                b64(0xffff_ffff_ffff_ffff),
292                b64(0x7fff_ffff_ffff_ffff),
293            ],
294        )
295        || words_equal(
296            words,
297            [
298                b64(0xb027b2c28f95e826),
299                b64(0xf098eff289f4c345),
300                b64(0x3933c6d305acdfd5),
301                b64(0x85fc536d880238b1),
302            ],
303        )
304        || words_equal(words, [b64(0), b64(0), b64(0), b64(0)])
305        || words_equal(
306            words,
307            [
308                b64(0x4fd84d3d706a17c7),
309                b64(0x0f67100d760b3cba),
310                b64(0xc6cc392cfa53202a),
311                b64(0xfa03ac9277fdc74e),
312            ],
313        )
314}
315
316#[kernel]
317fn wrap20(value: b5) -> b5 {
318    if value >= b5(20) {
319        value - b5(20)
320    } else {
321        value
322    }
323}
324
325#[kernel]
326fn signed_root(value: FieldElement51, sign: bool) -> FieldElement51 {
327    let odd = (field_to_words_kernel(value)[0] & b64(1)) != b64(0);
328    if odd == sign {
329        value
330    } else {
331        field_neg_kernel(value)
332    }
333}
334
335/// State transition for [`PointDecodePipe`].
336#[kernel]
337pub fn point_decode_kernel(
338    _cr: ClockReset,
339    i: PointDecodeIn,
340    q: PointDecodePipeQ,
341) -> (PointDecodeOut, PointDecodePipeD) {
342    let r = q.control;
343    let mut n = r;
344    let zero = field_zero();
345    let one = field_one();
346    let mut output = PointDecodeOut {
347        ready: false,
348        valid: false,
349        point_valid: false,
350        small_order: false,
351        point: ExtendedPoint {
352            x: zero,
353            y: one,
354            z: one,
355            t: zero,
356        },
357        t2d: zero,
358        tag: b16(0),
359        occupancy: b5(0),
360    };
361
362    let mut free_found = false;
363    let mut free_context = b5(0);
364    for index in 0..CONTEXTS {
365        if r.contexts[index].state != b2(FREE) {
366            output.occupancy += b5(1);
367        } else if !free_found {
368            free_found = true;
369            free_context = b5(index as u128);
370        }
371    }
372    output.ready = free_found;
373
374    let mut issue_found = false;
375    let mut issue_context = b5(0);
376    for offset in 0..CONTEXTS {
377        let candidate = wrap20(r.round_robin + b5(offset as u128));
378        if !issue_found && r.contexts[candidate].state == b2(READY) {
379            issue_found = true;
380            issue_context = candidate;
381        }
382    }
383
384    let context = r.contexts[issue_context];
385    let mut operand_a = zero;
386    let mut operand_b = zero;
387    if issue_found {
388        match context.step {
389            DecodeStep::Y2 => {
390                operand_a = context.y;
391                operand_b = context.y;
392            }
393            DecodeStep::Dy2 => {
394                operand_a = field_add_kernel(context.u, one);
395                operand_b = edwards_d();
396            }
397            DecodeStep::V2 => {
398                operand_a = context.v;
399                operand_b = context.v;
400            }
401            DecodeStep::V3 => {
402                operand_a = context.v2;
403                operand_b = context.v;
404            }
405            DecodeStep::V6 => {
406                operand_a = context.v3;
407                operand_b = context.v3;
408            }
409            DecodeStep::V7 => {
410                operand_a = context.v2;
411                operand_b = context.v;
412            }
413            DecodeStep::Uv3 => {
414                operand_a = context.u;
415                operand_b = context.v3;
416            }
417            DecodeStep::Uv7 => {
418                operand_a = context.u;
419                operand_b = context.v7;
420            }
421            DecodeStep::ExpSquare => {
422                operand_a = context.accumulator;
423                operand_b = context.accumulator;
424            }
425            DecodeStep::ExpMul => {
426                operand_a = context.accumulator;
427                operand_b = context.base;
428            }
429            DecodeStep::Root => {
430                operand_a = context.uv3;
431                operand_b = context.accumulator;
432            }
433            DecodeStep::Root2 => {
434                operand_a = context.root;
435                operand_b = context.root;
436            }
437            DecodeStep::Check => {
438                operand_a = context.v;
439                operand_b = context.accumulator;
440            }
441            DecodeStep::Correct => {
442                operand_a = context.root;
443                operand_b = if context.flipped { sqrt_m1() } else { one };
444            }
445            DecodeStep::T => {
446                operand_a = context.x;
447                operand_b = context.y;
448            }
449            _ => {
450                operand_a = context.t;
451                operand_b = edwards_d2();
452            }
453        }
454        n.contexts[issue_context].state = b2(WAIT);
455        n.round_robin = wrap20(issue_context + b5(1));
456    }
457
458    n.issue_valid[0] = issue_found;
459    n.issue_context[0] = issue_context;
460    for stage in 1..17 {
461        n.issue_valid[stage] = r.issue_valid[stage - 1];
462        n.issue_context[stage] = r.issue_context[stage - 1];
463    }
464
465    if r.issue_valid[16] && q.multiplier.valid {
466        let result_context = r.issue_context[16];
467        let current = r.contexts[result_context];
468        let result = q.multiplier.result;
469        n.contexts[result_context].state = b2(READY);
470        match current.step {
471            DecodeStep::Y2 => {
472                n.contexts[result_context].u = field_sub_kernel(result, one);
473                n.contexts[result_context].step = DecodeStep::Dy2;
474            }
475            DecodeStep::Dy2 => {
476                n.contexts[result_context].v = field_add_kernel(result, one);
477                n.contexts[result_context].step = DecodeStep::V2;
478            }
479            DecodeStep::V2 => {
480                n.contexts[result_context].v2 = result;
481                n.contexts[result_context].step = DecodeStep::V3;
482            }
483            DecodeStep::V3 => {
484                n.contexts[result_context].v3 = result;
485                n.contexts[result_context].step = DecodeStep::V6;
486            }
487            DecodeStep::V6 => {
488                n.contexts[result_context].v2 = result;
489                n.contexts[result_context].step = DecodeStep::V7;
490            }
491            DecodeStep::V7 => {
492                n.contexts[result_context].v7 = result;
493                n.contexts[result_context].step = DecodeStep::Uv3;
494            }
495            DecodeStep::Uv3 => {
496                n.contexts[result_context].uv3 = result;
497                n.contexts[result_context].step = DecodeStep::Uv7;
498            }
499            DecodeStep::Uv7 => {
500                n.contexts[result_context].base = result;
501                n.contexts[result_context].accumulator = one;
502                n.contexts[result_context].exponent_bit = b8(251);
503                n.contexts[result_context].step = DecodeStep::ExpSquare;
504            }
505            DecodeStep::ExpSquare => {
506                n.contexts[result_context].accumulator = result;
507                if current.exponent_bit == b8(1) {
508                    n.contexts[result_context].exponent_bit = b8(0);
509                    n.contexts[result_context].step = DecodeStep::ExpSquare;
510                } else {
511                    n.contexts[result_context].step = DecodeStep::ExpMul;
512                }
513            }
514            DecodeStep::ExpMul => {
515                n.contexts[result_context].accumulator = result;
516                if current.exponent_bit == b8(0) {
517                    n.contexts[result_context].step = DecodeStep::Root;
518                } else {
519                    n.contexts[result_context].exponent_bit = current.exponent_bit - b8(1);
520                    n.contexts[result_context].step = DecodeStep::ExpSquare;
521                }
522            }
523            DecodeStep::Root => {
524                n.contexts[result_context].root = result;
525                n.contexts[result_context].step = DecodeStep::Root2;
526            }
527            DecodeStep::Root2 => {
528                n.contexts[result_context].accumulator = result;
529                n.contexts[result_context].step = DecodeStep::Check;
530            }
531            DecodeStep::Check => {
532                let negative_u = field_neg_kernel(current.u);
533                n.contexts[result_context].square =
534                    field_eq_kernel(result, current.u) || field_eq_kernel(result, negative_u);
535                n.contexts[result_context].flipped = !field_eq_kernel(result, current.u);
536                n.contexts[result_context].step = DecodeStep::Correct;
537            }
538            DecodeStep::Correct => {
539                let x = signed_root(result, current.sign);
540                n.contexts[result_context].x = x;
541                n.contexts[result_context].step = DecodeStep::T;
542            }
543            DecodeStep::T => {
544                n.contexts[result_context].t = result;
545                n.contexts[result_context].step = DecodeStep::T2d;
546            }
547            _ => {
548                output.valid = true;
549                output.point_valid = current.canonical
550                    && current.square
551                    && !(field_eq_kernel(current.x, zero) && current.sign);
552                output.small_order = current.small_order;
553                output.point = ExtendedPoint {
554                    x: current.x,
555                    y: current.y,
556                    z: one,
557                    t: current.t,
558                };
559                output.t2d = result;
560                output.tag = current.tag;
561                n.contexts[result_context] = empty_context_kernel(false);
562            }
563        }
564    }
565
566    if i.valid && free_found {
567        let mut y_words = i.encoded.words;
568        let sign = (y_words[3] >> 63) != b64(0);
569        y_words[3] &= b64(0x7fff_ffff_ffff_ffff);
570        n.contexts[free_context] = DecodeContext {
571            state: b2(READY),
572            step: DecodeStep::Y2,
573            tag: i.tag,
574            sign,
575            canonical: canonical_y(i.encoded.words),
576            small_order: small_order_encoding(i.encoded.words),
577            exponent_bit: b8(0),
578            y: field_from_words_kernel(y_words),
579            ..empty_context_kernel(false)
580        };
581    }
582
583    let mut d = PointDecodePipeD::dont_care();
584    d.control = n;
585    d.multiplier = FieldMulIn {
586        valid: issue_found,
587        a: operand_a,
588        b: operand_b,
589    };
590    (output, d)
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use curve25519_dalek::{constants::ED25519_BASEPOINT_COMPRESSED, edwards::CompressedEdwardsY};
597    use ed25519_dalek::{Signer, SigningKey};
598    use std::iter;
599
600    fn compressed(bytes: [u8; 32]) -> CompressedPoint {
601        CompressedPoint {
602            words: std::array::from_fn(|index| {
603                b64(u64::from_le_bytes(bytes[index * 8..index * 8 + 8].try_into().unwrap()) as u128)
604            }),
605        }
606    }
607
608    fn point_bytes(point: ExtendedPoint) -> [u8; 32] {
609        let mut bytes = [0u8; 32];
610        for (index, word) in field_to_words_kernel(point.y).iter().enumerate() {
611            bytes[index * 8..index * 8 + 8].copy_from_slice(&(word.raw() as u64).to_le_bytes());
612        }
613        bytes[31] |= ((field_to_words_kernel(point.x)[0].raw() as u8) & 1) << 7;
614        bytes
615    }
616
617    fn idle() -> PointDecodeIn {
618        PointDecodeIn {
619            valid: false,
620            encoded: CompressedPoint::zero(),
621            tag: b16(0),
622        }
623    }
624
625    #[test]
626    fn decodes_basepoint_and_rejects_noncanonical_and_low_order() {
627        std::thread::Builder::new()
628            .name("point-decode-sim".into())
629            .stack_size(256 * 1024 * 1024)
630            .spawn(|| {
631                let mut noncanonical = [0xff; 32];
632                noncanonical[31] = 0x7f;
633                let identity = {
634                    let mut value = [0u8; 32];
635                    value[0] = 1;
636                    value
637                };
638                let key = SigningKey::from_bytes(&[0x42; 32]);
639                let signature = key.sign(&[0x5a; 64]).to_bytes();
640                let mut encoded_r = [0u8; 32];
641                encoded_r.copy_from_slice(&signature[..32]);
642                let public_key = key.verifying_key().to_bytes();
643                let requests = [
644                    PointDecodeIn {
645                        valid: true,
646                        encoded: compressed(ED25519_BASEPOINT_COMPRESSED.to_bytes()),
647                        tag: b16(1),
648                    },
649                    PointDecodeIn {
650                        valid: true,
651                        encoded: compressed(noncanonical),
652                        tag: b16(2),
653                    },
654                    PointDecodeIn {
655                        valid: true,
656                        encoded: compressed(identity),
657                        tag: b16(3),
658                    },
659                    PointDecodeIn {
660                        valid: true,
661                        encoded: compressed(public_key),
662                        tag: b16(4),
663                    },
664                    PointDecodeIn {
665                        valid: true,
666                        encoded: compressed(encoded_r),
667                        tag: b16(5),
668                    },
669                ];
670                let outputs: Vec<_> = PointDecodePipe::default()
671                    .run(
672                        iter::once(idle())
673                            .chain(requests)
674                            .chain(iter::repeat_n(idle(), 12_000))
675                            .with_reset(1)
676                            .clock_pos_edge(100),
677                    )
678                    .synchronous_sample()
679                    .map(|sample| sample.output)
680                    .filter(|output| output.valid)
681                    .collect();
682                assert_eq!(outputs.len(), 5);
683                let base = outputs.iter().find(|output| output.tag == b16(1)).unwrap();
684                assert!(base.point_valid);
685                assert!(!base.small_order);
686                assert_eq!(
687                    CompressedEdwardsY(point_bytes(base.point)),
688                    ED25519_BASEPOINT_COMPRESSED
689                );
690                let bad = outputs.iter().find(|output| output.tag == b16(2)).unwrap();
691                assert!(!bad.point_valid);
692                let low = outputs.iter().find(|output| output.tag == b16(3)).unwrap();
693                assert!(low.point_valid);
694                assert!(low.small_order);
695                let public = outputs.iter().find(|output| output.tag == b16(4)).unwrap();
696                assert!(public.point_valid);
697                assert_eq!(point_bytes(public.point), public_key);
698                let r = outputs.iter().find(|output| output.tag == b16(5)).unwrap();
699                assert!(r.point_valid);
700                assert_eq!(point_bytes(r.point), encoded_r);
701            })
702            .unwrap()
703            .join()
704            .unwrap();
705    }
706
707    #[test]
708    fn compiles_to_native_rhdl_netlist() -> miette::Result<()> {
709        unsafe { std::env::set_var("RHDL_SKIP_IVERILOG_CHECK", "1") };
710        PointDecodePipe::default()
711            .descriptor(ScopedName::top())?
712            .hdl()?;
713        Ok(())
714    }
715}