Skip to main content

miden_precompiles/math/curve/
mod.rs

1//! Fixed-curve precompile backed by fixed uint coordinate domains.
2//!
3//! This crate API is internal to `miden-precompiles`; it is not a public curve library and does not
4//! promise stable external encodings or trait APIs.
5//!
6//! This precompile owns concrete curve configurations and exposes a small generic operation surface
7//! over point-valued deferred nodes:
8//!
9//! - `VALUE`: canonical point value, represented as a join payload `[x_digest, y_digest]`; the
10//!   identity point is the single canonical value `[TRUE_DIGEST, TRUE_DIGEST]`.
11//! - `ADD` / `SUB`: point addition and subtraction.
12//! - `MSM`: multi-scalar multiplication over one or more structural `(point_digest, scalar_digest)`
13//!   pairs with nonzero scalars and distinct canonical points.
14//! - `EQ`: trapping equality predicate that evaluates to `Node::TRUE` only when both operands
15//!   reduce to the same canonical point.
16//!
17//! Affine coordinates are canonical uint values in the curve's base-field domain. Concrete MASM
18//! support modules are generated separately and are currently internal implementation detail.
19//!
20//! ## Trust contract
21//!
22//! Curve `VALUE` nodes are the validation boundary. Raw affine coordinates and raw payload digests
23//! are untrusted until this precompile evaluates them into a canonical curve `VALUE` node.
24//! Registration/evaluation through `DeferredState::register` and `evaluate_digest` routes curve
25//! nodes through this precompile, so once a digest reduces to a canonical curve `VALUE` node its
26//! decoded `CurvePoint` is trusted by the internal arithmetic below.
27//!
28//! The canonical identity payload is `[TRUE_DIGEST, TRUE_DIGEST]`. Curve implementations must
29//! preserve this single identity representation when building canonical `VALUE` nodes.
30//!
31//! Arithmetic methods (`add`, `neg`, `sub`, `mul_scalar`) are internal trusted operations over
32//! canonical valid points and scalars. Deferred multiplication is exposed only through `MSM`.
33//! These methods may use debug assertions to document invariants in debug builds, but release
34//! builds do not revalidate curve membership before applying formulas.
35//!
36//! This precompile does not provide compressed point encodings, subgroup checks, signature
37//! semantics, or public API stability guarantees beyond this internal precompile contract.
38
39mod secp256k1;
40mod short_weierstrass;
41
42use alloc::vec::Vec;
43
44use miden_core::{
45    Felt, ZERO,
46    deferred::{
47        DeferredContext, DeferredError, Digest, Node, NodeType, Payload, Precompile,
48        PrecompileError, TRUE_DIGEST, Tag, precompile_id,
49    },
50};
51
52use self::secp256k1::Secp256k1;
53pub use self::secp256k1::{SECP256K1_GENERATOR_X, SECP256K1_GENERATOR_Y, SECP256K1_ID};
54use crate::math::uint::{Limbs, UintDomain, UintPrecompile, UintSpec};
55
56/// VM-owned store pointer for the secp256k1 curve coefficient `A`.
57pub const K1_A_PTR: u32 = 8;
58/// VM-owned store pointer for the secp256k1 curve coefficient `B`.
59pub const K1_B_PTR: u32 = 9;
60
61/// VM-owned store pointer for the secp256k1 group configuration.
62pub const K1_GROUP_PTR: u32 = 1;
63
64/// A fixed curve coefficient uint pinned at a VM-owned store pointer.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct CurveCoefficient {
67    /// VM-owned pointer for this coefficient value.
68    pub ptr: u32,
69    /// VM-owned pointer for the uint domain bound/modulus used by this coefficient.
70    pub bound_ptr: u32,
71    /// Canonical coefficient value, little-endian u32 limbs.
72    pub value: Limbs,
73}
74
75/// Returns all fixed curve coefficients in VM pointer order.
76///
77/// The order is secp256k1 `A`/`B`.
78pub fn curve_coefficients() -> [CurveCoefficient; 2] {
79    [
80        CurveCoefficient {
81            ptr: CurveId::Secp256k1.a_ptr(),
82            bound_ptr: CurveId::Secp256k1.base_domain().bound_ptr(),
83            value: <Secp256k1 as ShortWeierstrassSpec>::A,
84        },
85        CurveCoefficient {
86            ptr: CurveId::Secp256k1.b_ptr(),
87            bound_ptr: CurveId::Secp256k1.base_domain().bound_ptr(),
88            value: <Secp256k1 as ShortWeierstrassSpec>::B,
89        },
90    ]
91}
92
93/// Curve-generic point value.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum CurvePoint {
96    /// The identity point.
97    Identity,
98    /// Affine coordinate limbs. Raw values remain untrusted until checked by curve evaluation or a
99    /// checked [`CurveSpec`] boundary.
100    Affine { x: Limbs, y: Limbs },
101}
102
103/// Spec for one fixed curve.
104///
105/// This trait intentionally describes only the affine encoding and group operations needed by the
106/// precompile dispatcher. It is curve-model-generic, not short-Weierstrass-specific;
107/// curve-model-specific equations, coefficients, and formulas live behind narrower extension traits
108/// such as [`ShortWeierstrassSpec`].
109///
110/// Trust contract:
111///
112/// - [`Self::point_from_affine`] and [`Self::canonical_point`] are checked boundary helpers. They
113///   validate and canonicalize raw affine coordinates before a [`CurvePoint`] becomes trusted.
114/// - [`Self::add`], [`Self::neg`], [`Self::sub`], and [`Self::mul_scalar`] are trusted internal
115///   operations. Their point inputs must already be canonical valid points obtained from checked
116///   boundaries, canonical curve `VALUE` nodes, or previous curve operations.
117/// - Violating these preconditions is not memory-unsafe, but formulas may return invalid or
118///   nonsensical arithmetic results, or fail with `InvalidPayload`.
119pub trait CurveSpec: Sized + 'static {
120    /// Stable local curve selector used by host-side metadata.
121    ///
122    /// Deferred curve `VALUE` tags carry fixed VM-owned group pointers, while operation tags use
123    /// zero immediates.
124    const ID: Felt;
125
126    /// Base field used by affine point coordinates.
127    type BaseField: UintSpec;
128
129    /// Scalar field associated with this curve.
130    type ScalarField: UintSpec;
131
132    /// Conventional generator x-coordinate, little-endian u32 limbs.
133    const GENERATOR_X: Limbs;
134
135    /// Conventional generator y-coordinate, little-endian u32 limbs.
136    const GENERATOR_Y: Limbs;
137
138    /// Returns this curve's conventional generator point.
139    fn generator() -> CurvePoint {
140        Self::point_from_affine(Self::GENERATOR_X, Self::GENERATOR_Y)
141            .expect("curve generator coordinates must be valid")
142    }
143
144    /// Checked boundary helper that constructs this curve's canonical point from affine
145    /// coordinates.
146    ///
147    /// This is where raw affine limbs become a trusted [`CurvePoint`]. Implementations must
148    /// validate that `x` and `y` are canonical base-field elements and satisfy the curve
149    /// equation. They should also canonicalize any model-specific identity representation to
150    /// [`CurvePoint::Identity`] before the point is re-encoded as a graph `VALUE` node.
151    fn point_from_affine(x: Limbs, y: Limbs) -> Result<CurvePoint, PrecompileError>;
152
153    /// Checked boundary helper that returns the canonical representation of `point`.
154    ///
155    /// Affine inputs are revalidated through [`Self::point_from_affine`]; identity is accepted as
156    /// the already-canonical identity. Arithmetic methods below assume their inputs already
157    /// came from checked graph nodes or previous curve operations.
158    fn canonical_point(point: CurvePoint) -> Result<CurvePoint, PrecompileError> {
159        match point {
160            CurvePoint::Identity => Ok(CurvePoint::Identity),
161            CurvePoint::Affine { x, y } => Self::point_from_affine(x, y),
162        }
163    }
164
165    /// Returns whether `point` is a valid point on this curve.
166    fn is_on_curve(point: &CurvePoint) -> bool {
167        Self::canonical_point(*point).is_ok()
168    }
169
170    /// Trusted internal operation that adds two canonical valid points on this curve.
171    ///
172    /// Preconditions: both operands must have come from [`Self::point_from_affine`],
173    /// [`Self::canonical_point`], a canonical curve `VALUE` node, or previous curve operations.
174    /// Release builds do not revalidate arbitrary coordinates before applying the curve formula.
175    /// Violating this contract is not memory-unsafe, but may produce invalid or nonsensical
176    /// results, or fail with `InvalidPayload`.
177    fn add(lhs: CurvePoint, rhs: CurvePoint) -> Result<CurvePoint, PrecompileError>;
178
179    /// Trusted internal operation that negates a canonical valid point on this curve.
180    ///
181    /// Precondition: `point` must have come from [`Self::point_from_affine`],
182    /// [`Self::canonical_point`], a canonical curve `VALUE` node, or previous curve operations.
183    /// Release builds do not revalidate arbitrary coordinates before applying the curve formula.
184    /// Violating this contract is not memory-unsafe, but may produce invalid or nonsensical
185    /// results, or fail with `InvalidPayload`.
186    fn neg(point: CurvePoint) -> Result<CurvePoint, PrecompileError>;
187
188    /// Trusted internal operation that subtracts two canonical valid points on this curve.
189    ///
190    /// Preconditions are the same as [`Self::add`] and [`Self::neg`]: operands must already be
191    /// canonical valid points from checked boundaries, canonical curve `VALUE` nodes, or previous
192    /// curve operations. Violating this contract is not memory-unsafe, but may produce invalid or
193    /// nonsensical results, or fail with `InvalidPayload`.
194    fn sub(lhs: CurvePoint, rhs: CurvePoint) -> Result<CurvePoint, PrecompileError> {
195        let rhs = Self::neg(rhs)?;
196        Self::add(lhs, rhs)
197    }
198
199    /// Trusted internal operation that multiplies a canonical valid point by a canonical scalar.
200    ///
201    /// Precondition: `point` must already be a canonical valid point from a checked boundary,
202    /// canonical curve `VALUE` node, or previous curve operation. `scalar` must be canonical in
203    /// this curve's scalar field. Violating the point precondition is not memory-unsafe, but may
204    /// produce invalid or nonsensical results, or fail with `InvalidPayload`.
205    fn mul_scalar(point: CurvePoint, scalar: Limbs) -> Result<CurvePoint, PrecompileError> {
206        debug_assert!(Self::is_on_curve(&point));
207        debug_assert!(Self::ScalarField::is_canonical(&scalar));
208
209        let Some(highest_limb) = scalar.iter().rposition(|&limb| limb != 0) else {
210            return Ok(CurvePoint::Identity);
211        };
212        let highest_bit =
213            highest_limb * 32 + (u32::BITS - 1 - scalar[highest_limb].leading_zeros()) as usize;
214
215        let mut acc = CurvePoint::Identity;
216        let mut base = point;
217
218        for bit_index in 0..=highest_bit {
219            let limb = scalar[bit_index / 32];
220            if ((limb >> (bit_index % 32)) & 1) == 1 {
221                acc = Self::add(acc, base)?;
222            }
223            if bit_index != highest_bit {
224                base = Self::add(base, base)?;
225            }
226        }
227
228        Ok(acc)
229    }
230}
231
232/// Short-Weierstrass-specific parameters for curves of the form `y^2 = x^3 + A*x + B`.
233pub trait ShortWeierstrassSpec: CurveSpec {
234    /// Short-Weierstrass coefficient `A`.
235    const A: Limbs;
236
237    /// Short-Weierstrass coefficient `B`.
238    const B: Limbs;
239}
240
241/// Fixed curves supported by the native curve precompile.
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum CurveId {
244    Secp256k1,
245}
246
247impl CurveId {
248    /// All fixed curves in deterministic precompile initialization order.
249    pub const ALL: [Self; 1] = [Self::Secp256k1];
250
251    /// Returns the supported curve for an internal curve selector.
252    pub fn from_id(id: Felt) -> Option<Self> {
253        match id {
254            id if id == <Secp256k1 as CurveSpec>::ID => Some(Self::Secp256k1),
255            _ => None,
256        }
257    }
258
259    /// Returns the stable local curve selector retained for dispatch metadata.
260    pub fn id(self) -> Felt {
261        match self {
262            Self::Secp256k1 => SECP256K1_ID,
263        }
264    }
265
266    /// Returns the VM-owned group configuration pointer used in curve VALUE tags.
267    pub const fn group_ptr(self) -> u32 {
268        match self {
269            Self::Secp256k1 => K1_GROUP_PTR,
270        }
271    }
272
273    /// Returns the supported curve for a VM-owned group configuration pointer.
274    pub const fn from_group_ptr(ptr: u32) -> Option<Self> {
275        match ptr {
276            K1_GROUP_PTR => Some(Self::Secp256k1),
277            _ => None,
278        }
279    }
280
281    /// Returns the VM-owned pointer for this curve's first coefficient.
282    pub const fn a_ptr(self) -> u32 {
283        match self {
284            Self::Secp256k1 => K1_A_PTR,
285        }
286    }
287
288    /// Returns the VM-owned pointer for this curve's second coefficient.
289    pub const fn b_ptr(self) -> u32 {
290        match self {
291            Self::Secp256k1 => K1_B_PTR,
292        }
293    }
294
295    /// Returns the base-field domain used by affine point coordinates.
296    pub const fn base_domain(self) -> UintDomain {
297        match self {
298            Self::Secp256k1 => UintDomain::K1Base,
299        }
300    }
301
302    /// Returns this curve's first fixed coefficient value.
303    pub fn a_value(self) -> Limbs {
304        match self {
305            Self::Secp256k1 => <Secp256k1 as ShortWeierstrassSpec>::A,
306        }
307    }
308
309    /// Returns this curve's second fixed coefficient value.
310    pub fn b_value(self) -> Limbs {
311        match self {
312            Self::Secp256k1 => <Secp256k1 as ShortWeierstrassSpec>::B,
313        }
314    }
315
316    /// Returns this curve's scalar-field domain.
317    pub const fn scalar_domain(self) -> UintDomain {
318        match self {
319            Self::Secp256k1 => UintDomain::K1Scalar,
320        }
321    }
322
323    /// Returns this curve's conventional generator point.
324    pub fn generator(self) -> CurvePoint {
325        match self {
326            Self::Secp256k1 => Secp256k1::generator(),
327        }
328    }
329
330    /// Checked boundary dispatcher that constructs this curve's canonical point for affine
331    /// coordinates.
332    pub fn point_from_affine(self, x: Limbs, y: Limbs) -> Result<CurvePoint, PrecompileError> {
333        match self {
334            Self::Secp256k1 => Secp256k1::point_from_affine(x, y),
335        }
336    }
337
338    /// Returns whether `point` is a valid point on this curve.
339    pub fn is_on_curve(self, point: &CurvePoint) -> bool {
340        match self {
341            Self::Secp256k1 => Secp256k1::is_on_curve(point),
342        }
343    }
344
345    /// Trusted dispatcher that adds two canonical valid points on this curve.
346    pub fn add(self, lhs: CurvePoint, rhs: CurvePoint) -> Result<CurvePoint, PrecompileError> {
347        match self {
348            Self::Secp256k1 => Secp256k1::add(lhs, rhs),
349        }
350    }
351
352    /// Trusted dispatcher that negates a canonical valid point on this curve.
353    pub fn neg(self, point: CurvePoint) -> Result<CurvePoint, PrecompileError> {
354        match self {
355            Self::Secp256k1 => Secp256k1::neg(point),
356        }
357    }
358
359    /// Trusted dispatcher that subtracts two canonical valid points on this curve.
360    pub fn sub(self, lhs: CurvePoint, rhs: CurvePoint) -> Result<CurvePoint, PrecompileError> {
361        match self {
362            Self::Secp256k1 => Secp256k1::sub(lhs, rhs),
363        }
364    }
365
366    /// Trusted dispatcher that multiplies a canonical valid point by a canonical scalar-field
367    /// value.
368    pub fn mul_scalar(
369        self,
370        point: CurvePoint,
371        scalar: Limbs,
372    ) -> Result<CurvePoint, PrecompileError> {
373        match self {
374            Self::Secp256k1 => Secp256k1::mul_scalar(point, scalar),
375        }
376    }
377}
378
379/// Structural view of a curve precompile node.
380///
381/// All digest fields are structural payload children. This type does not decode coordinate uint
382/// values, validate curve membership, or canonicalize points.
383#[derive(Debug, Clone, PartialEq, Eq)]
384pub enum CurveNodeRef {
385    /// Curve point value with structural coordinate child digests.
386    Value { curve: CurveId, x: Digest, y: Digest },
387    /// Point addition over two structural child digests.
388    Add { lhs: Digest, rhs: Digest },
389    /// Point subtraction over two structural child digests.
390    Sub { lhs: Digest, rhs: Digest },
391    /// Equality assertion over two structural child digests.
392    Eq { lhs: Digest, rhs: Digest },
393    /// Multi-scalar multiplication payload pairs in structural order.
394    Msm { pairs: Vec<(Digest, Digest)> },
395}
396
397/// Recognized curve binary operation.
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399enum CurveBinaryOp {
400    Add,
401    Sub,
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405enum CurveOp {
406    Value(CurveId),
407    Binary(CurveBinaryOp),
408    Eq,
409    Msm,
410}
411
412impl CurveOp {
413    fn decode(args: [Felt; 3]) -> Option<Self> {
414        match args[0].as_canonical_u64() {
415            CurvePrecompile::VALUE_OP_ID if args[2] == ZERO => {
416                let group_ptr = u32::try_from(args[1].as_canonical_u64()).ok()?;
417                let curve = CurveId::from_group_ptr(group_ptr)?;
418                Some(Self::Value(curve))
419            },
420            CurvePrecompile::ADD_OP_ID if args[1] == ZERO && args[2] == ZERO => {
421                Some(Self::Binary(CurveBinaryOp::Add))
422            },
423            CurvePrecompile::SUB_OP_ID if args[1] == ZERO && args[2] == ZERO => {
424                Some(Self::Binary(CurveBinaryOp::Sub))
425            },
426            CurvePrecompile::EQ_OP_ID if args[1] == ZERO && args[2] == ZERO => Some(Self::Eq),
427            CurvePrecompile::MSM_OP_ID if args[1] == ZERO && args[2] == ZERO => Some(Self::Msm),
428            _ => None,
429        }
430    }
431
432    fn node_type(self) -> NodeType {
433        match self {
434            Self::Value(_) | Self::Binary(_) | Self::Eq => NodeType::Join,
435            Self::Msm => NodeType::PairList,
436        }
437    }
438}
439
440enum CurveNode {
441    Value {
442        curve: CurveId,
443        lhs: Digest,
444        rhs: Digest,
445    },
446    BinaryOp {
447        op: CurveBinaryOp,
448        lhs: Digest,
449        rhs: Digest,
450    },
451    Eq {
452        lhs: Digest,
453        rhs: Digest,
454    },
455    Msm {
456        pairs: Vec<(Digest, Digest)>,
457    },
458}
459
460impl CurveNode {
461    fn parse(op: CurveOp, payload: &Payload) -> Result<Self, PrecompileError> {
462        Ok(match op {
463            CurveOp::Value(curve) => {
464                let (lhs, rhs) = payload.as_join()?;
465                Self::Value { curve, lhs, rhs }
466            },
467            CurveOp::Binary(op) => {
468                let (lhs, rhs) = payload.as_join()?;
469                Self::BinaryOp { op, lhs, rhs }
470            },
471            CurveOp::Eq => {
472                let (lhs, rhs) = payload.as_join()?;
473                Self::Eq { lhs, rhs }
474            },
475            CurveOp::Msm => {
476                let pairs = payload.as_pair_list()?;
477                Self::Msm { pairs }
478            },
479        })
480    }
481}
482
483/// Precompile for point operations over the fixed supported curves.
484#[derive(Clone, Copy, Debug, Default)]
485pub struct CurvePrecompile;
486
487impl CurvePrecompile {
488    /// Stable precompile name used to derive this precompile's tag id.
489    pub const NAME: &'static str = "curve";
490
491    /// Operation discriminants owned by this precompile.
492    pub const VALUE_OP_ID: u64 = 0;
493    pub const ADD_OP_ID: u64 = 1;
494    pub const SUB_OP_ID: u64 = 2;
495    pub const EQ_OP_ID: u64 = 3;
496    pub const MSM_OP_ID: u64 = 4;
497
498    /// Stable precompile id derived from [`Self::NAME`].
499    pub fn id() -> Felt {
500        precompile_id(Self::NAME)
501    }
502
503    /// Builds a canonical curve `VALUE` tag for `curve`.
504    pub fn value_tag(curve: CurveId) -> Tag {
505        let op_id = Felt::new(Self::VALUE_OP_ID).expect("curve VALUE op id must fit in a felt");
506        Tag::precompile(Self::id(), [op_id, Felt::from(curve.group_ptr()), ZERO])
507            .expect("curve precompile id is not framework-reserved")
508    }
509
510    /// Builds a curve operation tag from `op_id`.
511    ///
512    /// Known operation ids decode to their declared shapes; unknown ids produce a tag that this
513    /// precompile rejects.
514    pub fn op_tag(op_id: u64) -> Tag {
515        let op_id = Felt::new(op_id).expect("curve op id must fit in a felt");
516        Tag::precompile(Self::id(), [op_id, ZERO, ZERO])
517            .expect("curve precompile id is not framework-reserved")
518    }
519
520    /// Builds the canonical curve MSM tag.
521    pub fn msm_tag() -> Tag {
522        Self::op_tag(Self::MSM_OP_ID)
523    }
524
525    /// Builds a structural point `VALUE` node from point data.
526    ///
527    /// This does not validate raw affine coordinates; registration and evaluation perform that
528    /// validation before producing a canonical curve value.
529    pub fn value_node(curve: CurveId, point: CurvePoint) -> Node {
530        match point {
531            CurvePoint::Identity => Self::identity_node(curve),
532            CurvePoint::Affine { x, y } => Self::affine_node_from_digests(
533                curve,
534                UintPrecompile::value_node(curve.base_domain(), x).digest(),
535                UintPrecompile::value_node(curve.base_domain(), y).digest(),
536            ),
537        }
538    }
539
540    /// Builds the canonical identity point value node for `curve`.
541    pub fn identity_node(curve: CurveId) -> Node {
542        Node::join(Self::value_tag(curve), TRUE_DIGEST, TRUE_DIGEST)
543            .expect("curve value tag is precompile-owned")
544    }
545
546    /// Builds the canonical generator value node for `curve`.
547    pub fn generator_node(curve: CurveId) -> Node {
548        Self::value_node(curve, curve.generator())
549    }
550
551    /// Builds an affine point VALUE node from coordinate digests.
552    pub fn affine_node_from_digests(curve: CurveId, x: Digest, y: Digest) -> Node {
553        Node::join(Self::value_tag(curve), x, y).expect("curve value tag is precompile-owned")
554    }
555
556    /// Decodes a curve precompile node without evaluating its children.
557    ///
558    /// Returns `Ok(None)` when `node` belongs to another precompile. Owned nodes return their
559    /// structural child digests or pair list directly from the payload.
560    pub fn decode_node(node: &Node) -> Result<Option<CurveNodeRef>, PrecompileError> {
561        if node.tag().id() != Self::id() {
562            return Ok(None);
563        }
564
565        let op = CurveOp::decode(node.tag().args()).ok_or(PrecompileError::InvalidNode)?;
566        let parsed = CurveNode::parse(op, node.payload())?;
567        Ok(Some(match parsed {
568            CurveNode::Value { curve, lhs: x, rhs: y } => CurveNodeRef::Value { curve, x, y },
569            CurveNode::BinaryOp { op: CurveBinaryOp::Add, lhs, rhs } => {
570                CurveNodeRef::Add { lhs, rhs }
571            },
572            CurveNode::BinaryOp { op: CurveBinaryOp::Sub, lhs, rhs } => {
573                CurveNodeRef::Sub { lhs, rhs }
574            },
575            CurveNode::Eq { lhs, rhs } => CurveNodeRef::Eq { lhs, rhs },
576            CurveNode::Msm { pairs } => CurveNodeRef::Msm { pairs },
577        }))
578    }
579
580    fn extend_init_nodes_with_point(nodes: &mut Vec<Node>, curve: CurveId, point: CurvePoint) {
581        if let CurvePoint::Affine { x, y } = point {
582            let x = UintPrecompile::value_node(curve.base_domain(), x);
583            let y = UintPrecompile::value_node(curve.base_domain(), y);
584            nodes.push(x.clone());
585            nodes.push(y.clone());
586            nodes.push(Self::affine_node_from_digests(curve, x.digest(), y.digest()));
587        }
588    }
589
590    /// Builds the canonical curve `VALUE` node for a trusted point.
591    ///
592    /// This is used after a checked boundary or trusted arithmetic operation has produced a
593    /// canonical [`CurvePoint`]. Affine coordinates are registered as canonical base-field uint
594    /// `VALUE` nodes in the deferred context; identity always uses `[TRUE_DIGEST, TRUE_DIGEST]`.
595    fn canonical_value_node(
596        curve: CurveId,
597        point: CurvePoint,
598        context: &mut DeferredContext<'_>,
599    ) -> Result<Node, PrecompileError> {
600        match point {
601            CurvePoint::Identity => Ok(Self::identity_node(curve)),
602            CurvePoint::Affine { x, y } => {
603                let x = context.register(UintPrecompile::value_node(curve.base_domain(), x))?;
604                let y = context.register(UintPrecompile::value_node(curve.base_domain(), y))?;
605                Ok(Self::affine_node_from_digests(curve, x, y))
606            },
607        }
608    }
609
610    fn evaluate_msm_term(
611        expected_curve: Option<CurveId>,
612        point: Digest,
613        scalar: Digest,
614        context: &mut DeferredContext<'_>,
615    ) -> Result<(CurveId, CurvePoint, Limbs), PrecompileError> {
616        let (point_digest, scalar_digest) = context.evaluate_digest_pair(point, scalar)?;
617        let point_node = context.get_node(&point_digest).ok_or(PrecompileError::MissingNode)?;
618        let scalar_node = context.get_node(&scalar_digest).ok_or(PrecompileError::MissingNode)?;
619
620        let (point_curve, point) = Self::point_from_value_node(point_node, context)?;
621        if let Some(expected_curve) = expected_curve
622            && expected_curve != point_curve
623        {
624            return Err(DeferredError::InvalidPayload.into());
625        }
626        let scalar =
627            UintPrecompile::limbs_from_value_node(scalar_node, point_curve.scalar_domain())?;
628        Ok((point_curve, point, scalar))
629    }
630
631    fn evaluate_msm(
632        pairs: &[(Digest, Digest)],
633        context: &mut DeferredContext<'_>,
634    ) -> Result<(CurveId, CurvePoint), PrecompileError> {
635        let Some((&(point, scalar), rest)) = pairs.split_first() else {
636            return Err(DeferredError::InvalidPayload.into());
637        };
638
639        let (curve, point, scalar) = Self::evaluate_msm_term(None, point, scalar, context)?;
640        if scalar == [0; 8] {
641            return Err(DeferredError::InvalidPayload.into());
642        }
643        let mut acc = curve.mul_scalar(point, scalar)?;
644        let mut points = Vec::with_capacity(pairs.len());
645        points.push(point);
646        for &(point, scalar) in rest {
647            let (_, point, scalar) = Self::evaluate_msm_term(Some(curve), point, scalar, context)?;
648            if scalar == [0; 8] || points.contains(&point) {
649                return Err(DeferredError::InvalidPayload.into());
650            }
651            points.push(point);
652            let term = curve.mul_scalar(point, scalar)?;
653            acc = curve.add(acc, term)?;
654        }
655        Ok((curve, acc))
656    }
657
658    fn evaluate_point_pair(
659        context: &mut DeferredContext<'_>,
660        lhs: Digest,
661        rhs: Digest,
662    ) -> Result<(CurveId, CurvePoint, CurvePoint), PrecompileError> {
663        let (lhs, rhs) = context.evaluate_digest_pair(lhs, rhs)?;
664        let (lhs_curve, lhs) = {
665            let lhs = context.get_node(&lhs).ok_or(PrecompileError::MissingNode)?;
666            Self::point_from_value_node(lhs, context)?
667        };
668        let (rhs_curve, rhs) = {
669            let rhs = context.get_node(&rhs).ok_or(PrecompileError::MissingNode)?;
670            Self::point_from_value_node(rhs, context)?
671        };
672
673        if lhs_curve != rhs_curve {
674            return Err(DeferredError::InvalidPayload.into());
675        }
676
677        Ok((lhs_curve, lhs, rhs))
678    }
679
680    /// Decodes an already-evaluated canonical curve `VALUE` node and infers its curve.
681    fn point_from_value_node(
682        node: &Node,
683        context: &DeferredContext<'_>,
684    ) -> Result<(CurveId, CurvePoint), PrecompileError> {
685        let Some(CurveOp::Value(curve)) = CurveOp::decode(node.tag().args()) else {
686            return Err(DeferredError::InvalidPayload.into());
687        };
688        let point = Self::point_of_canonical_node(curve, node, context)?;
689        Ok((curve, point))
690    }
691
692    /// Decodes an already-evaluated canonical curve `VALUE` node.
693    ///
694    /// The caller must have reached `node` through deferred evaluation for this curve. This helper
695    /// still checks the expected curve `VALUE` tag and join structure before taking the trusted
696    /// canonical payload path.
697    fn point_of_canonical_node(
698        curve: CurveId,
699        node: &Node,
700        context: &DeferredContext<'_>,
701    ) -> Result<CurvePoint, PrecompileError> {
702        let payload = node.payload_for_tag(Self::value_tag(curve))?;
703        let (x_digest, y_digest) = payload.as_join()?;
704        Self::point_from_canonical_value_payload(curve, x_digest, y_digest, context)
705    }
706
707    /// Checked decoder for a curve `VALUE` payload from the raw affine boundary.
708    ///
709    /// Used while evaluating a `VALUE` node after coordinate digests have themselves been
710    /// evaluated. It rejects mixed identity payloads, decodes both coordinates as base-field
711    /// uint `VALUE` nodes, and calls [`CurveId::point_from_affine`] to validate curve
712    /// membership and canonicalize model-specific affine identities.
713    fn point_from_checked_value_payload(
714        curve: CurveId,
715        x_digest: Digest,
716        y_digest: Digest,
717        context: &DeferredContext<'_>,
718    ) -> Result<CurvePoint, PrecompileError> {
719        match (x_digest == TRUE_DIGEST, y_digest == TRUE_DIGEST) {
720            (true, true) => Ok(CurvePoint::Identity),
721            (true, false) | (false, true) => Err(DeferredError::InvalidPayload.into()),
722            (false, false) => {
723                let x_node = context.get_node(&x_digest).ok_or(PrecompileError::MissingNode)?;
724                let y_node = context.get_node(&y_digest).ok_or(PrecompileError::MissingNode)?;
725                let x = UintPrecompile::limbs_from_value_node(x_node, curve.base_domain())?;
726                let y = UintPrecompile::limbs_from_value_node(y_node, curve.base_domain())?;
727                curve.point_from_affine(x, y)
728            },
729        }
730    }
731
732    /// Trusted decoder for a payload of an already-canonical curve `VALUE` node.
733    ///
734    /// Canonical curve `VALUE` nodes are produced by this precompile, so release builds rely on the
735    /// prior checked boundary instead of revalidating curve membership here. Structural checks
736    /// remain: mixed identity payloads are rejected and affine coordinate digests must decode
737    /// as base-field uint `VALUE` nodes. Debug builds assert curve membership for invariant
738    /// checking.
739    fn point_from_canonical_value_payload(
740        curve: CurveId,
741        x_digest: Digest,
742        y_digest: Digest,
743        context: &DeferredContext<'_>,
744    ) -> Result<CurvePoint, PrecompileError> {
745        match (x_digest == TRUE_DIGEST, y_digest == TRUE_DIGEST) {
746            (true, true) => Ok(CurvePoint::Identity),
747            (true, false) | (false, true) => Err(DeferredError::InvalidPayload.into()),
748            (false, false) => {
749                let x_node = context.get_node(&x_digest).ok_or(PrecompileError::MissingNode)?;
750                let y_node = context.get_node(&y_digest).ok_or(PrecompileError::MissingNode)?;
751                let x = UintPrecompile::limbs_from_value_node(x_node, curve.base_domain())?;
752                let y = UintPrecompile::limbs_from_value_node(y_node, curve.base_domain())?;
753                let point = CurvePoint::Affine { x, y };
754                debug_assert!(curve.is_on_curve(&point));
755                Ok(point)
756            },
757        }
758    }
759}
760
761impl Precompile for CurvePrecompile {
762    fn name(&self) -> &'static str {
763        Self::NAME
764    }
765
766    fn id(&self) -> Felt {
767        Self::id()
768    }
769
770    fn init(&self) -> Vec<Node> {
771        let mut nodes = Vec::with_capacity(CurveId::ALL.len() * 4);
772        for curve in CurveId::ALL {
773            nodes.push(Self::identity_node(curve));
774            Self::extend_init_nodes_with_point(&mut nodes, curve, curve.generator());
775        }
776        nodes
777    }
778
779    fn decode(&self, args: [Felt; 3]) -> Option<NodeType> {
780        let op = CurveOp::decode(args)?;
781        Some(op.node_type())
782    }
783
784    fn evaluate(
785        &self,
786        args: [Felt; 3],
787        payload: &Payload,
788        context: &mut DeferredContext<'_>,
789    ) -> Result<Node, PrecompileError> {
790        let op = CurveOp::decode(args).ok_or(PrecompileError::InvalidNode)?;
791
792        match CurveNode::parse(op, payload)? {
793            CurveNode::Value { curve, lhs, rhs } => {
794                match (lhs == TRUE_DIGEST, rhs == TRUE_DIGEST) {
795                    (true, true) => Ok(Self::identity_node(curve)),
796                    (true, false) | (false, true) => Err(DeferredError::InvalidPayload.into()),
797                    (false, false) => {
798                        let (x_digest, y_digest) = context.evaluate_digest_pair(lhs, rhs)?;
799                        let point = Self::point_from_checked_value_payload(
800                            curve, x_digest, y_digest, context,
801                        )?;
802                        Self::canonical_value_node(curve, point, context)
803                    },
804                }
805            },
806            CurveNode::BinaryOp { op, lhs, rhs } => {
807                let (curve, lhs, rhs) = Self::evaluate_point_pair(context, lhs, rhs)?;
808                let value = match op {
809                    CurveBinaryOp::Add => curve.add(lhs, rhs)?,
810                    CurveBinaryOp::Sub => curve.sub(lhs, rhs)?,
811                };
812                Self::canonical_value_node(curve, value, context)
813            },
814            CurveNode::Eq { lhs, rhs } => {
815                let (_, lhs, rhs) = Self::evaluate_point_pair(context, lhs, rhs)?;
816                if lhs == rhs {
817                    Ok(Node::TRUE)
818                } else {
819                    Err(PrecompileError::AssertionFailed)
820                }
821            },
822            CurveNode::Msm { pairs } => {
823                let (curve, value) = Self::evaluate_msm(&pairs, context)?;
824                Self::canonical_value_node(curve, value, context)
825            },
826        }
827    }
828}
829
830#[cfg(test)]
831mod tests {
832    use alloc::{sync::Arc, vec};
833
834    use miden_core::deferred::DeferredState;
835
836    use super::*;
837    use crate::math::{
838        k1_scalar::K1Scalar,
839        uint::{UintPrecompile, ZERO_LIMBS},
840    };
841
842    fn state() -> DeferredState {
843        DeferredState::new(Arc::new(crate::registry()), usize::MAX)
844            .expect("precompile init must succeed")
845    }
846
847    fn evaluate(state: &mut DeferredState, node: Node) -> Result<Node, PrecompileError> {
848        let digest = state.register(node)?;
849        state.require_canonical_node(digest).map(|(_, node)| node.clone())
850    }
851
852    fn assert_invalid_payload<T>(result: Result<T, PrecompileError>) {
853        let Err(error) = result else {
854            panic!("expected invalid payload");
855        };
856        assert!(
857            matches!(error.root(), PrecompileError::Other(DeferredError::InvalidPayload)),
858            "expected invalid payload, got {error:?}",
859        );
860    }
861
862    fn affine_limbs(point: CurvePoint) -> (Limbs, Limbs) {
863        match point {
864            CurvePoint::Affine { x, y } => (x, y),
865            CurvePoint::Identity => panic!("expected affine point"),
866        }
867    }
868
869    fn register_affine_point(state: &mut DeferredState, curve: CurveId, point: CurvePoint) -> Node {
870        let (x, y) = affine_limbs(point);
871        let x = UintPrecompile::value_node(curve.base_domain(), x);
872        let y = UintPrecompile::value_node(curve.base_domain(), y);
873        state.register(x.clone()).expect("x coordinate must register");
874        state.register(y.clone()).expect("y coordinate must register");
875        let point = CurvePrecompile::affine_node_from_digests(curve, x.digest(), y.digest());
876        state.register(point.clone()).expect("point must register");
877        point
878    }
879
880    #[test]
881    fn decode_curve_value_tags() {
882        let precompile = CurvePrecompile;
883        let curve = CurveId::Secp256k1;
884
885        assert_eq!(
886            CurvePrecompile::value_tag(curve).as_word(),
887            [
888                CurvePrecompile::id(),
889                Felt::from_u32(CurvePrecompile::VALUE_OP_ID as u32),
890                Felt::from(curve.group_ptr()),
891                ZERO,
892            ],
893        );
894        assert_eq!(
895            precompile.decode(CurvePrecompile::value_tag(curve).args()),
896            Some(NodeType::Join)
897        );
898        assert_eq!(
899            precompile.decode([
900                Felt::from_u32(CurvePrecompile::VALUE_OP_ID as u32),
901                Felt::from(curve.group_ptr()),
902                Felt::from_u32(1),
903            ]),
904            None
905        );
906        assert_eq!(
907            precompile.decode([
908                Felt::from_u32(CurvePrecompile::VALUE_OP_ID as u32),
909                Felt::new_unchecked(99),
910                ZERO,
911            ]),
912            None
913        );
914    }
915
916    #[test]
917    fn decode_curve_operation_tags() {
918        let precompile = CurvePrecompile;
919        let curve = CurveId::Secp256k1;
920
921        assert_eq!(
922            precompile.decode(CurvePrecompile::op_tag(CurvePrecompile::ADD_OP_ID).args()),
923            Some(NodeType::Join)
924        );
925
926        let mut add_with_curve = CurvePrecompile::op_tag(CurvePrecompile::ADD_OP_ID).args();
927        add_with_curve[1] = Felt::from(curve.group_ptr());
928        assert_eq!(precompile.decode(add_with_curve), None);
929        assert_eq!(precompile.decode(CurvePrecompile::op_tag(5).args()), None);
930    }
931
932    #[test]
933    fn decode_curve_msm_tags() {
934        let precompile = CurvePrecompile;
935        let curve = CurveId::Secp256k1;
936
937        assert_eq!(
938            CurvePrecompile::msm_tag().as_word(),
939            [
940                CurvePrecompile::id(),
941                Felt::from_u32(CurvePrecompile::MSM_OP_ID as u32),
942                ZERO,
943                ZERO,
944            ],
945        );
946        assert_eq!(precompile.decode(CurvePrecompile::msm_tag().args()), Some(NodeType::PairList));
947
948        let mut msm_with_curve = CurvePrecompile::msm_tag().args();
949        msm_with_curve[1] = Felt::from(curve.group_ptr());
950        assert_eq!(precompile.decode(msm_with_curve), None);
951        assert_eq!(
952            precompile.decode([
953                Felt::from_u32(CurvePrecompile::MSM_OP_ID as u32),
954                Felt::from(curve.group_ptr()),
955                Felt::from_u32(1),
956            ]),
957            None
958        );
959    }
960
961    #[test]
962    fn same_curve_add_succeeds() {
963        let mut state = state();
964        let curve = CurveId::Secp256k1;
965        let generator = CurvePrecompile::generator_node(curve);
966        let identity = CurvePrecompile::identity_node(curve);
967        let node = Node::join(
968            CurvePrecompile::op_tag(CurvePrecompile::ADD_OP_ID),
969            generator.digest(),
970            identity.digest(),
971        )
972        .expect("tag is curve-owned");
973
974        assert_eq!(evaluate(&mut state, node).unwrap(), generator);
975    }
976
977    #[test]
978    fn msm_one_pair_evaluates_point_and_scalar_operands() {
979        let mut state = state();
980        let curve = CurveId::Secp256k1;
981        let generator = CurvePrecompile::generator_node(curve);
982        let scalar = UintPrecompile::value_node(curve.scalar_domain(), [2, 0, 0, 0, 0, 0, 0, 0]);
983        state.register(scalar.clone()).expect("scalar must register");
984        let node = Node::try_pair_list(
985            CurvePrecompile::msm_tag(),
986            vec![(generator.digest(), scalar.digest())],
987        )
988        .expect("tag is curve-owned");
989        let expected = CurvePrecompile::value_node(
990            curve,
991            curve
992                .mul_scalar(curve.generator(), [2, 0, 0, 0, 0, 0, 0, 0])
993                .expect("valid mul_scalar"),
994        );
995
996        assert_eq!(evaluate(&mut state, node).unwrap(), expected);
997    }
998
999    #[test]
1000    fn msm_accumulates_multiple_pairs() {
1001        let mut state = state();
1002        let curve = CurveId::Secp256k1;
1003        let generator = CurvePrecompile::generator_node(curve);
1004        let two_g = curve
1005            .mul_scalar(curve.generator(), [2, 0, 0, 0, 0, 0, 0, 0])
1006            .expect("valid scalar multiplication");
1007        let two_g_node = register_affine_point(&mut state, curve, two_g);
1008        let scalar_2 = UintPrecompile::value_node(curve.scalar_domain(), [2, 0, 0, 0, 0, 0, 0, 0]);
1009        let scalar_3 = UintPrecompile::value_node(curve.scalar_domain(), [3, 0, 0, 0, 0, 0, 0, 0]);
1010        state.register(scalar_2.clone()).expect("scalar must register");
1011        state.register(scalar_3.clone()).expect("scalar must register");
1012        let node = Node::try_pair_list(
1013            CurvePrecompile::msm_tag(),
1014            vec![
1015                (generator.digest(), scalar_2.digest()),
1016                (two_g_node.digest(), scalar_3.digest()),
1017            ],
1018        )
1019        .expect("tag is curve-owned");
1020        let two_g_scaled = curve
1021            .mul_scalar(curve.generator(), [2, 0, 0, 0, 0, 0, 0, 0])
1022            .expect("valid scalar multiplication");
1023        let six_g = curve
1024            .mul_scalar(two_g, [3, 0, 0, 0, 0, 0, 0, 0])
1025            .expect("valid scalar multiplication");
1026        let expected = CurvePrecompile::value_node(
1027            curve,
1028            curve.add(two_g_scaled, six_g).expect("valid point addition"),
1029        );
1030
1031        assert_eq!(evaluate(&mut state, node).unwrap(), expected);
1032    }
1033
1034    #[test]
1035    fn msm_rejects_duplicate_canonical_points() {
1036        let mut state = state();
1037        let curve = CurveId::Secp256k1;
1038        let generator = CurvePrecompile::generator_node(curve);
1039        let scalar_2 = UintPrecompile::value_node(curve.scalar_domain(), [2, 0, 0, 0, 0, 0, 0, 0]);
1040        let scalar_3 = UintPrecompile::value_node(curve.scalar_domain(), [3, 0, 0, 0, 0, 0, 0, 0]);
1041        state.register(scalar_2.clone()).expect("scalar must register");
1042        state.register(scalar_3.clone()).expect("scalar must register");
1043        let node = Node::try_pair_list(
1044            CurvePrecompile::msm_tag(),
1045            vec![(generator.digest(), scalar_2.digest()), (generator.digest(), scalar_3.digest())],
1046        )
1047        .expect("tag is curve-owned");
1048
1049        assert_invalid_payload(evaluate(&mut state, node));
1050    }
1051
1052    #[test]
1053    fn msm_rejects_zero_scalar_terms() {
1054        let mut state = state();
1055        let curve = CurveId::Secp256k1;
1056        let generator = CurvePrecompile::generator_node(curve);
1057        let zero = UintPrecompile::value_node(curve.scalar_domain(), [0; 8]);
1058        state.register(zero.clone()).expect("scalar must register");
1059        let node = Node::try_pair_list(
1060            CurvePrecompile::msm_tag(),
1061            vec![(generator.digest(), zero.digest())],
1062        )
1063        .expect("tag is curve-owned");
1064
1065        assert_invalid_payload(evaluate(&mut state, node));
1066    }
1067
1068    #[test]
1069    fn msm_rejects_wrong_scalar_domain() {
1070        let mut state = state();
1071        let curve = CurveId::Secp256k1;
1072        let generator = CurvePrecompile::generator_node(curve);
1073        let scalar = UintPrecompile::value_node(curve.base_domain(), [2, 0, 0, 0, 0, 0, 0, 0]);
1074        state.register(scalar.clone()).expect("scalar must register");
1075        let node = Node::try_pair_list(
1076            CurvePrecompile::msm_tag(),
1077            vec![(generator.digest(), scalar.digest())],
1078        )
1079        .expect("tag is curve-owned");
1080
1081        assert_invalid_payload(evaluate(&mut state, node));
1082    }
1083
1084    #[test]
1085    fn msm_rejects_non_curve_point_node() {
1086        let mut state = state();
1087        let curve = CurveId::Secp256k1;
1088        let point = UintPrecompile::value_node(curve.base_domain(), [1, 0, 0, 0, 0, 0, 0, 0]);
1089        let scalar = UintPrecompile::value_node(curve.scalar_domain(), [2, 0, 0, 0, 0, 0, 0, 0]);
1090        state.register(point.clone()).expect("point placeholder must register");
1091        state.register(scalar.clone()).expect("scalar must register");
1092        let node = Node::try_pair_list(
1093            CurvePrecompile::msm_tag(),
1094            vec![(point.digest(), scalar.digest())],
1095        )
1096        .expect("tag is curve-owned");
1097
1098        assert_invalid_payload(evaluate(&mut state, node));
1099    }
1100
1101    #[test]
1102    fn mixed_true_payload_is_invalid() {
1103        let identity = CurvePrecompile::identity_node(CurveId::Secp256k1);
1104        let node = CurvePrecompile::affine_node_from_digests(
1105            CurveId::Secp256k1,
1106            TRUE_DIGEST,
1107            identity.digest(),
1108        );
1109        let mut state = state();
1110
1111        assert_invalid_payload(evaluate(&mut state, node));
1112    }
1113
1114    #[test]
1115    fn affine_value_rejects_scalar_field_coordinate_nodes() {
1116        let curve = CurveId::Secp256k1;
1117        let x = UintPrecompile::value_node(UintDomain::K1Scalar, Secp256k1::GENERATOR_X);
1118        let y = UintPrecompile::value_node(UintDomain::K1Scalar, Secp256k1::GENERATOR_Y);
1119        let point = CurvePrecompile::affine_node_from_digests(curve, x.digest(), y.digest());
1120        let mut state = state();
1121        state.register(x).expect("x coordinate must register");
1122        state.register(y).expect("y coordinate must register");
1123
1124        assert_invalid_payload(evaluate(&mut state, point));
1125    }
1126
1127    #[test]
1128    fn off_curve_affine_value_fails_at_registration() {
1129        let curve = CurveId::Secp256k1;
1130        let x = UintPrecompile::value_node(curve.base_domain(), [1, 0, 0, 0, 0, 0, 0, 0]);
1131        let y = UintPrecompile::value_node(curve.base_domain(), [1, 0, 0, 0, 0, 0, 0, 0]);
1132        let point = CurvePrecompile::affine_node_from_digests(curve, x.digest(), y.digest());
1133        let mut state = state();
1134        state.register(x).expect("x coordinate must register");
1135        state.register(y).expect("y coordinate must register");
1136
1137        assert_invalid_payload(state.register(point));
1138    }
1139
1140    #[test]
1141    fn mul_scalar_two_generator_matches_hardcoded_known_answers() {
1142        const K1_2G_X: Limbs = [
1143            0x5c70_9ee5,
1144            0xabac_09b9,
1145            0x8cef_3ca7,
1146            0x5c77_8e4b,
1147            0x95c0_7cd8,
1148            0x3045_406e,
1149            0x41ed_7d6d,
1150            0xc604_7f94,
1151        ];
1152        const K1_2G_Y: Limbs = [
1153            0x50cf_e52a,
1154            0x2364_31a9,
1155            0x3266_d0e1,
1156            0xf7f6_3265,
1157            0x466c_eaee,
1158            0xa3c5_8419,
1159            0xa63d_c339,
1160            0x1ae1_68fe,
1161        ];
1162        let curve = CurveId::Secp256k1;
1163        let (x, y) =
1164            affine_limbs(curve.mul_scalar(curve.generator(), [2, 0, 0, 0, 0, 0, 0, 0]).unwrap());
1165        assert_eq!((x, y), (K1_2G_X, K1_2G_Y));
1166    }
1167
1168    #[test]
1169    fn fixed_curve_public_pointers_and_generators_validate() {
1170        for curve in CurveId::ALL {
1171            assert_eq!(CurveId::from_group_ptr(curve.group_ptr()), Some(curve));
1172            assert!(curve.is_on_curve(&curve.generator()));
1173            assert!(curve.scalar_domain().is_prime_field());
1174        }
1175        assert_eq!(CurveId::from_group_ptr(99), None);
1176        assert!(!K1Scalar::is_canonical(&K1Scalar::MODULUS));
1177
1178        assert_eq!(
1179            curve_coefficients(),
1180            [
1181                CurveCoefficient {
1182                    ptr: CurveId::Secp256k1.a_ptr(),
1183                    bound_ptr: CurveId::Secp256k1.base_domain().bound_ptr(),
1184                    value: CurveId::Secp256k1.a_value(),
1185                },
1186                CurveCoefficient {
1187                    ptr: CurveId::Secp256k1.b_ptr(),
1188                    bound_ptr: CurveId::Secp256k1.base_domain().bound_ptr(),
1189                    value: CurveId::Secp256k1.b_value(),
1190                },
1191            ],
1192        );
1193    }
1194
1195    #[test]
1196    fn mul_scalar_matches_affine_double_and_add_reference() {
1197        // Faithful affine double-and-add reference, built only from the affine point addition law.
1198        // This is the pre-optimization algorithm and is independent of the projective code path.
1199        fn affine_mul_scalar(curve: CurveId, point: CurvePoint, scalar: Limbs) -> CurvePoint {
1200            let Some(highest_limb) = scalar.iter().rposition(|&limb| limb != 0) else {
1201                return CurvePoint::Identity;
1202            };
1203            let highest_bit =
1204                highest_limb * 32 + (u32::BITS - 1 - scalar[highest_limb].leading_zeros()) as usize;
1205
1206            let mut acc = CurvePoint::Identity;
1207            let mut base = point;
1208            for bit_index in 0..=highest_bit {
1209                if ((scalar[bit_index / 32] >> (bit_index % 32)) & 1) == 1 {
1210                    acc = curve.add(acc, base).expect("affine add");
1211                }
1212                if bit_index != highest_bit {
1213                    base = curve.add(base, base).expect("affine double");
1214                }
1215            }
1216            acc
1217        }
1218
1219        fn next_u32(state: &mut u64) -> u32 {
1220            *state = state
1221                .wrapping_mul(6_364_136_223_846_793_005)
1222                .wrapping_add(1_442_695_040_888_963_407);
1223            (*state >> 32) as u32
1224        }
1225
1226        let mut state: u64 = 0xdead_beef_cafe_f00d;
1227        for curve in CurveId::ALL {
1228            let modulus = match curve {
1229                CurveId::Secp256k1 => K1Scalar::MODULUS,
1230            };
1231            let minus_one = match curve {
1232                CurveId::Secp256k1 => K1Scalar::minus_one(),
1233            };
1234
1235            // Valid on-curve base points: generator, [2^128]generator, and their sum. The
1236            // [2^128]generator point is built through the affine reference so it stays independent
1237            // of the projective `mul_scalar` under test.
1238            let generator = curve.generator();
1239            let generator_128 = affine_mul_scalar(curve, generator, [0, 0, 0, 0, 1, 0, 0, 0]);
1240            let sum = curve.add(generator, generator_128).expect("valid point");
1241            let points = [generator, generator_128, sum];
1242
1243            // Edge scalars exercise the empty, minimal, and maximal-canonical cases.
1244            let edges = [ZERO_LIMBS, [1, 0, 0, 0, 0, 0, 0, 0], [2, 0, 0, 0, 0, 0, 0, 0], minus_one];
1245
1246            for point in points {
1247                for scalar in edges {
1248                    assert_eq!(
1249                        curve.mul_scalar(point, scalar).expect("projective mul_scalar"),
1250                        affine_mul_scalar(curve, point, scalar),
1251                        "{curve:?} mul_scalar edge mismatch for scalar {scalar:?}",
1252                    );
1253                }
1254
1255                for _ in 0..40 {
1256                    let mut scalar = [0u32; 8];
1257                    for limb in scalar.iter_mut() {
1258                        *limb = next_u32(&mut state);
1259                    }
1260                    // Force a canonical scalar: a strictly smaller top limb implies value <
1261                    // modulus.
1262                    scalar[7] %= modulus[7];
1263                    assert!(curve.scalar_domain().is_canonical(&scalar));
1264
1265                    assert_eq!(
1266                        curve.mul_scalar(point, scalar).expect("projective mul_scalar"),
1267                        affine_mul_scalar(curve, point, scalar),
1268                        "{curve:?} mul_scalar random mismatch for scalar {scalar:?}",
1269                    );
1270                }
1271            }
1272        }
1273    }
1274}