bsv-sdk 0.5.0 → 0.6.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.
@@ -0,0 +1,504 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Primitives
5
+ # Pure Ruby secp256k1 elliptic curve implementation.
6
+ #
7
+ # Provides field arithmetic, point operations with Jacobian coordinates,
8
+ # and windowed-NAF scalar multiplication. Ported from the BSV TypeScript
9
+ # SDK reference implementation.
10
+ #
11
+ # All field operations work on plain Ruby +Integer+ values (arbitrary
12
+ # precision, C-backed in MRI). No external gems required.
13
+ module Secp256k1
14
+ # The secp256k1 field prime: p = 2^256 - 2^32 - 977
15
+ P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
16
+
17
+ # The curve order (number of points on the curve).
18
+ N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
19
+
20
+ # Half the curve order, used for low-S normalisation (BIP-62).
21
+ HALF_N = N >> 1
22
+
23
+ # Generator point x-coordinate.
24
+ GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
25
+
26
+ # Generator point y-coordinate.
27
+ GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
28
+
29
+ # (P + 1) / 4 — used for modular square root since P ≡ 3 (mod 4).
30
+ P_PLUS1_DIV4 = (P + 1) >> 2
31
+
32
+ # 256-bit mask for fast reduction.
33
+ MASK_256 = (1 << 256) - 1
34
+
35
+ module_function
36
+
37
+ # -------------------------------------------------------------------
38
+ # Byte conversion helpers
39
+ # -------------------------------------------------------------------
40
+
41
+ # Convert a big-endian binary string to an Integer.
42
+ #
43
+ # @param bytes [String] binary string (ASCII-8BIT)
44
+ # @return [Integer]
45
+ def bytes_to_int(bytes)
46
+ bytes.unpack1('H*').to_i(16)
47
+ end
48
+
49
+ # Convert an Integer to a fixed-length big-endian binary string.
50
+ #
51
+ # @param n [Integer] the integer to convert
52
+ # @param length [Integer] desired byte length (default 32)
53
+ # @return [String] binary string (ASCII-8BIT)
54
+ def int_to_bytes(n, length = 32)
55
+ raise ArgumentError, 'negative integer' if n.negative?
56
+
57
+ hex = n.to_s(16)
58
+ hex = "0#{hex}" if hex.length.odd?
59
+ raise ArgumentError, "integer too large for #{length} bytes" if hex.length > length * 2
60
+
61
+ hex = hex.rjust(length * 2, '0')
62
+ [hex].pack('H*')
63
+ end
64
+
65
+ # -------------------------------------------------------------------
66
+ # Field arithmetic (mod P)
67
+ # -------------------------------------------------------------------
68
+
69
+ # Fast reduction modulo the secp256k1 prime.
70
+ #
71
+ # Exploits the structure P = 2^256 - 2^32 - 977 to avoid generic
72
+ # modular division. Two folding passes plus a conditional subtraction.
73
+ #
74
+ # @param x [Integer] non-negative integer
75
+ # @return [Integer] x mod P, in range [0, P)
76
+ def fred(x)
77
+ # First fold
78
+ hi = x >> 256
79
+ x = (x & MASK_256) + (hi << 32) + (hi * 977)
80
+
81
+ # Second fold (hi <= 2^32 + 977, so one more pass suffices)
82
+ hi = x >> 256
83
+ x = (x & MASK_256) + (hi << 32) + (hi * 977)
84
+
85
+ # Final conditional subtraction
86
+ x >= P ? x - P : x
87
+ end
88
+
89
+ # Modular multiplication in the field.
90
+ def fmul(a, b)
91
+ fred(a * b)
92
+ end
93
+
94
+ # Modular squaring in the field.
95
+ def fsqr(a)
96
+ fred(a * a)
97
+ end
98
+
99
+ # Modular addition in the field.
100
+ def fadd(a, b)
101
+ fred(a + b)
102
+ end
103
+
104
+ # Modular subtraction in the field.
105
+ def fsub(a, b)
106
+ a >= b ? a - b : P - (b - a)
107
+ end
108
+
109
+ # Modular negation in the field.
110
+ def fneg(a)
111
+ a.zero? ? 0 : P - a
112
+ end
113
+
114
+ # Modular multiplicative inverse in the field (Fermat's little theorem).
115
+ #
116
+ # @param a [Integer] value to invert (must be non-zero mod P)
117
+ # @return [Integer] a^(P-2) mod P
118
+ # @raise [ArgumentError] if a is zero mod P
119
+ def finv(a)
120
+ raise ArgumentError, 'field inverse is undefined for zero' if (a % P).zero?
121
+
122
+ a.pow(P - 2, P)
123
+ end
124
+
125
+ # Modular square root in the field.
126
+ #
127
+ # Uses the identity sqrt(a) = a^((P+1)/4) mod P, valid because
128
+ # P ≡ 3 (mod 4). Returns +nil+ if +a+ is not a quadratic residue.
129
+ #
130
+ # @param a [Integer]
131
+ # @return [Integer, nil] the square root, or nil if none exists
132
+ def fsqrt(a)
133
+ r = a.pow(P_PLUS1_DIV4, P)
134
+ fsqr(r) == (a % P) ? r : nil
135
+ end
136
+
137
+ # -------------------------------------------------------------------
138
+ # Scalar arithmetic (mod N)
139
+ # -------------------------------------------------------------------
140
+
141
+ # Reduce modulo the curve order.
142
+ def scalar_mod(a)
143
+ r = a % N
144
+ r += N if r.negative?
145
+ r
146
+ end
147
+
148
+ # Scalar multiplicative inverse (Fermat).
149
+ #
150
+ # @raise [ArgumentError] if a is zero mod N
151
+ def scalar_inv(a)
152
+ raise ArgumentError, 'scalar inverse is undefined for zero' if (a % N).zero?
153
+
154
+ a.pow(N - 2, N)
155
+ end
156
+
157
+ # Scalar multiplication mod N.
158
+ def scalar_mul(a, b)
159
+ (a * b) % N
160
+ end
161
+
162
+ # Scalar addition mod N.
163
+ def scalar_add(a, b)
164
+ (a + b) % N
165
+ end
166
+
167
+ # -------------------------------------------------------------------
168
+ # Jacobian point operations (internal)
169
+ #
170
+ # Points are represented as [X, Y, Z] arrays of Integers.
171
+ # The point at infinity is [0, 1, 0].
172
+ # -------------------------------------------------------------------
173
+
174
+ # @!visibility private
175
+ JP_INFINITY = [0, 1, 0].freeze
176
+
177
+ # Double a Jacobian point.
178
+ #
179
+ # Formula from hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html
180
+ # (a=0 for secp256k1).
181
+ #
182
+ # @param p [Array(Integer, Integer, Integer)] Jacobian point [X, Y, Z]
183
+ # @return [Array(Integer, Integer, Integer)]
184
+ def jp_double(p)
185
+ x1, y1, z1 = p
186
+ return JP_INFINITY if y1.zero?
187
+
188
+ y1sq = fsqr(y1)
189
+ s = fmul(4, fmul(x1, y1sq))
190
+ m = fmul(3, fsqr(x1)) # a=0 for secp256k1
191
+ x3 = fsub(fsqr(m), fmul(2, s))
192
+ y3 = fsub(fmul(m, fsub(s, x3)), fmul(8, fsqr(y1sq)))
193
+ z3 = fmul(2, fmul(y1, z1))
194
+ [x3, y3, z3]
195
+ end
196
+
197
+ # Add two Jacobian points.
198
+ #
199
+ # @param p [Array] first Jacobian point
200
+ # @param q [Array] second Jacobian point
201
+ # @return [Array] resulting Jacobian point
202
+ def jp_add(p, q)
203
+ _px, _py, pz = p
204
+ _qx, _qy, qz = q
205
+ return q if pz.zero?
206
+ return p if qz.zero?
207
+
208
+ z1z1 = fsqr(pz)
209
+ z2z2 = fsqr(qz)
210
+ u1 = fmul(p[0], z2z2)
211
+ u2 = fmul(q[0], z1z1)
212
+ s1 = fmul(p[1], fmul(z2z2, qz))
213
+ s2 = fmul(q[1], fmul(z1z1, pz))
214
+
215
+ h = fsub(u2, u1)
216
+ r = fsub(s2, s1)
217
+
218
+ if h.zero?
219
+ return r.zero? ? jp_double(p) : JP_INFINITY
220
+ end
221
+
222
+ hh = fsqr(h)
223
+ hhh = fmul(h, hh)
224
+ v = fmul(u1, hh)
225
+
226
+ x3 = fsub(fsub(fsqr(r), hhh), fmul(2, v))
227
+ y3 = fsub(fmul(r, fsub(v, x3)), fmul(s1, hhh))
228
+ z3 = fmul(h, fmul(pz, qz))
229
+ [x3, y3, z3]
230
+ end
231
+
232
+ # Convert a Jacobian point to affine coordinates.
233
+ #
234
+ # @param jp [Array(Integer, Integer, Integer)]
235
+ # @return [Array(Integer, Integer)] affine [x, y], or nil for infinity
236
+ def jp_to_affine(jp)
237
+ _x, _y, z = jp
238
+ return nil if z.zero?
239
+
240
+ zinv = finv(z)
241
+ zinv2 = fsqr(zinv)
242
+ x = fmul(jp[0], zinv2)
243
+ y = fmul(jp[1], fmul(zinv2, zinv))
244
+ [x, y]
245
+ end
246
+
247
+ # -------------------------------------------------------------------
248
+ # Windowed-NAF scalar multiplication
249
+ # -------------------------------------------------------------------
250
+
251
+ # @!visibility private
252
+ # Cache for precomputed wNAF tables, keyed by "window:x:y".
253
+ WNAF_TABLE_CACHE = {} # rubocop:disable Style/MutableConstant
254
+
255
+ # @!visibility private
256
+ # Multiply a point by a scalar using windowed-NAF.
257
+ #
258
+ # Internal method — use {Point#mul} instead. Exposed as a module
259
+ # function only so the nested Point class can call it; not part of
260
+ # the public API.
261
+ #
262
+ # @param k [Integer] the scalar (must be in [1, N))
263
+ # @param px [Integer] affine x-coordinate of the base point
264
+ # @param py [Integer] affine y-coordinate of the base point
265
+ # @param window [Integer] wNAF window size (default 5)
266
+ # @return [Array(Integer, Integer, Integer)] result as Jacobian point
267
+ def scalar_multiply_wnaf(k, px, py, window = 5)
268
+ return JP_INFINITY if k.zero?
269
+
270
+ cache_key = "#{window}:#{px.to_s(16)}:#{py.to_s(16)}"
271
+ tbl = WNAF_TABLE_CACHE[cache_key]
272
+
273
+ if tbl.nil?
274
+ tbl_size = 1 << (window - 1) # e.g. w=5 -> 16 entries
275
+ tbl = Array.new(tbl_size)
276
+ tbl[0] = [px, py, 1]
277
+ two_p = jp_double(tbl[0])
278
+ 1.upto(tbl_size - 1) do |i|
279
+ tbl[i] = jp_add(tbl[i - 1], two_p)
280
+ end
281
+ WNAF_TABLE_CACHE[cache_key] = tbl
282
+ end
283
+
284
+ # Build wNAF representation
285
+ w_big = 1 << window
286
+ w_half = w_big >> 1
287
+ wnaf = []
288
+ k_tmp = k
289
+ while k_tmp.positive?
290
+ if k_tmp.odd?
291
+ z = k_tmp & (w_big - 1)
292
+ z -= w_big if z > w_half
293
+ wnaf << z
294
+ k_tmp -= z
295
+ else
296
+ wnaf << 0
297
+ end
298
+ k_tmp >>= 1
299
+ end
300
+
301
+ # Accumulate from MSB to LSB
302
+ q = JP_INFINITY
303
+ (wnaf.length - 1).downto(0) do |i|
304
+ q = jp_double(q)
305
+ di = wnaf[i]
306
+ next if di.zero?
307
+
308
+ idx = di.abs >> 1
309
+ addend = di.positive? ? tbl[idx] : jp_neg(tbl[idx])
310
+ q = jp_add(q, addend)
311
+ end
312
+ q
313
+ end
314
+
315
+ # Negate a Jacobian point.
316
+ def jp_neg(p)
317
+ return p if p[2].zero?
318
+
319
+ [p[0], fneg(p[1]), p[2]]
320
+ end
321
+
322
+ # -------------------------------------------------------------------
323
+ # Point class
324
+ # -------------------------------------------------------------------
325
+
326
+ # An elliptic curve point on secp256k1.
327
+ #
328
+ # Stores affine coordinates (x, y) or represents the point at infinity.
329
+ # Scalar multiplication uses Jacobian coordinates internally with
330
+ # windowed-NAF for performance.
331
+ class Point
332
+ # @return [Integer, nil] x-coordinate (nil for infinity)
333
+ attr_reader :x
334
+
335
+ # @return [Integer, nil] y-coordinate (nil for infinity)
336
+ attr_reader :y
337
+
338
+ # @param x [Integer, nil] x-coordinate (nil for infinity)
339
+ # @param y [Integer, nil] y-coordinate (nil for infinity)
340
+ def initialize(x, y)
341
+ @x = x
342
+ @y = y
343
+ end
344
+
345
+ # The point at infinity (additive identity).
346
+ #
347
+ # @return [Point]
348
+ def self.infinity
349
+ new(nil, nil)
350
+ end
351
+
352
+ # The generator point G.
353
+ #
354
+ # @return [Point]
355
+ def self.generator
356
+ @generator ||= new(GX, GY)
357
+ end
358
+
359
+ # Deserialise a point from compressed (33 bytes) or uncompressed
360
+ # (65 bytes) SEC1 encoding.
361
+ #
362
+ # @param bytes [String] binary string
363
+ # @return [Point]
364
+ # @raise [ArgumentError] if the encoding is invalid or the point
365
+ # is not on the curve
366
+ def self.from_bytes(bytes)
367
+ bytes = bytes.b if bytes.encoding != Encoding::BINARY
368
+ prefix = bytes.getbyte(0)
369
+
370
+ case prefix
371
+ when 0x04 # Uncompressed
372
+ raise ArgumentError, 'invalid uncompressed point length' unless bytes.length == 65
373
+
374
+ x = Secp256k1.bytes_to_int(bytes[1, 32])
375
+ y = Secp256k1.bytes_to_int(bytes[33, 32])
376
+ raise ArgumentError, 'x coordinate out of field range' if x >= P
377
+ raise ArgumentError, 'y coordinate out of field range' if y >= P
378
+
379
+ pt = new(x, y)
380
+ raise ArgumentError, 'point is not on the curve' unless pt.on_curve?
381
+
382
+ pt
383
+ when 0x02, 0x03 # Compressed
384
+ raise ArgumentError, 'invalid compressed point length' unless bytes.length == 33
385
+
386
+ x = Secp256k1.bytes_to_int(bytes[1, 32])
387
+ raise ArgumentError, 'x coordinate out of field range' if x >= P
388
+ y_squared = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(x), x), 7)
389
+ y = Secp256k1.fsqrt(y_squared)
390
+ raise ArgumentError, 'invalid point: x not on curve' if y.nil?
391
+
392
+ # Ensure y-parity matches prefix
393
+ y = Secp256k1.fneg(y) if (y.odd? ? 0x03 : 0x02) != prefix
394
+
395
+ new(x, y)
396
+ else
397
+ raise ArgumentError, "unknown point prefix: 0x#{prefix.to_s(16).rjust(2, '0')}"
398
+ end
399
+ end
400
+
401
+ # Whether this is the point at infinity.
402
+ #
403
+ # @return [Boolean]
404
+ def infinity?
405
+ @x.nil?
406
+ end
407
+
408
+ # Whether this point lies on the secp256k1 curve (y² = x³ + 7).
409
+ #
410
+ # @return [Boolean]
411
+ def on_curve?
412
+ return true if infinity?
413
+
414
+ lhs = Secp256k1.fsqr(@y)
415
+ rhs = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(@x), @x), 7)
416
+ lhs == rhs
417
+ end
418
+
419
+ # Serialise the point in SEC1 format.
420
+ #
421
+ # @param format [:compressed, :uncompressed]
422
+ # @return [String] binary string (33 or 65 bytes)
423
+ # @raise [RuntimeError] if the point is at infinity
424
+ def to_octet_string(format = :compressed)
425
+ raise 'cannot serialise point at infinity' if infinity?
426
+
427
+ case format
428
+ when :compressed
429
+ prefix = @y.odd? ? "\x03".b : "\x02".b
430
+ prefix + Secp256k1.int_to_bytes(@x, 32)
431
+ when :uncompressed
432
+ "\x04".b + Secp256k1.int_to_bytes(@x, 32) + Secp256k1.int_to_bytes(@y, 32)
433
+ else
434
+ raise ArgumentError, "unknown format: #{format}"
435
+ end
436
+ end
437
+
438
+ # Scalar multiplication: self * scalar.
439
+ #
440
+ # @param scalar [Integer] the scalar multiplier
441
+ # @return [Point] the resulting point
442
+ def mul(scalar)
443
+ return self.class.infinity if scalar.zero? || infinity?
444
+
445
+ scalar %= N
446
+ return self.class.infinity if scalar.zero?
447
+
448
+ jp = Secp256k1.scalar_multiply_wnaf(scalar, @x, @y)
449
+ affine = Secp256k1.jp_to_affine(jp)
450
+ return self.class.infinity if affine.nil?
451
+
452
+ self.class.new(affine[0], affine[1])
453
+ end
454
+
455
+ # Point addition: self + other.
456
+ #
457
+ # @param other [Point]
458
+ # @return [Point]
459
+ def add(other)
460
+ return other if infinity?
461
+ return self if other.infinity?
462
+
463
+ jp1 = [@x, @y, 1]
464
+ jp2 = [other.x, other.y, 1]
465
+ jp_result = Secp256k1.jp_add(jp1, jp2)
466
+ affine = Secp256k1.jp_to_affine(jp_result)
467
+ return self.class.infinity if affine.nil?
468
+
469
+ self.class.new(affine[0], affine[1])
470
+ end
471
+
472
+ # Point negation: -self.
473
+ #
474
+ # @return [Point]
475
+ def negate
476
+ return self if infinity?
477
+
478
+ self.class.new(@x, Secp256k1.fneg(@y))
479
+ end
480
+
481
+ # Equality comparison.
482
+ #
483
+ # @param other [Point]
484
+ # @return [Boolean]
485
+ def ==(other)
486
+ return false unless other.is_a?(Point)
487
+
488
+ if infinity? && other.infinity?
489
+ true
490
+ elsif infinity? || other.infinity?
491
+ false
492
+ else
493
+ @x == other.x && @y == other.y
494
+ end
495
+ end
496
+ alias eql? ==
497
+
498
+ def hash
499
+ infinity? ? 0 : @x.hash ^ @y.hash
500
+ end
501
+ end
502
+ end
503
+ end
504
+ end
@@ -7,6 +7,7 @@ module BSV
7
7
  # HD key derivation (BIP-32), and mnemonic phrase generation (BIP-39).
8
8
  # All cryptography uses Ruby's stdlib +openssl+ — no external gems.
9
9
  module Primitives
10
+ autoload :Secp256k1, 'bsv/primitives/secp256k1'
10
11
  autoload :Curve, 'bsv/primitives/curve'
11
12
  autoload :Digest, 'bsv/primitives/digest'
12
13
  autoload :Base58, 'bsv/primitives/base58'