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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +3 -3
- data/lib/bsv/primitives/curve.rb +5 -4
- data/lib/bsv/primitives/openssl_ec_shim.rb +182 -0
- data/lib/bsv/primitives/secp256k1.rb +504 -0
- data/lib/bsv/primitives.rb +1 -0
- data/lib/bsv/registry/client.rb +582 -0
- data/lib/bsv/registry/constants.rb +39 -0
- data/lib/bsv/registry/types.rb +227 -0
- data/lib/bsv/registry.rb +19 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv-sdk.rb +1 -0
- metadata +7 -1
|
@@ -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
|
data/lib/bsv/primitives.rb
CHANGED
|
@@ -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'
|