Elliptic Curves
lib/ec/elliptic_curve.h is the curve layer that the ECDSA verification circuit consumes. It provides a curve-agnostic EllipticCurve<Field, W, kN> template plus two concrete instantiations, P256 and P256k1. Points live in standard projective (homogeneous) coordinates, scalar multiplication uses Bos-Coster when combining multiple scalars, and the concrete curves ship with pre-computed generators and orders.
When to reach for it
- You are preparing an ECDSA witness off-circuit — hashing the message, computing
rands^{-1}, and packing(g \cdot e + pk \cdot r + (r_x, r_y) \cdot (-s))for the circuit to check. - You are adding support for a new curve: this page shows what the
EllipticCurvetemplate needs. - You are debugging a signature mismatch and want to know whether the fault is in coordinate conversion, scalar reduction, or the circuit itself — this page draws the off-circuit/on-circuit boundary.
Design overview
EllipticCurve<Field, W, kN> is parametrised by:
Field— the base field the curve’s coordinates live in (e.g.,Fp256).W— the scalar’s word count (usually the same as the base field, but the template keeps them separate so the order can sit in a different field).kN— the bit-length of the curve order, used to cap scalar-multiplication loops.
Point representation. Points are stored in standard projective (homogeneous) coordinates (x, y, z) representing the affine point (x/z, y/z). z = 0 is the point at infinity (canonical form (0, 1, 0)). Projective coordinates avoid field inversions during doubling and addition; the one inversion happens once at the end, in normalize().
Doubling and addition. Both routines have specialised fast paths:
doubleE/doubleEfhas ana = 0path (secp256k1) and ana = -3path (NIST curves) that both use the exception-free complete projective formulas from Renes-Costello-Batina 2016. The general fallback path uses the 1998 Cohen-Miyaji-Ono formulas.addE/addEflikewise hasa = 0anda = -3specialisations using Renes-Costello-Batina complete projective addition, plus the Cohen-Miyaji-Ono general path with short-circuits for the point-at-infinity and self-addition edges.
Scalar multiplication.
scalar_multf(p, scalar)— single-point right-to-left (LSB-first) binary method.scalar_multf(n, p[], scalar[])— multi-scalar multiplication onn(P_i, k_i)pairs using the Bos-Coster heap algorithm (called internally asbos_coster) forn >= 2;n == 1falls through to single-scalar,n == 0returns identity. This is whatverify_circuituses for the triple-scalar-mul identityg \cdot e + pk \cdot r + (r_x, r_y) \cdot (-s); it minimises the total bit count across the three scalars. Each iteration chooses between a Bernstein step and a double-and-add step — whichever reduces the largest scalar more — which caps the worst-case at plain binary multiplication.
API surface
EllipticCurve<Field, W, kN>
template <class Field, size_t W, size_t kN>
class EllipticCurve {
public:
using Field = Field_;
using Elt = typename Field::Elt;
using N = Nat<W>;
struct ECPoint { Elt x, y, z; }; // standard projective
// Curve constants
ECPoint generator() const;
ECPoint zero() const;
// Point equality and validity
bool equal(const ECPoint& p, const ECPoint& q) const;
bool is_on_curve(const ECPoint& p) const;
// Point ops
void addE(ECPoint& p, const ECPoint& q) const; // in-place
ECPoint addEf(ECPoint p, const ECPoint& q) const; // functional
void doubleE(ECPoint& p) const; // in-place
ECPoint doubleEf(ECPoint p) const; // functional
void normalize(ECPoint& p) const; // projective → affine, one inversion
// Scalar multiplication
ECPoint scalar_multf(const ECPoint& p, const N& scalar) const;
ECPoint scalar_multf(size_t n, ECPoint p[], N scalar[]) const;
};The curve order is not a method on EllipticCurve. It is exposed as an external constant in each curve-specific header: n256_order (p256.h) and n256k1_order (p256k1.h).
Concrete curves
P256(lib/ec/p256.h) — instantiation withFp256Baseas the base field and the NISTsecp256r1curve constants:a = -3,bis the standard secp256r1 coefficient, generator and order drawn from the NIST specification. The curve order is exposed as the external constantn256_order.P256k1(lib/ec/p256k1.h) — instantiation withFp256k1Baseand the Bitcoin/Ethereumsecp256k1constants:a = 0,b = 7, different generator and order. The curve order is exposed as the external constantn256k1_order.
Both ship pre-computed generator() and zero() helpers so constructor cost is essentially nil.
The off-circuit / on-circuit split
EllipticCurve runs in native C++ at witness-generation time. The in-circuit check lives in circuits/ecdsa/verify_circuit.h and uses the sumcheck-friendly arithmetic gates from circuits/logic/, not the projective-doubling formulas above. The bridge is the witness: the prover uses EllipticCurve::scalar_multf to compute the intermediate points, packs them as witnesses, and the circuit verifies the elliptic-curve relation as a small arithmetic predicate over the affine coordinates. See the ECDSA gadget for that side.
See it used
longfellow-zk/lib/ec/p256_test.cc— p256_test.cclongfellow-zk/lib/ec/p256k1_test.cc— p256k1_test.cclongfellow-zk/lib/circuits/ecdsa/verify_witness.h— callsscalar_multf(multi-scalar overload) to build the witness for the triple-scalar-mul identity.
Related
- Prime Fields — the base field the curve stands on.
- ECDSA Gadget — the in-circuit consumer.
- Verifying a Signed Document — end-to-end pattern.