secp256k1-native 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/secp256k1.rb ADDED
@@ -0,0 +1,626 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Single-letter parameter names (k, p, q, x, y, z, etc.) match standard
4
+ # elliptic-curve mathematical notation and the BSV TypeScript reference SDK
5
+ # this module is ported from. The whole-module length cop is disabled because
6
+ # the curve implementation (field arithmetic + Jacobian point ops + wNAF
7
+ # scalar multiplication + Point class) intentionally lives in one module to
8
+ # keep the secp256k1 surface coherent.
9
+ # rubocop:disable Naming/MethodParameterName, Metrics/ModuleLength
10
+
11
+ require_relative 'secp256k1/version'
12
+
13
+ # Pure Ruby secp256k1 elliptic curve implementation.
14
+ #
15
+ # Provides field arithmetic, point operations with Jacobian coordinates,
16
+ # and windowed-NAF scalar multiplication. Ported from the BSV TypeScript
17
+ # SDK reference implementation.
18
+ #
19
+ # All field operations work on plain Ruby +Integer+ values (arbitrary
20
+ # precision, C-backed in MRI). No external gems required.
21
+ module Secp256k1
22
+ # The secp256k1 field prime: p = 2^256 - 2^32 - 977
23
+ P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
24
+
25
+ # The curve order (number of points on the curve).
26
+ N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
27
+
28
+ # Half the curve order, used for low-S normalisation (BIP-62).
29
+ HALF_N = N >> 1
30
+
31
+ # Generator point x-coordinate.
32
+ GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
33
+
34
+ # Generator point y-coordinate.
35
+ GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
36
+
37
+ # (P + 1) / 4 — used for modular square root since P ≡ 3 (mod 4).
38
+ P_PLUS1_DIV4 = (P + 1) >> 2
39
+
40
+ # 256-bit mask for fast reduction.
41
+ MASK_256 = (1 << 256) - 1
42
+
43
+ module_function
44
+
45
+ # -------------------------------------------------------------------
46
+ # Byte conversion helpers
47
+ # -------------------------------------------------------------------
48
+
49
+ # Convert a big-endian binary string to an Integer.
50
+ #
51
+ # @param bytes [String] binary string (ASCII-8BIT)
52
+ # @return [Integer]
53
+ def bytes_to_int(bytes)
54
+ # C-backed hex route is the fastest pure-Ruby byte→integer path (10x faster than inject).
55
+ bytes.unpack1('H*').to_i(16)
56
+ end
57
+
58
+ # Convert an Integer to a fixed-length big-endian binary string.
59
+ #
60
+ # @param n [Integer] the integer to convert
61
+ # @param length [Integer] desired byte length (default 32)
62
+ # @return [String] binary string (ASCII-8BIT)
63
+ def int_to_bytes(n, length = 32)
64
+ raise ArgumentError, 'negative integer' if n.negative?
65
+
66
+ # C-backed hex route is the fastest pure-Ruby integer→byte path. Module deliberately avoids OpenSSL.
67
+ hex = n.to_s(16)
68
+ hex = "0#{hex}" if hex.length.odd?
69
+ raise ArgumentError, "integer too large for #{length} bytes" if hex.length > length * 2
70
+
71
+ hex = hex.rjust(length * 2, '0')
72
+ [hex].pack('H*')
73
+ end
74
+
75
+ # -------------------------------------------------------------------
76
+ # Field arithmetic (mod P)
77
+ # -------------------------------------------------------------------
78
+
79
+ # Fast reduction modulo the secp256k1 prime.
80
+ #
81
+ # Exploits the structure P = 2^256 - 2^32 - 977 to avoid generic
82
+ # modular division. Two folding passes plus a conditional subtraction.
83
+ #
84
+ # @param x [Integer] non-negative integer
85
+ # @return [Integer] x mod P, in range [0, P)
86
+ def fred(x)
87
+ # First fold
88
+ hi = x >> 256
89
+ x = (x & MASK_256) + (hi << 32) + (hi * 977)
90
+
91
+ # Second fold (hi <= 2^32 + 977, so one more pass suffices)
92
+ hi = x >> 256
93
+ x = (x & MASK_256) + (hi << 32) + (hi * 977)
94
+
95
+ # Final conditional subtraction
96
+ x >= P ? x - P : x
97
+ end
98
+
99
+ # Modular multiplication in the field.
100
+ def fmul(a, b)
101
+ fred(a * b)
102
+ end
103
+
104
+ # Modular squaring in the field.
105
+ def fsqr(a)
106
+ fred(a * a)
107
+ end
108
+
109
+ # Modular addition in the field.
110
+ def fadd(a, b)
111
+ fred(a + b)
112
+ end
113
+
114
+ # Modular subtraction in the field.
115
+ def fsub(a, b)
116
+ a >= b ? a - b : P - (b - a)
117
+ end
118
+
119
+ # Modular negation in the field.
120
+ def fneg(a)
121
+ a.zero? ? 0 : P - a
122
+ end
123
+
124
+ # Modular multiplicative inverse in the field (Fermat's little theorem).
125
+ #
126
+ # @param a [Integer] value to invert (must be non-zero mod P)
127
+ # @return [Integer] a^(P-2) mod P
128
+ # @raise [ArgumentError] if a is zero mod P
129
+ def finv(a)
130
+ raise ArgumentError, 'field inverse is undefined for zero' if (a % P).zero?
131
+
132
+ a.pow(P - 2, P)
133
+ end
134
+
135
+ # Modular square root in the field.
136
+ #
137
+ # Uses the identity sqrt(a) = a^((P+1)/4) mod P, valid because
138
+ # P ≡ 3 (mod 4). Returns +nil+ if +a+ is not a quadratic residue.
139
+ #
140
+ # @param a [Integer]
141
+ # @return [Integer, nil] the square root, or nil if none exists
142
+ def fsqrt(a)
143
+ r = a.pow(P_PLUS1_DIV4, P)
144
+ fsqr(r) == (a % P) ? r : nil
145
+ end
146
+
147
+ # -------------------------------------------------------------------
148
+ # Scalar arithmetic (mod N)
149
+ # -------------------------------------------------------------------
150
+
151
+ # Reduce modulo the curve order.
152
+ def scalar_mod(a)
153
+ r = a % N
154
+ r += N if r.negative?
155
+ r
156
+ end
157
+
158
+ # Scalar multiplicative inverse (Fermat).
159
+ #
160
+ # @raise [ArgumentError] if a is zero mod N
161
+ def scalar_inv(a)
162
+ raise ArgumentError, 'scalar inverse is undefined for zero' if (a % N).zero?
163
+
164
+ a.pow(N - 2, N)
165
+ end
166
+
167
+ # Scalar multiplication mod N.
168
+ def scalar_mul(a, b)
169
+ (a * b) % N
170
+ end
171
+
172
+ # Scalar addition mod N.
173
+ def scalar_add(a, b)
174
+ (a + b) % N
175
+ end
176
+
177
+ # -------------------------------------------------------------------
178
+ # Jacobian point operations (internal)
179
+ #
180
+ # Points are represented as [X, Y, Z] arrays of Integers.
181
+ # The point at infinity is [0, 1, 0].
182
+ # -------------------------------------------------------------------
183
+
184
+ # @!visibility private
185
+ JP_INFINITY = [0, 1, 0].freeze
186
+
187
+ # Double a Jacobian point.
188
+ #
189
+ # Formula from hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html
190
+ # (a=0 for secp256k1).
191
+ #
192
+ # @param p [Array(Integer, Integer, Integer)] Jacobian point [X, Y, Z]
193
+ # @return [Array(Integer, Integer, Integer)]
194
+ def jp_double(p)
195
+ x1, y1, z1 = p
196
+ return JP_INFINITY if y1.zero?
197
+
198
+ y1sq = fsqr(y1)
199
+ s = fmul(4, fmul(x1, y1sq))
200
+ m = fmul(3, fsqr(x1)) # a=0 for secp256k1
201
+ x3 = fsub(fsqr(m), fmul(2, s))
202
+ y3 = fsub(fmul(m, fsub(s, x3)), fmul(8, fsqr(y1sq)))
203
+ z3 = fmul(2, fmul(y1, z1))
204
+ [x3, y3, z3]
205
+ end
206
+
207
+ # Add two Jacobian points.
208
+ #
209
+ # @param p [Array] first Jacobian point
210
+ # @param q [Array] second Jacobian point
211
+ # @return [Array] resulting Jacobian point
212
+ def jp_add(p, q)
213
+ _px, _py, pz = p
214
+ _qx, _qy, qz = q
215
+ return q if pz.zero?
216
+ return p if qz.zero?
217
+
218
+ z1z1 = fsqr(pz)
219
+ z2z2 = fsqr(qz)
220
+ u1 = fmul(p[0], z2z2)
221
+ u2 = fmul(q[0], z1z1)
222
+ s1 = fmul(p[1], fmul(z2z2, qz))
223
+ s2 = fmul(q[1], fmul(z1z1, pz))
224
+
225
+ h = fsub(u2, u1)
226
+ r = fsub(s2, s1)
227
+
228
+ if h.zero?
229
+ return r.zero? ? jp_double(p) : JP_INFINITY
230
+ end
231
+
232
+ hh = fsqr(h)
233
+ hhh = fmul(h, hh)
234
+ v = fmul(u1, hh)
235
+
236
+ x3 = fsub(fsub(fsqr(r), hhh), fmul(2, v))
237
+ y3 = fsub(fmul(r, fsub(v, x3)), fmul(s1, hhh))
238
+ z3 = fmul(h, fmul(pz, qz))
239
+ [x3, y3, z3]
240
+ end
241
+
242
+ # Convert a Jacobian point to affine coordinates.
243
+ #
244
+ # @param jp [Array(Integer, Integer, Integer)]
245
+ # @return [Array(Integer, Integer)] affine [x, y], or nil for infinity
246
+ def jp_to_affine(jp)
247
+ _x, _y, z = jp
248
+ return nil if z.zero?
249
+
250
+ zinv = finv(z)
251
+ zinv2 = fsqr(zinv)
252
+ x = fmul(jp[0], zinv2)
253
+ y = fmul(jp[1], fmul(zinv2, zinv))
254
+ [x, y]
255
+ end
256
+
257
+ # -------------------------------------------------------------------
258
+ # Windowed-NAF scalar multiplication (variable-time, public scalars)
259
+ # -------------------------------------------------------------------
260
+
261
+ # @!visibility private
262
+ # Maximum number of entries kept in the wNAF precomputation cache.
263
+ # Bounds memory usage for long-running processes (e.g. servers).
264
+ WNAF_CACHE_MAX = 512
265
+
266
+ # @!visibility private
267
+ # Cache for precomputed wNAF tables, keyed by "window:x:y".
268
+ # Evicts oldest entry when the LRU limit is reached.
269
+ WNAF_TABLE_CACHE = {} # rubocop:disable Style/MutableConstant
270
+
271
+ # @!visibility private
272
+ # Multiply a point by a scalar using windowed-NAF.
273
+ #
274
+ # Variable-time algorithm — suitable only for public scalars (e.g.
275
+ # signature verification). Secret-scalar paths MUST use
276
+ # {scalar_multiply_ct} instead.
277
+ #
278
+ # Internal method — use {Point#mul} or {Point#mul_ct} instead.
279
+ # Exposed as a module function only so the nested Point class can
280
+ # call it; not part of the public API.
281
+ #
282
+ # @param k [Integer] the scalar (must be in [1, N))
283
+ # @param px [Integer] affine x-coordinate of the base point
284
+ # @param py [Integer] affine y-coordinate of the base point
285
+ # @param window [Integer] wNAF window size (default 5)
286
+ # @return [Array(Integer, Integer, Integer)] result as Jacobian point
287
+ def scalar_multiply_wnaf(k, px, py, window = 5)
288
+ return JP_INFINITY if k.zero?
289
+
290
+ cache_key = "#{window}:#{px.to_s(16)}:#{py.to_s(16)}"
291
+ tbl = WNAF_TABLE_CACHE[cache_key]
292
+
293
+ if tbl.nil?
294
+ # Evict the oldest entry when the cache is full (simple LRU).
295
+ WNAF_TABLE_CACHE.delete(WNAF_TABLE_CACHE.keys.first) if WNAF_TABLE_CACHE.size >= WNAF_CACHE_MAX
296
+
297
+ tbl_size = 1 << (window - 1) # e.g. w=5 -> 16 entries
298
+ tbl = Array.new(tbl_size)
299
+ tbl[0] = [px, py, 1]
300
+ two_p = jp_double(tbl[0])
301
+ 1.upto(tbl_size - 1) do |i|
302
+ tbl[i] = jp_add(tbl[i - 1], two_p)
303
+ end
304
+ WNAF_TABLE_CACHE[cache_key] = tbl
305
+ end
306
+
307
+ # Build wNAF representation
308
+ w_big = 1 << window
309
+ w_half = w_big >> 1
310
+ wnaf = []
311
+ k_tmp = k
312
+ while k_tmp.positive?
313
+ if k_tmp.odd?
314
+ z = k_tmp & (w_big - 1)
315
+ z -= w_big if z > w_half
316
+ wnaf << z
317
+ k_tmp -= z
318
+ else
319
+ wnaf << 0
320
+ end
321
+ k_tmp >>= 1
322
+ end
323
+
324
+ # Accumulate from MSB to LSB
325
+ q = JP_INFINITY
326
+ (wnaf.length - 1).downto(0) do |i|
327
+ q = jp_double(q)
328
+ di = wnaf[i]
329
+ next if di.zero?
330
+
331
+ idx = di.abs >> 1
332
+ addend = di.positive? ? tbl[idx] : jp_neg(tbl[idx])
333
+ q = jp_add(q, addend)
334
+ end
335
+ q
336
+ end
337
+
338
+ # -------------------------------------------------------------------
339
+ # Montgomery ladder scalar multiplication (constant-time, secret scalars)
340
+ # -------------------------------------------------------------------
341
+
342
+ # @!visibility private
343
+ # Multiply a point by a scalar using the Montgomery ladder.
344
+ #
345
+ # Executes a fixed number of iterations (256) with one +jp_double+
346
+ # and one +jp_add+ per iteration regardless of the scalar value.
347
+ # Use this for ALL secret-scalar paths (key generation, signing,
348
+ # ECDH, BIP-32 derivation).
349
+ #
350
+ # *Best-effort constant-time in interpreted Ruby.* The branch on
351
+ # +bit+ selects which register receives each operation, and both
352
+ # operations always execute. However, Ruby's interpreter, GC, and
353
+ # the early-return branches in +jp_add+/+jp_double+ (for infinity
354
+ # edge cases) mean true constant-time execution is not achievable
355
+ # without native code. This matches the ts-sdk's TypeScript
356
+ # implementation, which has the same structural properties. For
357
+ # production deployments requiring side-channel resistance beyond
358
+ # what an interpreted language can offer, use a native secp256k1
359
+ # library (e.g. libsecp256k1 via FFI).
360
+ #
361
+ # Internal method — use {Point#mul_ct} instead. Not part of the
362
+ # public API.
363
+ #
364
+ # @param k [Integer] secret scalar (must be in [1, N))
365
+ # @param px [Integer] affine x-coordinate of the base point
366
+ # @param py [Integer] affine y-coordinate of the base point
367
+ # @return [Array(Integer, Integer, Integer)] result as Jacobian point
368
+ def scalar_multiply_ct(k, px, py)
369
+ return JP_INFINITY if k.zero?
370
+
371
+ # r0 accumulates the result; r1 = r0 + base_point at all times.
372
+ r0 = JP_INFINITY
373
+ r1 = [px, py, 1]
374
+
375
+ 256.times do |i|
376
+ bit = (k >> (255 - i)) & 1
377
+ if bit.zero?
378
+ r1 = jp_add(r0, r1)
379
+ r0 = jp_double(r0)
380
+ else
381
+ r0 = jp_add(r0, r1)
382
+ r1 = jp_double(r1)
383
+ end
384
+ end
385
+
386
+ r0
387
+ end
388
+
389
+ # Negate a Jacobian point.
390
+ def jp_neg(p)
391
+ return p if p[2].zero?
392
+
393
+ [p[0], fneg(p[1]), p[2]]
394
+ end
395
+
396
+ # -------------------------------------------------------------------
397
+ # Point class
398
+ # -------------------------------------------------------------------
399
+
400
+ # An elliptic curve point on secp256k1.
401
+ #
402
+ # Stores affine coordinates (x, y) or represents the point at infinity.
403
+ # Scalar multiplication uses Jacobian coordinates internally with
404
+ # windowed-NAF for performance.
405
+ class Point
406
+ # @return [Integer, nil] x-coordinate (nil for infinity)
407
+ attr_reader :x
408
+
409
+ # @return [Integer, nil] y-coordinate (nil for infinity)
410
+ attr_reader :y
411
+
412
+ # @param x [Integer, nil] x-coordinate (nil for infinity)
413
+ # @param y [Integer, nil] y-coordinate (nil for infinity)
414
+ def initialize(x, y)
415
+ @x = x
416
+ @y = y
417
+ end
418
+
419
+ # The point at infinity (additive identity).
420
+ #
421
+ # @return [Point]
422
+ def self.infinity
423
+ new(nil, nil)
424
+ end
425
+
426
+ # The generator point G.
427
+ #
428
+ # @return [Point]
429
+ def self.generator
430
+ @generator ||= new(GX, GY)
431
+ end
432
+
433
+ # Deserialise a point from compressed (33 bytes) or uncompressed
434
+ # (65 bytes) SEC1 encoding.
435
+ #
436
+ # @param bytes [String] binary string
437
+ # @return [Point]
438
+ # @raise [ArgumentError] if the encoding is invalid or the point
439
+ # is not on the curve
440
+ def self.from_bytes(bytes)
441
+ bytes = bytes.b if bytes.encoding != Encoding::BINARY
442
+ prefix = bytes.getbyte(0)
443
+
444
+ case prefix
445
+ when 0x04 # Uncompressed
446
+ raise ArgumentError, 'invalid uncompressed point length' unless bytes.length == 65
447
+
448
+ x = Secp256k1.bytes_to_int(bytes[1, 32])
449
+ y = Secp256k1.bytes_to_int(bytes[33, 32])
450
+ raise ArgumentError, 'x coordinate out of field range' if x >= P
451
+ raise ArgumentError, 'y coordinate out of field range' if y >= P
452
+
453
+ pt = new(x, y)
454
+ raise ArgumentError, 'point is not on the curve' unless pt.on_curve?
455
+
456
+ pt
457
+ when 0x02, 0x03 # Compressed
458
+ raise ArgumentError, 'invalid compressed point length' unless bytes.length == 33
459
+
460
+ x = Secp256k1.bytes_to_int(bytes[1, 32])
461
+ raise ArgumentError, 'x coordinate out of field range' if x >= P
462
+
463
+ y_squared = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(x), x), 7)
464
+ y = Secp256k1.fsqrt(y_squared)
465
+ raise ArgumentError, 'invalid point: x not on curve' if y.nil?
466
+
467
+ # Ensure y-parity matches prefix
468
+ y = Secp256k1.fneg(y) if (y.odd? ? 0x03 : 0x02) != prefix
469
+
470
+ new(x, y)
471
+ else
472
+ raise ArgumentError, "unknown point prefix: 0x#{prefix.to_s(16).rjust(2, '0')}"
473
+ end
474
+ end
475
+
476
+ # Whether this is the point at infinity.
477
+ #
478
+ # @return [Boolean]
479
+ def infinity?
480
+ @x.nil?
481
+ end
482
+
483
+ # Whether this point lies on the secp256k1 curve (y² = x³ + 7).
484
+ #
485
+ # @return [Boolean]
486
+ def on_curve?
487
+ return true if infinity?
488
+
489
+ lhs = Secp256k1.fsqr(@y)
490
+ rhs = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(@x), @x), 7)
491
+ lhs == rhs
492
+ end
493
+
494
+ # Serialise the point in SEC1 format.
495
+ #
496
+ # @param format [:compressed, :uncompressed]
497
+ # @return [String] binary string (33 or 65 bytes)
498
+ # @raise [RuntimeError] if the point is at infinity
499
+ def to_octet_string(format = :compressed)
500
+ raise 'cannot serialise point at infinity' if infinity?
501
+
502
+ case format
503
+ when :compressed
504
+ prefix = @y.odd? ? "\x03".b : "\x02".b
505
+ prefix + Secp256k1.int_to_bytes(@x, 32)
506
+ when :uncompressed
507
+ "\x04".b + Secp256k1.int_to_bytes(@x, 32) + Secp256k1.int_to_bytes(@y, 32)
508
+ else
509
+ raise ArgumentError, "unknown format: #{format}"
510
+ end
511
+ end
512
+
513
+ # Scalar multiplication: self * scalar (variable-time, wNAF).
514
+ #
515
+ # Suitable for public scalars only (e.g. signature verification).
516
+ # For secret-scalar paths use {#mul_ct}.
517
+ #
518
+ # @param scalar [Integer] the scalar multiplier
519
+ # @return [Point] the resulting point
520
+ def mul(scalar)
521
+ return self.class.infinity if scalar.zero? || infinity?
522
+
523
+ scalar %= N
524
+ return self.class.infinity if scalar.zero?
525
+
526
+ jp = Secp256k1.scalar_multiply_wnaf(scalar, @x, @y)
527
+ affine = Secp256k1.jp_to_affine(jp)
528
+ return self.class.infinity if affine.nil?
529
+
530
+ self.class.new(affine[0], affine[1])
531
+ end
532
+
533
+ # Constant-time scalar multiplication: self * scalar (Montgomery ladder).
534
+ #
535
+ # Processes all 256 bits unconditionally so execution time does not
536
+ # depend on the scalar value. Use this for secret-scalar paths:
537
+ # key generation, signing, and ECDH shared-secret derivation.
538
+ #
539
+ # @param scalar [Integer] the secret scalar multiplier
540
+ # @return [Point] the resulting point
541
+ def mul_ct(scalar)
542
+ return self.class.infinity if scalar.zero? || infinity?
543
+
544
+ scalar %= N
545
+ return self.class.infinity if scalar.zero?
546
+
547
+ jp = Secp256k1.scalar_multiply_ct(scalar, @x, @y)
548
+ affine = Secp256k1.jp_to_affine(jp)
549
+ return self.class.infinity if affine.nil?
550
+
551
+ self.class.new(affine[0], affine[1])
552
+ end
553
+
554
+ # Point addition: self + other.
555
+ #
556
+ # @param other [Point]
557
+ # @return [Point]
558
+ def add(other)
559
+ return other if infinity?
560
+ return self if other.infinity?
561
+
562
+ jp1 = [@x, @y, 1]
563
+ jp2 = [other.x, other.y, 1]
564
+ jp_result = Secp256k1.jp_add(jp1, jp2)
565
+ affine = Secp256k1.jp_to_affine(jp_result)
566
+ return self.class.infinity if affine.nil?
567
+
568
+ self.class.new(affine[0], affine[1])
569
+ end
570
+
571
+ # Point negation: -self.
572
+ #
573
+ # @return [Point]
574
+ def negate
575
+ return self if infinity?
576
+
577
+ self.class.new(@x, Secp256k1.fneg(@y))
578
+ end
579
+
580
+ # Equality comparison.
581
+ #
582
+ # @param other [Point]
583
+ # @return [Boolean]
584
+ def ==(other)
585
+ return false unless other.is_a?(Point)
586
+
587
+ if infinity? && other.infinity?
588
+ true
589
+ elsif infinity? || other.infinity?
590
+ false
591
+ else
592
+ @x == other.x && @y == other.y
593
+ end
594
+ end
595
+ alias eql? ==
596
+
597
+ def hash
598
+ infinity? ? 0 : [@x, @y].hash
599
+ end
600
+ end
601
+
602
+ # Load native C acceleration if available.
603
+ # When the extension is compiled, field, scalar, and point operations
604
+ # are replaced with C implementations. The pure-Ruby methods above
605
+ # remain as the readable reference and are used as fallback.
606
+ begin
607
+ require 'secp256k1_native'
608
+
609
+ # Replace field, scalar, and point operations with native C versions.
610
+ #
611
+ # `method(m).to_proc` converts the C singleton method to a Proc,
612
+ # stripping the receiver binding so it can be attached to this module.
613
+ # We define directly on the singleton class (the public module-function
614
+ # surface) only — `module_function` is NOT called again here, because
615
+ # it would re-copy the private Ruby instance method back over our new
616
+ # singleton definition.
617
+ %i[fmul fsqr fadd fsub fneg finv fsqrt fred
618
+ scalar_mod scalar_mul scalar_inv scalar_add
619
+ jp_double jp_add jp_neg scalar_multiply_ct].each do |m|
620
+ singleton_class.define_method(m, Secp256k1Native.method(m).to_proc)
621
+ end
622
+ rescue LoadError
623
+ # Extension not compiled — pure-Ruby fallback, no action needed.
624
+ end
625
+ end
626
+ # rubocop:enable Naming/MethodParameterName, Metrics/ModuleLength
Binary file
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/secp256k1/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'secp256k1-native'
7
+ spec.version = Secp256k1::VERSION
8
+ spec.authors = ['Simon Bettison']
9
+ spec.email = ['simon@bettison.org']
10
+ spec.summary = 'Pure native C secp256k1 implementation for Ruby (no libsecp256k1 dependency)'
11
+ spec.description = <<~DESC
12
+ A standalone Ruby gem providing secp256k1 elliptic curve primitives via a native C
13
+ extension. Implements field arithmetic, scalar operations, Jacobian point arithmetic,
14
+ and constant-time Montgomery ladder scalar multiplication — all without any dependency
15
+ on libsecp256k1. Suitable for any Ruby project requiring secp256k1 operations.
16
+ DESC
17
+ spec.homepage = 'https://github.com/sgbett/secp256k1-native'
18
+ spec.license = 'MIT'
19
+
20
+ spec.required_ruby_version = '>= 2.7'
21
+
22
+ spec.extensions = ['ext/secp256k1_native/extconf.rb']
23
+
24
+ spec.files = Dir.glob('lib/**/*') +
25
+ Dir.glob('ext/**/*.{c,h,rb}') +
26
+ ['secp256k1-native.gemspec', 'LICENSE', 'README.md', 'CHANGELOG.md']
27
+
28
+ spec.require_paths = ['lib']
29
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: secp256k1-native
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.15.0
5
+ platform: ruby
6
+ authors:
7
+ - Simon Bettison
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ A standalone Ruby gem providing secp256k1 elliptic curve primitives via a native C
14
+ extension. Implements field arithmetic, scalar operations, Jacobian point arithmetic,
15
+ and constant-time Montgomery ladder scalar multiplication — all without any dependency
16
+ on libsecp256k1. Suitable for any Ruby project requiring secp256k1 operations.
17
+ email:
18
+ - simon@bettison.org
19
+ executables: []
20
+ extensions:
21
+ - ext/secp256k1_native/extconf.rb
22
+ extra_rdoc_files: []
23
+ files:
24
+ - CHANGELOG.md
25
+ - LICENSE
26
+ - README.md
27
+ - ext/secp256k1_native/extconf.rb
28
+ - ext/secp256k1_native/field.c
29
+ - ext/secp256k1_native/jacobian.c
30
+ - ext/secp256k1_native/scalar.c
31
+ - ext/secp256k1_native/secp256k1_native.c
32
+ - ext/secp256k1_native/secp256k1_native.h
33
+ - lib/secp256k1.rb
34
+ - lib/secp256k1/version.rb
35
+ - lib/secp256k1_native.bundle
36
+ - secp256k1-native.gemspec
37
+ homepage: https://github.com/sgbett/secp256k1-native
38
+ licenses:
39
+ - MIT
40
+ metadata: {}
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2.7'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 4.0.10
56
+ specification_version: 4
57
+ summary: Pure native C secp256k1 implementation for Ruby (no libsecp256k1 dependency)
58
+ test_files: []