bsv-sdk 0.14.0 → 0.16.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 +43 -0
- data/README.md +14 -2
- data/lib/bsv/auth/auth_middleware.rb +6 -6
- data/lib/bsv/auth/certificate.rb +16 -16
- data/lib/bsv/auth/master_certificate.rb +5 -5
- data/lib/bsv/auth/nonce.rb +13 -13
- data/lib/bsv/auth/peer.rb +53 -53
- data/lib/bsv/auth/verifiable_certificate.rb +1 -1
- data/lib/bsv/identity/client.rb +26 -32
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +17 -11
- data/lib/bsv/mcp/tools/check_balance.rb +16 -4
- data/lib/bsv/mcp/tools/fetch_tx.rb +11 -4
- data/lib/bsv/mcp/tools/fetch_utxos.rb +16 -4
- data/lib/bsv/network/arc.rb +13 -153
- data/lib/bsv/network/whats_on_chain.rb +13 -107
- data/lib/bsv/overlay/admin_token_template.rb +4 -4
- data/lib/bsv/primitives/base58.rb +2 -1
- data/lib/bsv/primitives/curve.rb +37 -12
- data/lib/bsv/primitives/ecdsa.rb +4 -4
- data/lib/bsv/primitives/openssl_ec_shim.rb +32 -5
- data/lib/bsv/primitives/private_key.rb +2 -2
- data/lib/bsv/primitives/public_key.rb +1 -1
- data/lib/bsv/primitives/schnorr.rb +4 -4
- data/lib/bsv/primitives/secp256k1.rb +4 -595
- data/lib/bsv/primitives/signature.rb +2 -0
- data/lib/bsv/primitives/signed_message.rb +6 -5
- data/lib/bsv/registry/client.rb +23 -27
- data/lib/bsv/script/push_drop_template.rb +4 -4
- data/lib/bsv/secp256k1_native.bundle +0 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/errors.rb +47 -0
- data/lib/bsv/wallet/interface/brc100.rb +267 -0
- data/lib/bsv/wallet/interface.rb +9 -0
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +150 -0
- data/lib/bsv/wallet/proto_wallet/validators.rb +74 -0
- data/lib/bsv/wallet/proto_wallet.rb +321 -0
- data/lib/bsv/wallet.rb +16 -0
- data/lib/bsv-sdk.rb +4 -1
- metadata +37 -1
|
@@ -1,602 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
3
|
+
require 'secp256k1'
|
|
10
4
|
|
|
11
5
|
module BSV
|
|
12
6
|
module Primitives
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
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
|
-
bytes.unpack1('H*').to_i(16)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Convert an Integer to a fixed-length big-endian binary string.
|
|
58
|
-
#
|
|
59
|
-
# @param n [Integer] the integer to convert
|
|
60
|
-
# @param length [Integer] desired byte length (default 32)
|
|
61
|
-
# @return [String] binary string (ASCII-8BIT)
|
|
62
|
-
def int_to_bytes(n, length = 32)
|
|
63
|
-
raise ArgumentError, 'negative integer' if n.negative?
|
|
64
|
-
|
|
65
|
-
hex = n.to_s(16)
|
|
66
|
-
hex = "0#{hex}" if hex.length.odd?
|
|
67
|
-
raise ArgumentError, "integer too large for #{length} bytes" if hex.length > length * 2
|
|
68
|
-
|
|
69
|
-
hex = hex.rjust(length * 2, '0')
|
|
70
|
-
[hex].pack('H*')
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# -------------------------------------------------------------------
|
|
74
|
-
# Field arithmetic (mod P)
|
|
75
|
-
# -------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
# Fast reduction modulo the secp256k1 prime.
|
|
78
|
-
#
|
|
79
|
-
# Exploits the structure P = 2^256 - 2^32 - 977 to avoid generic
|
|
80
|
-
# modular division. Two folding passes plus a conditional subtraction.
|
|
81
|
-
#
|
|
82
|
-
# @param x [Integer] non-negative integer
|
|
83
|
-
# @return [Integer] x mod P, in range [0, P)
|
|
84
|
-
def fred(x)
|
|
85
|
-
# First fold
|
|
86
|
-
hi = x >> 256
|
|
87
|
-
x = (x & MASK_256) + (hi << 32) + (hi * 977)
|
|
88
|
-
|
|
89
|
-
# Second fold (hi <= 2^32 + 977, so one more pass suffices)
|
|
90
|
-
hi = x >> 256
|
|
91
|
-
x = (x & MASK_256) + (hi << 32) + (hi * 977)
|
|
92
|
-
|
|
93
|
-
# Final conditional subtraction
|
|
94
|
-
x >= P ? x - P : x
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Modular multiplication in the field.
|
|
98
|
-
def fmul(a, b)
|
|
99
|
-
fred(a * b)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Modular squaring in the field.
|
|
103
|
-
def fsqr(a)
|
|
104
|
-
fred(a * a)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Modular addition in the field.
|
|
108
|
-
def fadd(a, b)
|
|
109
|
-
fred(a + b)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Modular subtraction in the field.
|
|
113
|
-
def fsub(a, b)
|
|
114
|
-
a >= b ? a - b : P - (b - a)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Modular negation in the field.
|
|
118
|
-
def fneg(a)
|
|
119
|
-
a.zero? ? 0 : P - a
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Modular multiplicative inverse in the field (Fermat's little theorem).
|
|
123
|
-
#
|
|
124
|
-
# @param a [Integer] value to invert (must be non-zero mod P)
|
|
125
|
-
# @return [Integer] a^(P-2) mod P
|
|
126
|
-
# @raise [ArgumentError] if a is zero mod P
|
|
127
|
-
def finv(a)
|
|
128
|
-
raise ArgumentError, 'field inverse is undefined for zero' if (a % P).zero?
|
|
129
|
-
|
|
130
|
-
a.pow(P - 2, P)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Modular square root in the field.
|
|
134
|
-
#
|
|
135
|
-
# Uses the identity sqrt(a) = a^((P+1)/4) mod P, valid because
|
|
136
|
-
# P ≡ 3 (mod 4). Returns +nil+ if +a+ is not a quadratic residue.
|
|
137
|
-
#
|
|
138
|
-
# @param a [Integer]
|
|
139
|
-
# @return [Integer, nil] the square root, or nil if none exists
|
|
140
|
-
def fsqrt(a)
|
|
141
|
-
r = a.pow(P_PLUS1_DIV4, P)
|
|
142
|
-
fsqr(r) == (a % P) ? r : nil
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# -------------------------------------------------------------------
|
|
146
|
-
# Scalar arithmetic (mod N)
|
|
147
|
-
# -------------------------------------------------------------------
|
|
148
|
-
|
|
149
|
-
# Reduce modulo the curve order.
|
|
150
|
-
def scalar_mod(a)
|
|
151
|
-
r = a % N
|
|
152
|
-
r += N if r.negative?
|
|
153
|
-
r
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Scalar multiplicative inverse (Fermat).
|
|
157
|
-
#
|
|
158
|
-
# @raise [ArgumentError] if a is zero mod N
|
|
159
|
-
def scalar_inv(a)
|
|
160
|
-
raise ArgumentError, 'scalar inverse is undefined for zero' if (a % N).zero?
|
|
161
|
-
|
|
162
|
-
a.pow(N - 2, N)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# Scalar multiplication mod N.
|
|
166
|
-
def scalar_mul(a, b)
|
|
167
|
-
(a * b) % N
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Scalar addition mod N.
|
|
171
|
-
def scalar_add(a, b)
|
|
172
|
-
(a + b) % N
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# -------------------------------------------------------------------
|
|
176
|
-
# Jacobian point operations (internal)
|
|
177
|
-
#
|
|
178
|
-
# Points are represented as [X, Y, Z] arrays of Integers.
|
|
179
|
-
# The point at infinity is [0, 1, 0].
|
|
180
|
-
# -------------------------------------------------------------------
|
|
181
|
-
|
|
182
|
-
# @!visibility private
|
|
183
|
-
JP_INFINITY = [0, 1, 0].freeze
|
|
184
|
-
|
|
185
|
-
# Double a Jacobian point.
|
|
186
|
-
#
|
|
187
|
-
# Formula from hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html
|
|
188
|
-
# (a=0 for secp256k1).
|
|
189
|
-
#
|
|
190
|
-
# @param p [Array(Integer, Integer, Integer)] Jacobian point [X, Y, Z]
|
|
191
|
-
# @return [Array(Integer, Integer, Integer)]
|
|
192
|
-
def jp_double(p)
|
|
193
|
-
x1, y1, z1 = p
|
|
194
|
-
return JP_INFINITY if y1.zero?
|
|
195
|
-
|
|
196
|
-
y1sq = fsqr(y1)
|
|
197
|
-
s = fmul(4, fmul(x1, y1sq))
|
|
198
|
-
m = fmul(3, fsqr(x1)) # a=0 for secp256k1
|
|
199
|
-
x3 = fsub(fsqr(m), fmul(2, s))
|
|
200
|
-
y3 = fsub(fmul(m, fsub(s, x3)), fmul(8, fsqr(y1sq)))
|
|
201
|
-
z3 = fmul(2, fmul(y1, z1))
|
|
202
|
-
[x3, y3, z3]
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Add two Jacobian points.
|
|
206
|
-
#
|
|
207
|
-
# @param p [Array] first Jacobian point
|
|
208
|
-
# @param q [Array] second Jacobian point
|
|
209
|
-
# @return [Array] resulting Jacobian point
|
|
210
|
-
def jp_add(p, q)
|
|
211
|
-
_px, _py, pz = p
|
|
212
|
-
_qx, _qy, qz = q
|
|
213
|
-
return q if pz.zero?
|
|
214
|
-
return p if qz.zero?
|
|
215
|
-
|
|
216
|
-
z1z1 = fsqr(pz)
|
|
217
|
-
z2z2 = fsqr(qz)
|
|
218
|
-
u1 = fmul(p[0], z2z2)
|
|
219
|
-
u2 = fmul(q[0], z1z1)
|
|
220
|
-
s1 = fmul(p[1], fmul(z2z2, qz))
|
|
221
|
-
s2 = fmul(q[1], fmul(z1z1, pz))
|
|
222
|
-
|
|
223
|
-
h = fsub(u2, u1)
|
|
224
|
-
r = fsub(s2, s1)
|
|
225
|
-
|
|
226
|
-
if h.zero?
|
|
227
|
-
return r.zero? ? jp_double(p) : JP_INFINITY
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
hh = fsqr(h)
|
|
231
|
-
hhh = fmul(h, hh)
|
|
232
|
-
v = fmul(u1, hh)
|
|
233
|
-
|
|
234
|
-
x3 = fsub(fsub(fsqr(r), hhh), fmul(2, v))
|
|
235
|
-
y3 = fsub(fmul(r, fsub(v, x3)), fmul(s1, hhh))
|
|
236
|
-
z3 = fmul(h, fmul(pz, qz))
|
|
237
|
-
[x3, y3, z3]
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# Convert a Jacobian point to affine coordinates.
|
|
241
|
-
#
|
|
242
|
-
# @param jp [Array(Integer, Integer, Integer)]
|
|
243
|
-
# @return [Array(Integer, Integer)] affine [x, y], or nil for infinity
|
|
244
|
-
def jp_to_affine(jp)
|
|
245
|
-
_x, _y, z = jp
|
|
246
|
-
return nil if z.zero?
|
|
247
|
-
|
|
248
|
-
zinv = finv(z)
|
|
249
|
-
zinv2 = fsqr(zinv)
|
|
250
|
-
x = fmul(jp[0], zinv2)
|
|
251
|
-
y = fmul(jp[1], fmul(zinv2, zinv))
|
|
252
|
-
[x, y]
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# -------------------------------------------------------------------
|
|
256
|
-
# Windowed-NAF scalar multiplication (variable-time, public scalars)
|
|
257
|
-
# -------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
# @!visibility private
|
|
260
|
-
# Maximum number of entries kept in the wNAF precomputation cache.
|
|
261
|
-
# Bounds memory usage for long-running processes (e.g. servers).
|
|
262
|
-
WNAF_CACHE_MAX = 512
|
|
263
|
-
|
|
264
|
-
# @!visibility private
|
|
265
|
-
# Cache for precomputed wNAF tables, keyed by "window:x:y".
|
|
266
|
-
# Evicts oldest entry when the LRU limit is reached.
|
|
267
|
-
WNAF_TABLE_CACHE = {} # rubocop:disable Style/MutableConstant
|
|
268
|
-
|
|
269
|
-
# @!visibility private
|
|
270
|
-
# Multiply a point by a scalar using windowed-NAF.
|
|
271
|
-
#
|
|
272
|
-
# Variable-time algorithm — suitable only for public scalars (e.g.
|
|
273
|
-
# signature verification). Secret-scalar paths MUST use
|
|
274
|
-
# {scalar_multiply_ct} instead.
|
|
275
|
-
#
|
|
276
|
-
# Internal method — use {Point#mul} or {Point#mul_ct} instead.
|
|
277
|
-
# Exposed as a module function only so the nested Point class can
|
|
278
|
-
# call it; not part of the public API.
|
|
279
|
-
#
|
|
280
|
-
# @param k [Integer] the scalar (must be in [1, N))
|
|
281
|
-
# @param px [Integer] affine x-coordinate of the base point
|
|
282
|
-
# @param py [Integer] affine y-coordinate of the base point
|
|
283
|
-
# @param window [Integer] wNAF window size (default 5)
|
|
284
|
-
# @return [Array(Integer, Integer, Integer)] result as Jacobian point
|
|
285
|
-
def scalar_multiply_wnaf(k, px, py, window = 5)
|
|
286
|
-
return JP_INFINITY if k.zero?
|
|
287
|
-
|
|
288
|
-
cache_key = "#{window}:#{px.to_s(16)}:#{py.to_s(16)}"
|
|
289
|
-
tbl = WNAF_TABLE_CACHE[cache_key]
|
|
290
|
-
|
|
291
|
-
if tbl.nil?
|
|
292
|
-
# Evict the oldest entry when the cache is full (simple LRU).
|
|
293
|
-
WNAF_TABLE_CACHE.delete(WNAF_TABLE_CACHE.keys.first) if WNAF_TABLE_CACHE.size >= WNAF_CACHE_MAX
|
|
294
|
-
|
|
295
|
-
tbl_size = 1 << (window - 1) # e.g. w=5 -> 16 entries
|
|
296
|
-
tbl = Array.new(tbl_size)
|
|
297
|
-
tbl[0] = [px, py, 1]
|
|
298
|
-
two_p = jp_double(tbl[0])
|
|
299
|
-
1.upto(tbl_size - 1) do |i|
|
|
300
|
-
tbl[i] = jp_add(tbl[i - 1], two_p)
|
|
301
|
-
end
|
|
302
|
-
WNAF_TABLE_CACHE[cache_key] = tbl
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
# Build wNAF representation
|
|
306
|
-
w_big = 1 << window
|
|
307
|
-
w_half = w_big >> 1
|
|
308
|
-
wnaf = []
|
|
309
|
-
k_tmp = k
|
|
310
|
-
while k_tmp.positive?
|
|
311
|
-
if k_tmp.odd?
|
|
312
|
-
z = k_tmp & (w_big - 1)
|
|
313
|
-
z -= w_big if z > w_half
|
|
314
|
-
wnaf << z
|
|
315
|
-
k_tmp -= z
|
|
316
|
-
else
|
|
317
|
-
wnaf << 0
|
|
318
|
-
end
|
|
319
|
-
k_tmp >>= 1
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
# Accumulate from MSB to LSB
|
|
323
|
-
q = JP_INFINITY
|
|
324
|
-
(wnaf.length - 1).downto(0) do |i|
|
|
325
|
-
q = jp_double(q)
|
|
326
|
-
di = wnaf[i]
|
|
327
|
-
next if di.zero?
|
|
328
|
-
|
|
329
|
-
idx = di.abs >> 1
|
|
330
|
-
addend = di.positive? ? tbl[idx] : jp_neg(tbl[idx])
|
|
331
|
-
q = jp_add(q, addend)
|
|
332
|
-
end
|
|
333
|
-
q
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# -------------------------------------------------------------------
|
|
337
|
-
# Montgomery ladder scalar multiplication (constant-time, secret scalars)
|
|
338
|
-
# -------------------------------------------------------------------
|
|
339
|
-
|
|
340
|
-
# @!visibility private
|
|
341
|
-
# Multiply a point by a scalar using the Montgomery ladder.
|
|
342
|
-
#
|
|
343
|
-
# Executes a fixed number of iterations (256) with one +jp_double+
|
|
344
|
-
# and one +jp_add+ per iteration regardless of the scalar value.
|
|
345
|
-
# Use this for ALL secret-scalar paths (key generation, signing,
|
|
346
|
-
# ECDH, BIP-32 derivation).
|
|
347
|
-
#
|
|
348
|
-
# *Best-effort constant-time in interpreted Ruby.* The branch on
|
|
349
|
-
# +bit+ selects which register receives each operation, and both
|
|
350
|
-
# operations always execute. However, Ruby's interpreter, GC, and
|
|
351
|
-
# the early-return branches in +jp_add+/+jp_double+ (for infinity
|
|
352
|
-
# edge cases) mean true constant-time execution is not achievable
|
|
353
|
-
# without native code. This matches the ts-sdk's TypeScript
|
|
354
|
-
# implementation, which has the same structural properties. For
|
|
355
|
-
# production deployments requiring side-channel resistance beyond
|
|
356
|
-
# what an interpreted language can offer, use a native secp256k1
|
|
357
|
-
# library (e.g. libsecp256k1 via FFI).
|
|
358
|
-
#
|
|
359
|
-
# Internal method — use {Point#mul_ct} instead. Not part of the
|
|
360
|
-
# public API.
|
|
361
|
-
#
|
|
362
|
-
# @param k [Integer] secret scalar (must be in [1, N))
|
|
363
|
-
# @param px [Integer] affine x-coordinate of the base point
|
|
364
|
-
# @param py [Integer] affine y-coordinate of the base point
|
|
365
|
-
# @return [Array(Integer, Integer, Integer)] result as Jacobian point
|
|
366
|
-
def scalar_multiply_ct(k, px, py)
|
|
367
|
-
return JP_INFINITY if k.zero?
|
|
368
|
-
|
|
369
|
-
# r0 accumulates the result; r1 = r0 + base_point at all times.
|
|
370
|
-
r0 = JP_INFINITY
|
|
371
|
-
r1 = [px, py, 1]
|
|
372
|
-
|
|
373
|
-
256.times do |i|
|
|
374
|
-
bit = (k >> (255 - i)) & 1
|
|
375
|
-
if bit.zero?
|
|
376
|
-
r1 = jp_add(r0, r1)
|
|
377
|
-
r0 = jp_double(r0)
|
|
378
|
-
else
|
|
379
|
-
r0 = jp_add(r0, r1)
|
|
380
|
-
r1 = jp_double(r1)
|
|
381
|
-
end
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
r0
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
# Negate a Jacobian point.
|
|
388
|
-
def jp_neg(p)
|
|
389
|
-
return p if p[2].zero?
|
|
390
|
-
|
|
391
|
-
[p[0], fneg(p[1]), p[2]]
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
# -------------------------------------------------------------------
|
|
395
|
-
# Point class
|
|
396
|
-
# -------------------------------------------------------------------
|
|
397
|
-
|
|
398
|
-
# An elliptic curve point on secp256k1.
|
|
399
|
-
#
|
|
400
|
-
# Stores affine coordinates (x, y) or represents the point at infinity.
|
|
401
|
-
# Scalar multiplication uses Jacobian coordinates internally with
|
|
402
|
-
# windowed-NAF for performance.
|
|
403
|
-
class Point
|
|
404
|
-
# @return [Integer, nil] x-coordinate (nil for infinity)
|
|
405
|
-
attr_reader :x
|
|
406
|
-
|
|
407
|
-
# @return [Integer, nil] y-coordinate (nil for infinity)
|
|
408
|
-
attr_reader :y
|
|
409
|
-
|
|
410
|
-
# @param x [Integer, nil] x-coordinate (nil for infinity)
|
|
411
|
-
# @param y [Integer, nil] y-coordinate (nil for infinity)
|
|
412
|
-
def initialize(x, y)
|
|
413
|
-
@x = x
|
|
414
|
-
@y = y
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
# The point at infinity (additive identity).
|
|
418
|
-
#
|
|
419
|
-
# @return [Point]
|
|
420
|
-
def self.infinity
|
|
421
|
-
new(nil, nil)
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
# The generator point G.
|
|
425
|
-
#
|
|
426
|
-
# @return [Point]
|
|
427
|
-
def self.generator
|
|
428
|
-
@generator ||= new(GX, GY)
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
# Deserialise a point from compressed (33 bytes) or uncompressed
|
|
432
|
-
# (65 bytes) SEC1 encoding.
|
|
433
|
-
#
|
|
434
|
-
# @param bytes [String] binary string
|
|
435
|
-
# @return [Point]
|
|
436
|
-
# @raise [ArgumentError] if the encoding is invalid or the point
|
|
437
|
-
# is not on the curve
|
|
438
|
-
def self.from_bytes(bytes)
|
|
439
|
-
bytes = bytes.b if bytes.encoding != Encoding::BINARY
|
|
440
|
-
prefix = bytes.getbyte(0)
|
|
441
|
-
|
|
442
|
-
case prefix
|
|
443
|
-
when 0x04 # Uncompressed
|
|
444
|
-
raise ArgumentError, 'invalid uncompressed point length' unless bytes.length == 65
|
|
445
|
-
|
|
446
|
-
x = Secp256k1.bytes_to_int(bytes[1, 32])
|
|
447
|
-
y = Secp256k1.bytes_to_int(bytes[33, 32])
|
|
448
|
-
raise ArgumentError, 'x coordinate out of field range' if x >= P
|
|
449
|
-
raise ArgumentError, 'y coordinate out of field range' if y >= P
|
|
450
|
-
|
|
451
|
-
pt = new(x, y)
|
|
452
|
-
raise ArgumentError, 'point is not on the curve' unless pt.on_curve?
|
|
453
|
-
|
|
454
|
-
pt
|
|
455
|
-
when 0x02, 0x03 # Compressed
|
|
456
|
-
raise ArgumentError, 'invalid compressed point length' unless bytes.length == 33
|
|
457
|
-
|
|
458
|
-
x = Secp256k1.bytes_to_int(bytes[1, 32])
|
|
459
|
-
raise ArgumentError, 'x coordinate out of field range' if x >= P
|
|
460
|
-
|
|
461
|
-
y_squared = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(x), x), 7)
|
|
462
|
-
y = Secp256k1.fsqrt(y_squared)
|
|
463
|
-
raise ArgumentError, 'invalid point: x not on curve' if y.nil?
|
|
464
|
-
|
|
465
|
-
# Ensure y-parity matches prefix
|
|
466
|
-
y = Secp256k1.fneg(y) if (y.odd? ? 0x03 : 0x02) != prefix
|
|
467
|
-
|
|
468
|
-
new(x, y)
|
|
469
|
-
else
|
|
470
|
-
raise ArgumentError, "unknown point prefix: 0x#{prefix.to_s(16).rjust(2, '0')}"
|
|
471
|
-
end
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
# Whether this is the point at infinity.
|
|
475
|
-
#
|
|
476
|
-
# @return [Boolean]
|
|
477
|
-
def infinity?
|
|
478
|
-
@x.nil?
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
# Whether this point lies on the secp256k1 curve (y² = x³ + 7).
|
|
482
|
-
#
|
|
483
|
-
# @return [Boolean]
|
|
484
|
-
def on_curve?
|
|
485
|
-
return true if infinity?
|
|
486
|
-
|
|
487
|
-
lhs = Secp256k1.fsqr(@y)
|
|
488
|
-
rhs = Secp256k1.fadd(Secp256k1.fmul(Secp256k1.fsqr(@x), @x), 7)
|
|
489
|
-
lhs == rhs
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# Serialise the point in SEC1 format.
|
|
493
|
-
#
|
|
494
|
-
# @param format [:compressed, :uncompressed]
|
|
495
|
-
# @return [String] binary string (33 or 65 bytes)
|
|
496
|
-
# @raise [RuntimeError] if the point is at infinity
|
|
497
|
-
def to_octet_string(format = :compressed)
|
|
498
|
-
raise 'cannot serialise point at infinity' if infinity?
|
|
499
|
-
|
|
500
|
-
case format
|
|
501
|
-
when :compressed
|
|
502
|
-
prefix = @y.odd? ? "\x03".b : "\x02".b
|
|
503
|
-
prefix + Secp256k1.int_to_bytes(@x, 32)
|
|
504
|
-
when :uncompressed
|
|
505
|
-
"\x04".b + Secp256k1.int_to_bytes(@x, 32) + Secp256k1.int_to_bytes(@y, 32)
|
|
506
|
-
else
|
|
507
|
-
raise ArgumentError, "unknown format: #{format}"
|
|
508
|
-
end
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
# Scalar multiplication: self * scalar (variable-time, wNAF).
|
|
512
|
-
#
|
|
513
|
-
# Suitable for public scalars only (e.g. signature verification).
|
|
514
|
-
# For secret-scalar paths use {#mul_ct}.
|
|
515
|
-
#
|
|
516
|
-
# @param scalar [Integer] the scalar multiplier
|
|
517
|
-
# @return [Point] the resulting point
|
|
518
|
-
def mul(scalar)
|
|
519
|
-
return self.class.infinity if scalar.zero? || infinity?
|
|
520
|
-
|
|
521
|
-
scalar %= N
|
|
522
|
-
return self.class.infinity if scalar.zero?
|
|
523
|
-
|
|
524
|
-
jp = Secp256k1.scalar_multiply_wnaf(scalar, @x, @y)
|
|
525
|
-
affine = Secp256k1.jp_to_affine(jp)
|
|
526
|
-
return self.class.infinity if affine.nil?
|
|
527
|
-
|
|
528
|
-
self.class.new(affine[0], affine[1])
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
# Constant-time scalar multiplication: self * scalar (Montgomery ladder).
|
|
532
|
-
#
|
|
533
|
-
# Processes all 256 bits unconditionally so execution time does not
|
|
534
|
-
# depend on the scalar value. Use this for secret-scalar paths:
|
|
535
|
-
# key generation, signing, and ECDH shared-secret derivation.
|
|
536
|
-
#
|
|
537
|
-
# @param scalar [Integer] the secret scalar multiplier
|
|
538
|
-
# @return [Point] the resulting point
|
|
539
|
-
def mul_ct(scalar)
|
|
540
|
-
return self.class.infinity if scalar.zero? || infinity?
|
|
541
|
-
|
|
542
|
-
scalar %= N
|
|
543
|
-
return self.class.infinity if scalar.zero?
|
|
544
|
-
|
|
545
|
-
jp = Secp256k1.scalar_multiply_ct(scalar, @x, @y)
|
|
546
|
-
affine = Secp256k1.jp_to_affine(jp)
|
|
547
|
-
return self.class.infinity if affine.nil?
|
|
548
|
-
|
|
549
|
-
self.class.new(affine[0], affine[1])
|
|
550
|
-
end
|
|
551
|
-
|
|
552
|
-
# Point addition: self + other.
|
|
553
|
-
#
|
|
554
|
-
# @param other [Point]
|
|
555
|
-
# @return [Point]
|
|
556
|
-
def add(other)
|
|
557
|
-
return other if infinity?
|
|
558
|
-
return self if other.infinity?
|
|
559
|
-
|
|
560
|
-
jp1 = [@x, @y, 1]
|
|
561
|
-
jp2 = [other.x, other.y, 1]
|
|
562
|
-
jp_result = Secp256k1.jp_add(jp1, jp2)
|
|
563
|
-
affine = Secp256k1.jp_to_affine(jp_result)
|
|
564
|
-
return self.class.infinity if affine.nil?
|
|
565
|
-
|
|
566
|
-
self.class.new(affine[0], affine[1])
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
# Point negation: -self.
|
|
570
|
-
#
|
|
571
|
-
# @return [Point]
|
|
572
|
-
def negate
|
|
573
|
-
return self if infinity?
|
|
574
|
-
|
|
575
|
-
self.class.new(@x, Secp256k1.fneg(@y))
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
# Equality comparison.
|
|
579
|
-
#
|
|
580
|
-
# @param other [Point]
|
|
581
|
-
# @return [Boolean]
|
|
582
|
-
def ==(other)
|
|
583
|
-
return false unless other.is_a?(Point)
|
|
584
|
-
|
|
585
|
-
if infinity? && other.infinity?
|
|
586
|
-
true
|
|
587
|
-
elsif infinity? || other.infinity?
|
|
588
|
-
false
|
|
589
|
-
else
|
|
590
|
-
@x == other.x && @y == other.y
|
|
591
|
-
end
|
|
592
|
-
end
|
|
593
|
-
alias eql? ==
|
|
594
|
-
|
|
595
|
-
def hash
|
|
596
|
-
infinity? ? 0 : [@x, @y].hash
|
|
597
|
-
end
|
|
598
|
-
end
|
|
599
|
-
end
|
|
7
|
+
# Backwards-compatibility alias: BSV::Primitives::Secp256k1 → ::Secp256k1
|
|
8
|
+
# This mapping will be removed in the next major version.
|
|
9
|
+
Secp256k1 = ::Secp256k1
|
|
600
10
|
end
|
|
601
11
|
end
|
|
602
|
-
# rubocop:enable Naming/MethodParameterName, Metrics/ModuleLength
|
|
@@ -59,7 +59,9 @@ module BSV
|
|
|
59
59
|
|
|
60
60
|
# Parse S
|
|
61
61
|
s_offset = 4 + r_len
|
|
62
|
+
raise ArgumentError, 'truncated: missing S tag' if s_offset >= bytes.length
|
|
62
63
|
raise ArgumentError, 'invalid integer tag for S' unless bytes[s_offset] == 0x02
|
|
64
|
+
raise ArgumentError, 'truncated: missing S length' if s_offset + 1 >= bytes.length
|
|
63
65
|
|
|
64
66
|
s_len = bytes[s_offset + 1]
|
|
65
67
|
raise ArgumentError, 'S length overflows' if s_offset + 2 + s_len > bytes.length
|
|
@@ -73,17 +73,18 @@ module BSV
|
|
|
73
73
|
else
|
|
74
74
|
# Specific recipient
|
|
75
75
|
verifier_pub_bytes = sig.byteslice(37, 33)
|
|
76
|
-
verifier_pub_hex = verifier_pub_bytes.unpack1('H*')
|
|
77
76
|
|
|
78
77
|
if recipient.nil?
|
|
79
78
|
raise ArgumentError,
|
|
80
|
-
|
|
79
|
+
'this signature can only be verified with knowledge of a specific private key. ' \
|
|
80
|
+
"The associated public key is: #{verifier_pub_bytes.unpack1('H*')}"
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
if
|
|
83
|
+
recipient_pub_bytes = recipient.public_key.compressed
|
|
84
|
+
if verifier_pub_bytes != recipient_pub_bytes
|
|
85
85
|
raise ArgumentError,
|
|
86
|
-
"the recipient public key is #{
|
|
86
|
+
"the recipient public key is #{recipient_pub_bytes.unpack1('H*')} " \
|
|
87
|
+
"but the signature requires the recipient to have public key #{verifier_pub_bytes.unpack1('H*')}"
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
key_id_offset = 70
|