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