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