UltrafastSecp256k1 3.68.0
Ultra high-performance secp256k1 elliptic curve cryptography library
Loading...
Searching...
No Matches
point.hpp
Go to the documentation of this file.
1#ifndef C870F4A3_192C_4B96_9AE6_497D1885C5D9
2#define C870F4A3_192C_4B96_9AE6_497D1885C5D9
3
4#include <array>
5#include <cstddef>
6#include <cstdint>
7#include <memory>
8#include <string>
9#include <utility>
10#include <vector>
11#include "field.hpp"
12#include "scalar.hpp"
13
14// On 5x52-capable platforms, Point stores FieldElement52 internally
15// for zero-conversion-overhead point arithmetic.
16// FE52 native storage: only on 64-bit platforms with __int128
17// Excluded on Emscripten/WASM: wasm32 emulates __int128 via compiler intrinsics,
18// which is correct but gives no speed benefit over 4x64 FieldElement.
19// The 52-bit dual_scalar_mul_gen_point also builds huge static tables (8192 entries)
20// that are unnecessary for WASM targets.
21#if defined(__SIZEOF_INT128__) && !defined(SECP256K1_PLATFORM_ESP32) && !defined(SECP256K1_PLATFORM_STM32) && !defined(__EMSCRIPTEN__)
22 #ifndef SECP256K1_FAST_52BIT
23 #define SECP256K1_FAST_52BIT 1
24 #endif
25 #include "field_52.hpp"
26#endif
27
28namespace secp256k1::fast {
29
30// Platform-optimal default GLV window width for k*P (scalar_mul_with_plan).
31//
32// Larger window = fewer point additions in the wNAF loop, but larger precompute
33// table (2^(w-2) entries, each requiring a mixed add + z-ratio tracking).
34//
35// Tradeoff by platform (BIP-352 pipeline benchmark, 10K ops, median):
36// w=4: table=4 entries, ~33 adds per 128-bit GLV half-scalar
37// w=5: table=8 entries, ~26 adds per 128-bit GLV half-scalar
38// w=6: table=16 entries, ~21 adds (diminishing returns, precompute dominates)
39//
40// On in-order / narrow OoO cores (RISC-V U74, ARM Cortex-A55) where point_add
41// is expensive relative to precompute, w=5 saves more than it costs.
42// On wide OoO x86-64 with fast MULX, the tradeoff is roughly neutral for
43// single k*P but w=5 still wins the full pipeline.
44//
45// Override at call site: KPlan::from_scalar(k, 6) for batch-heavy workloads
46// where precompute is amortized across many points.
47#if defined(SECP256K1_GLV_WINDOW_WIDTH)
48 // CMake override: -DSECP256K1_GLV_WINDOW_WIDTH=6
49 // Or direct compiler flag: -DSECP256K1_GLV_WINDOW_WIDTH=6
50 static_assert(SECP256K1_GLV_WINDOW_WIDTH >= 4 && SECP256K1_GLV_WINDOW_WIDTH <= 7,
51 "SECP256K1_GLV_WINDOW_WIDTH must be in [4,7]");
52 inline constexpr uint8_t kDefaultGlvWindow = SECP256K1_GLV_WINDOW_WIDTH;
53#elif defined(__riscv) || defined(__aarch64__) || defined(_M_ARM64)
54 inline constexpr uint8_t kDefaultGlvWindow = 5;
55#elif defined(__x86_64__) || defined(_M_X64)
56 inline constexpr uint8_t kDefaultGlvWindow = 5;
57#else
58 inline constexpr uint8_t kDefaultGlvWindow = 4; // ESP32, WASM, unknown
59#endif
60
61// Fixed K x Variable Q optimization plan
62// Caches all K-dependent work: GLV decomposition + wNAF computation
63// Use this when you need to multiply many different points Q by the same scalar K
64// Maximum wNAF buffer length for a 256-bit scalar: 256 bits + 1 extra digit + 3 padding
65constexpr std::size_t kWnafBufLen = 260;
66
67struct KPlan {
68 uint8_t window_width; // wNAF window size (see kDefaultGlvWindow)
69 Scalar k1; // Decomposed scalar k1
70 Scalar k2; // Decomposed scalar k2
71 // wNAF digits stored in fixed-size stack buffers — no heap allocation per plan
72 std::array<int32_t, kWnafBufLen> wnaf1{};
73 std::size_t wnaf1_len{0};
74 std::array<int32_t, kWnafBufLen> wnaf2{};
75 std::size_t wnaf2_len{0};
76 bool neg1; // Sign flag for k1
77 bool neg2; // Sign flag for k2
78
79 // Factory: Create plan from scalar K
80 // w: wNAF window width (default: platform-optimal kDefaultGlvWindow)
81 static KPlan from_scalar(const Scalar& k, uint8_t w = kDefaultGlvWindow);
82};
83
84class Point {
85public:
87
88 static Point generator();
89 static Point infinity();
90 static Point from_affine(const FieldElement& x, const FieldElement& y);
91
92 // Developer-friendly: Create from hex strings (64 hex chars each)
93 // Example: Point::from_hex("09af57f4...", "0947e4f9...")
94 static Point from_hex(const std::string& x_hex, const std::string& y_hex);
95
96 FieldElement x() const;
97 FieldElement y() const;
98 bool is_infinity() const noexcept { return infinity_; }
99 bool is_gen() const noexcept { return is_generator_; }
100
101 // Split x-coordinate helpers for split-keys database format
102 // x_first_half(): returns first 16 bytes of x-coordinate
103 // x_second_half(): returns last 16 bytes of x-coordinate
104 std::array<uint8_t, 16> x_first_half() const;
105 std::array<uint8_t, 16> x_second_half() const;
106
107 // Direct access to Jacobian coordinates (for batch processing)
108#if defined(SECP256K1_FAST_52BIT)
109 FieldElement X() const noexcept;
110 FieldElement Y() const noexcept;
111 FieldElement z() const noexcept;
112 // Direct access to 5x52 internals (for hot paths)
113 const FieldElement52& X52() const noexcept { return x_; }
114 const FieldElement52& Y52() const noexcept { return y_; }
115 const FieldElement52& Z52() const noexcept { return z_; }
116#else
117 const FieldElement& X() const noexcept { return x_; }
118 const FieldElement& Y() const noexcept { return y_; }
119 const FieldElement& z() const noexcept { return z_; }
120#endif
121
122 Point add(const Point& other) const;
123 Point dbl() const;
124 Point scalar_mul(const Scalar& scalar) const;
125
126 // Optimized: Q * K where K is precomputed constant
127 // This will use GLV decomposition and precomputed tables
129
130 // Optimized: Q * K with pre-decomposed K (k1, k2, signs)
131 // Use this when K decomposition is done once at startup
132 // Runtime only computes: Q*k1 + phi(Q)*k2
134 bool neg1, bool neg2) const;
135
136 // Optimized: Q * K with precomputed wNAF digits
137 // This is the fastest version - all K-related work is done at compile time
138 // Runtime only does: table generation + interleaved addition
139 Point scalar_mul_precomputed_wnaf(const std::vector<int32_t>& wnaf1,
140 const std::vector<int32_t>& wnaf2,
141 bool neg1, bool neg2) const;
142 // Raw-pointer overload — no heap access, used by KPlan hot path
143 Point scalar_mul_precomputed_wnaf(const int32_t* wnaf1, std::size_t len1,
144 const int32_t* wnaf2, std::size_t len2,
145 bool neg1, bool neg2) const;
146
147 // Fixed K x Variable Q: Use precomputed KPlan for maximum speed
148 // All K-dependent work (GLV + wNAF) is cached in the plan
149 // Runtime only: phi(Q), table generation, Shamir's trick
150 Point scalar_mul_with_plan(const KPlan& plan) const;
151
152 // Jacobian output variant: identical to scalar_mul() but skips the final
153 // normalize(). Result has z_one_=false (Jacobian coordinates, Z≠1).
154 // Use batch_normalize / batch_to_compressed / batch_x_only_bytes to convert
155 // N results to affine with ONE shared field inversion (Montgomery's trick).
156 // Saves ~500 ns/call when N points are processed together.
157 // Note: scalar_mul_with_plan() already returns Jacobian — no _jacobian variant needed.
158 Point scalar_mul_jacobian(const Scalar& scalar) const;
159
160 // Negation: returns the opposite point on the curve
161 Point negate() const; // -(x, y) = (x, -y)
162
163 // Fast increment/decrement by generator (optimized with precomputed -G)
164 Point next() const; // this + G (returns new Point)
165 Point prev() const; // this - G (returns new Point)
166
167 // In-place mutable versions (modify this object directly)
168 // In-place variants: modify this directly (same perf as immutable next/prev)
169 void next_inplace(); // this += G (modifies this)
170 void prev_inplace(); // this -= G (modifies this)
171 void add_inplace(const Point& other); // this += other (modifies this, no allocation)
172 void sub_inplace(const Point& other); // this -= other (modifies this, no allocation)
173 // Branchless mixed-add against affine point (z=1): avoids runtime checks
174 void add_mixed_inplace(const FieldElement& ax, const FieldElement& ay);
175 void sub_mixed_inplace(const FieldElement& ax, const FieldElement& ay);
176#if defined(SECP256K1_FAST_52BIT)
177 // FE52-native mixed-add: avoids FE52->FE->FE52 roundtrip in hot loops.
178 // Used by effective-affine Strauss MSM where precomp is stored as FE52.
179 void add_mixed52_inplace(const FieldElement52& ax, const FieldElement52& ay);
180#endif
181 void dbl_inplace(); // this = 2*this (modifies this, no allocation)
182 void negate_inplace(); // this = -this (modifies this, no allocation)
183 // Optimized repeated addition by a fixed affine point (z=1)
185
186 // Y-parity check (single inversion, no full serialization)
187 bool has_even_y() const;
188
189 // Combined: returns (x_bytes, y_is_odd) with a single field inversion
190 std::pair<std::array<uint8_t, 32>, bool> x_bytes_and_parity() const;
191
192 // Fast x-only: 32-byte big-endian x-coordinate (no Y recovery).
193 // Saves one multiply vs x_bytes_and_parity() by skipping Z^(-3)*Y.
194 // Use when only x is needed (e.g. BIP-352 SHA-256 input, BIP-340 x-only).
195 std::array<uint8_t, 32> x_only_bytes() const;
196
197 // Batch scalar mul: fixed K (from KPlan) × N variable points, N independent results.
198 // All points share the same wNAF (from plan), so:
199 // (1) Tables for all N points built and batch-inverted together (1 field_inv per chunk).
200 // (2) Shared wNAF loop processes chunk_size accumulators in lockstep.
201 // (3) Results stored as lazy-Jacobian Points — pass to batch_to_compressed / batch_x_only_bytes.
202 // Chunked internally (chunk_size ≈ 2048) to keep the working set in L2/L3 cache.
203 // Fallback to per-point scalar_mul_with_plan on non-FE52 or degenerate inputs.
204 // Expected speedup over N × scalar_mul_with_plan: ~15–25% on Stage 1 latency.
205 static void batch_scalar_mul_fixed_k(const KPlan& plan,
206 const Point* pts,
207 size_t n,
208 Point* results);
209
210 // 4× interleaved variant: process 4 points per inner loop iteration sharing
211 // the same wNAF digit sequence. Gives ILP speedup via 4 independent add chains.
212 // n must be a multiple of 4; caller pads if needed.
213 // Falls back to per-point scalar_mul_with_plan on degenerate inputs.
214 static void batch_scalar_mul_fixed_k_4x(const KPlan& plan,
215 const Point* pts,
216 size_t n,
217 Point* results);
218
219 // ---- Precomputed scan-table API ----------------------------------------
220 // Splits the work into a ONE-TIME setup phase and a fast hot-loop phase.
221 //
222 // Setup (once at startup or whenever pts change):
223 // auto cache = Point::batch_scan_precompute(plan, pts, n);
224 //
225 // Hot loop (called repeatedly, no table building inside):
226 // Point::batch_scan_run(cache, plan, results, n);
227 //
228 // Memory: n × table_size × 2 × sizeof(AffinePoint52).
229 // For n=100K, w=5: ~128 MB.
230 // -------------------------------------------------------------------------
231 using PointScanCacheHandle = std::shared_ptr<void>;
232
233 // Build per-point GLV52 tables for all N points. Thread-safe (read-only after).
235 const Point* pts,
236 size_t n);
237
238 // Run the wNAF digit loop using pre-built tables — no table building here.
239 // cache must have been produced by batch_scan_precompute with the same plan.
240 // cache_offset: starting index into the cache (allows parallel slicing).
241 // Processes n points from cache[cache_offset..cache_offset+n-1].
242 static void batch_scan_run(const PointScanCacheHandle& cache,
243 const KPlan& plan,
244 size_t cache_offset,
245 Point* results,
246 size_t n);
247
248 // Lockstep variant: outer loop over 128 b_scan digit positions, inner over n points.
249 // Each digit loaded ONCE; zero-digit positions skip the inner loop entirely.
250 // chunk_size controls working-set fit in L2 (default 256 ≈ 327 KB tables + 30 KB acc).
252 const KPlan& plan,
253 size_t cache_offset,
254 Point* results,
255 size_t n,
256 size_t chunk_size = 256);
257
258 // Persist cache to disk (atomic write via tmp+rename).
259 // Returns true on success. File format includes magic/version for validation.
260 static bool batch_scan_save(const PointScanCacheHandle& cache,
261 const std::string& path);
262
263 // Load cache from disk. Returns null handle on failure (missing/corrupt file).
264 static PointScanCacheHandle batch_scan_load(const std::string& path);
265
266 // Convenience: load from path if it exists and is valid; otherwise build from pts,
267 // save to path, and return the cache. Single call replaces the if/else pattern.
269 const KPlan& plan, const Point* pts, size_t n,
270 const std::string& cache_path);
271
272 // Batch normalize: convert N Jacobian points to affine with ONE inversion
273 // via Montgomery's trick. Cost: 1 inversion + 3(N-1) multiplications.
274 // For N=2048: ~9.5 ns/point vs ~1000 ns/point individually.
275 // out_x, out_y: output affine coordinates (caller-owned, size >= n).
276 // Skips infinity points (leaves output zero-filled).
277 static void batch_normalize(const Point* points, size_t n,
278 FieldElement* out_x, FieldElement* out_y);
279
280 // Batch to_compressed: serialize N Jacobian points using ONE inversion.
281 // out: caller-owned array of 33-byte compressed pubkeys, size >= n.
282 static void batch_to_compressed(const Point* points, size_t n,
283 std::array<uint8_t, 33>* out);
284
285 // Batch x_only_bytes: extract N x-coordinates using ONE inversion.
286 // out: caller-owned array of 32-byte x coords, size >= n.
287 static void batch_x_only_bytes(const Point* points, size_t n,
288 std::array<uint8_t, 32>* out);
289
290 // Normalize: convert Jacobian -> affine (Z=1) with ONE field inversion.
291 // After this call, all serialization methods become O(1) byte copies.
292 // Called automatically by scalar_mul/generator_mul/dual_scalar_mul.
293 void normalize();
294 bool is_normalized() const noexcept { return z_one_; }
295
296 // Dual scalar multiplication: a*G + b*P (4-stream GLV Shamir)
297 // Much faster than separate generator_mul(a) + scalar_mul(b) + add
298 static Point dual_scalar_mul_gen_point(const Scalar& a, const Scalar& b, const Point& P);
299
300 std::array<std::uint8_t, 33> to_compressed() const;
301 std::array<std::uint8_t, 65> to_uncompressed() const;
302
303#if defined(SECP256K1_FAST_52BIT)
304 FieldElement x_raw() const noexcept;
305 FieldElement y_raw() const noexcept;
306 FieldElement z_raw() const noexcept;
307#else
308 const FieldElement& x_raw() const noexcept { return x_; }
309 const FieldElement& y_raw() const noexcept { return y_; }
310 const FieldElement& z_raw() const noexcept { return z_; }
311#endif
312
314#if defined(SECP256K1_FAST_52BIT)
315 // Zero-conversion factory: constructs Point directly from FE52 Jacobian coords
316 static Point from_jacobian52(const FieldElement52& x, const FieldElement52& y, const FieldElement52& z, bool infinity);
317 // Zero-conversion affine construction: (x, y, z=1) directly in FE52
318 static Point from_affine52(const FieldElement52& x, const FieldElement52& y);
319#endif
320
321private:
322 Point(const FieldElement& x, const FieldElement& y, const FieldElement& z, bool infinity);
323#if defined(SECP256K1_FAST_52BIT)
324 // Zero-conversion constructor: directly initializes FE52 members
325 Point(const FieldElement52& x, const FieldElement52& y, const FieldElement52& z, bool infinity, bool is_gen);
326
327 // Convert z_ (FE52) -> normalized FieldElement + check for zero.
328 // Returns true if z is nonzero (normal case); false if z is zero.
329 // On true: out_z_fe contains the normalized 4x64 FieldElement.
330 // Used by x(), y(), to_compressed(), to_uncompressed(), has_even_y(),
331 // x_bytes_and_parity() to avoid duplicating the defensive Z=0 guard.
332 bool z_fe_nonzero(FieldElement& out_z_fe) const noexcept;
333#endif
334
335#if defined(SECP256K1_FAST_52BIT)
339#else
340 FieldElement x_;
341 FieldElement y_;
342 FieldElement z_;
343#endif
344 bool infinity_;
345 bool is_generator_;
346 bool z_one_ = false; // true when Z == 1 (point is affine-normalized)
347};
348
349// Self-test: Verify arithmetic correctness with known test vectors
350// Returns true if all tests pass, false otherwise
351// Run this after any code changes to ensure math is correct!
352// Set verbose=true to see detailed output for each test
353bool Selftest(bool verbose = false);
354
355} // namespace secp256k1::fast
356
357
358#endif /* C870F4A3_192C_4B96_9AE6_497D1885C5D9 */
static Point from_hex(const std::string &x_hex, const std::string &y_hex)
static PointScanCacheHandle batch_scan_precompute(const KPlan &plan, const Point *pts, size_t n)
static Point from_affine(const FieldElement &x, const FieldElement &y)
static Point dual_scalar_mul_gen_point(const Scalar &a, const Scalar &b, const Point &P)
const FieldElement & z_raw() const noexcept
Definition point.hpp:310
static Point from_jacobian_coords(const FieldElement &x, const FieldElement &y, const FieldElement &z, bool infinity)
static void batch_x_only_bytes(const Point *points, size_t n, std::array< uint8_t, 32 > *out)
std::array< std::uint8_t, 65 > to_uncompressed() const
void add_affine_constant_inplace(const FieldElement &ax, const FieldElement &ay)
Point negate() const
const FieldElement & Y() const noexcept
Definition point.hpp:118
std::pair< std::array< uint8_t, 32 >, bool > x_bytes_and_parity() const
static void batch_to_compressed(const Point *points, size_t n, std::array< uint8_t, 33 > *out)
Point scalar_mul_jacobian(const Scalar &scalar) const
static PointScanCacheHandle batch_scan_load(const std::string &path)
bool is_gen() const noexcept
Definition point.hpp:99
std::array< uint8_t, 32 > x_only_bytes() const
std::shared_ptr< void > PointScanCacheHandle
Definition point.hpp:231
static void batch_normalize(const Point *points, size_t n, FieldElement *out_x, FieldElement *out_y)
Point scalar_mul(const Scalar &scalar) const
static void batch_scan_run_lockstep(const PointScanCacheHandle &cache, const KPlan &plan, size_t cache_offset, Point *results, size_t n, size_t chunk_size=256)
void sub_mixed_inplace(const FieldElement &ax, const FieldElement &ay)
void add_inplace(const Point &other)
bool is_infinity() const noexcept
Definition point.hpp:98
static bool batch_scan_save(const PointScanCacheHandle &cache, const std::string &path)
Point scalar_mul_with_plan(const KPlan &plan) const
bool is_normalized() const noexcept
Definition point.hpp:294
FieldElement x() const
FieldElement y() const
Point add(const Point &other) const
static PointScanCacheHandle batch_scan_precompute_or_load(const KPlan &plan, const Point *pts, size_t n, const std::string &cache_path)
const FieldElement & z() const noexcept
Definition point.hpp:119
const FieldElement & y_raw() const noexcept
Definition point.hpp:309
static void batch_scan_run(const PointScanCacheHandle &cache, const KPlan &plan, size_t cache_offset, Point *results, size_t n)
void add_mixed_inplace(const FieldElement &ax, const FieldElement &ay)
bool has_even_y() const
static void batch_scalar_mul_fixed_k(const KPlan &plan, const Point *pts, size_t n, Point *results)
static Point generator()
Point scalar_mul_precomputed_wnaf(const std::vector< int32_t > &wnaf1, const std::vector< int32_t > &wnaf2, bool neg1, bool neg2) const
Point scalar_mul_precomputed_wnaf(const int32_t *wnaf1, std::size_t len1, const int32_t *wnaf2, std::size_t len2, bool neg1, bool neg2) const
const FieldElement & x_raw() const noexcept
Definition point.hpp:308
Point scalar_mul_predecomposed(const Scalar &k1, const Scalar &k2, bool neg1, bool neg2) const
void sub_inplace(const Point &other)
Point scalar_mul_precomputed_k(const Scalar &k) const
const FieldElement & X() const noexcept
Definition point.hpp:117
static Point infinity()
std::array< uint8_t, 16 > x_first_half() const
static void batch_scalar_mul_fixed_k_4x(const KPlan &plan, const Point *pts, size_t n, Point *results)
std::array< std::uint8_t, 33 > to_compressed() const
std::array< uint8_t, 16 > x_second_half() const
bool Selftest(bool verbose)
constexpr std::size_t kWnafBufLen
Definition point.hpp:65
constexpr uint8_t kDefaultGlvWindow
Definition point.hpp:58
std::array< int32_t, kWnafBufLen > wnaf2
Definition point.hpp:74
static KPlan from_scalar(const Scalar &k, uint8_t w=kDefaultGlvWindow)
std::array< int32_t, kWnafBufLen > wnaf1
Definition point.hpp:72
std::size_t wnaf1_len
Definition point.hpp:73
std::size_t wnaf2_len
Definition point.hpp:75