bsv-sdk 0.14.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +14 -2
- 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/secp256k1_native.bundle +0 -0
- data/lib/bsv/version.rb +1 -1
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27489947d80a203e32fad08b53cfee4edabca7df61f244dff2ca12c5fa45e261
|
|
4
|
+
data.tar.gz: d3bd5ead19a4fa67f4111e9bd3a203a1cad3c083ad4b8e174ee114b5b7fdb4bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ebdd5176602d584debb675d470dc31079cb16a29bb858c806123227efe03783c7c7f77eccf38174e254b7579fae9bad71d17c9e71791c21a694653647e15ad6
|
|
7
|
+
data.tar.gz: cedddebaec0eeb9c2fa037af4227ad7307492146c2b4dc18d3a7f4dfc7f3179c20428245424d0a8542db97bbad08831a334d7821f17fd3c05264d38289271552
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to the `bsv-sdk` gem are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
6
|
and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.15.0 — 2026-04-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Extracted secp256k1 elliptic curve operations to the standalone
|
|
12
|
+
`secp256k1-native` gem, with an optional native C extension providing
|
|
13
|
+
~22x speedup for field, scalar, and Jacobian point operations (#648)
|
|
14
|
+
- Native C extension scaffold with field arithmetic, scalar arithmetic,
|
|
15
|
+
and Jacobian point operations (#627, #628, #629, #630, #631)
|
|
16
|
+
- Constant-time Montgomery ladder with branchless conditional swap for
|
|
17
|
+
scalar multiplication; `mul` is now constant-time by default, with
|
|
18
|
+
`mul_vt` available for variable-time use cases (#641, #653)
|
|
19
|
+
- Wycheproof Bitcoin ECDSA test vectors (463 cases) with explicit
|
|
20
|
+
categorisation of high-S malleability cases as mathematically valid
|
|
21
|
+
but policy-rejected — documenting the layer separation between
|
|
22
|
+
ECDSA verification and Bitcoin's low-S enforcement (#652)
|
|
23
|
+
- Wycheproof standard ECDSA test vectors (474 cases), RFC 6979
|
|
24
|
+
compliance suite, and secp256k1 field/scalar/point compliance
|
|
25
|
+
examples (#636, #637, #638)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Carry overflow in schoolbook multiplication (L2) and branchless
|
|
29
|
+
borrow extraction in field reduction (#631)
|
|
30
|
+
- Gemspec version floor, docstring, and variable name corrections
|
|
31
|
+
from PR review (#653)
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- Replaced `BN` hex intermediaries with direct binary construction
|
|
35
|
+
for improved performance (#622)
|
|
36
|
+
- Binary byte comparison in `SignedMessage` verification (#624)
|
|
37
|
+
|
|
8
38
|
## 0.14.0 — 2026-04-22
|
|
9
39
|
|
|
10
40
|
### Added
|
data/README.md
CHANGED
|
@@ -34,7 +34,9 @@ These are maintained under the BSV Blockchain organisation and backed by the Bit
|
|
|
34
34
|
|
|
35
35
|
The BSV Blockchain Libraries Project aims to structure and maintain a middleware layer of the BSV Blockchain technology stack. By facilitating the development and maintenance of core libraries, it serves as an essential toolkit for developers looking to build on the BSV Blockchain.
|
|
36
36
|
|
|
37
|
-
This Ruby SDK brings maximum compatibility with the official SDK family to the Ruby ecosystem. It was born from a practical need: building an attestation gem ([bsv-attest](https://rubygems.org/gems/bsv-attest)) required a complete, idiomatic Ruby implementation of BSV primitives, script handling, and transaction construction.
|
|
37
|
+
This Ruby SDK brings maximum compatibility with the official SDK family to the Ruby ecosystem. It was born from a practical need: building an attestation gem ([bsv-attest](https://rubygems.org/gems/bsv-attest)) required a complete, idiomatic Ruby implementation of BSV primitives, script handling, and transaction construction.
|
|
38
|
+
|
|
39
|
+
Elliptic curve operations (secp256k1) are provided by the [`secp256k1-native`](https://github.com/sgbett/secp256k1-native) gem, which was originally part of this library and has been extracted as a standalone dependency. This is a custom implementation ported from the TypeScript reference SDK — it does not wrap `libsecp256k1`. It includes a pure Ruby implementation with an optional native C extension for constant-time field arithmetic and performance. OpenSSL is used only for hashing, HMAC, and symmetric encryption.
|
|
38
40
|
|
|
39
41
|
<!-- TODO: Update bsv-attest link once gem documentation is published (see #48) -->
|
|
40
42
|
|
|
@@ -59,6 +61,16 @@ Or install directly:
|
|
|
59
61
|
gem install bsv-sdk
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
### Native C Extension (Recommended)
|
|
65
|
+
|
|
66
|
+
The SDK depends on [`secp256k1-native`](https://github.com/sgbett/secp256k1-native) for elliptic curve operations. It works out of the box in pure Ruby, but compiling the optional C extension is strongly recommended — it provides constant-time field arithmetic, which protects against timing side-channel attacks on private key operations:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cd $(bundle show secp256k1-native) && bundle exec rake compile
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This requires a C99 compiler with `__uint128_t` support (GCC or Clang on macOS/Linux). If compilation is not possible, the SDK falls back to pure Ruby automatically — but the pure Ruby path does not guarantee constant-time execution.
|
|
73
|
+
|
|
62
74
|
### Basic Usage
|
|
63
75
|
|
|
64
76
|
Create and sign a P2PKH transaction:
|
|
@@ -101,7 +113,7 @@ puts tx.to_hex
|
|
|
101
113
|
|
|
102
114
|
## Features & Deliverables
|
|
103
115
|
|
|
104
|
-
- **Cryptographic Primitives** — ECDSA signing with RFC 6979 deterministic nonces, Schnorr signatures, ECIES encryption/decryption, Bitcoin Signed Messages. Elliptic curve operations
|
|
116
|
+
- **Cryptographic Primitives** — ECDSA signing with RFC 6979 deterministic nonces, Schnorr signatures, ECIES encryption/decryption, Bitcoin Signed Messages. Elliptic curve operations are provided by the [`secp256k1-native`](https://github.com/sgbett/secp256k1-native) gem — a pure Ruby secp256k1 implementation with an optional C extension (~22× speedup).
|
|
105
117
|
- **Key Management** — BIP-32 HD key derivation, BIP-39 mnemonic generation (12/24-word phrases), WIF import/export, Base58Check encoding/decoding.
|
|
106
118
|
- **Script Layer** — Complete opcode set, script parsing and serialisation, type detection and predicates (`p2pkh?`, `p2pk?`, `p2sh?`, `multisig?`, `op_return?`), data extraction (pubkey hashes, script hashes, addresses), and a fluent builder API.
|
|
107
119
|
- **Script Templates** — Ready-made locking and unlocking script generators for P2PKH, P2PK, P2MS (multisig), and OP_RETURN.
|
|
@@ -42,6 +42,7 @@ module BSV
|
|
|
42
42
|
bytes.each_byte { |b| b.zero? ? leading_zeros += 1 : break }
|
|
43
43
|
|
|
44
44
|
# Convert to big integer and repeatedly divide by 58
|
|
45
|
+
# C-backed hex conversion — 10x faster than pure-Ruby byte shifting; not a porting artefact.
|
|
45
46
|
n = bytes.unpack1('H*').to_i(16)
|
|
46
47
|
result = +''
|
|
47
48
|
while n.positive?
|
|
@@ -78,7 +79,7 @@ module BSV
|
|
|
78
79
|
n = (n * BASE) + digit
|
|
79
80
|
end
|
|
80
81
|
|
|
81
|
-
# Convert integer to bytes
|
|
82
|
+
# Convert integer to bytes — C-backed hex round-trip is the fastest pure-Ruby integer→bytes path.
|
|
82
83
|
hex = n.zero? ? '' : n.to_s(16)
|
|
83
84
|
hex = "0#{hex}" if hex.length.odd?
|
|
84
85
|
result = [hex].pack('H*')
|
data/lib/bsv/primitives/curve.rb
CHANGED
|
@@ -25,10 +25,11 @@ module BSV
|
|
|
25
25
|
|
|
26
26
|
module_function
|
|
27
27
|
|
|
28
|
-
# Multiply the generator point by a scalar (
|
|
28
|
+
# Multiply the generator point by a scalar (constant-time).
|
|
29
29
|
#
|
|
30
|
-
#
|
|
31
|
-
# scalars
|
|
30
|
+
# Uses the Montgomery ladder by default, matching OpenSSL convention.
|
|
31
|
+
# Safe for both secret and public scalars. For explicit variable-time
|
|
32
|
+
# multiplication of public scalars, use {multiply_generator_vt}.
|
|
32
33
|
#
|
|
33
34
|
# @param scalar_bn [OpenSSL::BN] the scalar multiplier
|
|
34
35
|
# @return [OpenSSL::PKey::EC::Point] the resulting curve point
|
|
@@ -38,19 +39,31 @@ module BSV
|
|
|
38
39
|
|
|
39
40
|
# Multiply the generator point by a secret scalar (constant-time).
|
|
40
41
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
42
|
+
# Alias for {multiply_generator} — retained for backward compatibility
|
|
43
|
+
# and expressiveness.
|
|
43
44
|
#
|
|
44
45
|
# @param scalar_bn [OpenSSL::BN] the secret scalar multiplier
|
|
45
46
|
# @return [OpenSSL::PKey::EC::Point] the resulting curve point
|
|
46
47
|
def multiply_generator_ct(scalar_bn)
|
|
47
|
-
G.
|
|
48
|
+
G.mul(scalar_bn)
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
# Multiply
|
|
51
|
+
# Multiply the generator point by a public scalar (variable-time, wNAF).
|
|
51
52
|
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
53
|
+
# Faster than {multiply_generator} but leaks timing information about
|
|
54
|
+
# the scalar. Use only for public scalars (e.g. signature verification).
|
|
55
|
+
#
|
|
56
|
+
# @param scalar_bn [OpenSSL::BN] the public scalar multiplier
|
|
57
|
+
# @return [OpenSSL::PKey::EC::Point] the resulting curve point
|
|
58
|
+
def multiply_generator_vt(scalar_bn)
|
|
59
|
+
G.mul_vt(scalar_bn)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Multiply an arbitrary curve point by a scalar (constant-time).
|
|
63
|
+
#
|
|
64
|
+
# Uses the Montgomery ladder by default, matching OpenSSL convention.
|
|
65
|
+
# Safe for both secret and public scalars. For explicit variable-time
|
|
66
|
+
# multiplication of public scalars, use {multiply_point_vt}.
|
|
54
67
|
#
|
|
55
68
|
# @param point [OpenSSL::PKey::EC::Point] the point to multiply
|
|
56
69
|
# @param scalar_bn [OpenSSL::BN] the scalar multiplier
|
|
@@ -61,14 +74,26 @@ module BSV
|
|
|
61
74
|
|
|
62
75
|
# Multiply an arbitrary curve point by a secret scalar (constant-time).
|
|
63
76
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
77
|
+
# Alias for {multiply_point} — retained for backward compatibility
|
|
78
|
+
# and expressiveness.
|
|
66
79
|
#
|
|
67
80
|
# @param point [OpenSSL::PKey::EC::Point] the base point
|
|
68
81
|
# @param scalar_bn [OpenSSL::BN] the secret scalar multiplier
|
|
69
82
|
# @return [OpenSSL::PKey::EC::Point] the resulting curve point
|
|
70
83
|
def multiply_point_ct(point, scalar_bn)
|
|
71
|
-
point.
|
|
84
|
+
point.mul(scalar_bn)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Multiply an arbitrary curve point by a public scalar (variable-time, wNAF).
|
|
88
|
+
#
|
|
89
|
+
# Faster than {multiply_point} but leaks timing information about
|
|
90
|
+
# the scalar. Use only for public scalars (e.g. signature verification).
|
|
91
|
+
#
|
|
92
|
+
# @param point [OpenSSL::PKey::EC::Point] the point to multiply
|
|
93
|
+
# @param scalar_bn [OpenSSL::BN] the public scalar multiplier
|
|
94
|
+
# @return [OpenSSL::PKey::EC::Point] the resulting curve point
|
|
95
|
+
def multiply_point_vt(point, scalar_bn)
|
|
96
|
+
point.mul_vt(scalar_bn)
|
|
72
97
|
end
|
|
73
98
|
|
|
74
99
|
# Add two curve points together.
|
data/lib/bsv/primitives/ecdsa.rb
CHANGED
|
@@ -82,8 +82,8 @@ module BSV
|
|
|
82
82
|
u1 = ((n - e) * r_inv) % n
|
|
83
83
|
u2 = (s * r_inv) % n
|
|
84
84
|
|
|
85
|
-
p1 = Curve.
|
|
86
|
-
p2 = Curve.
|
|
85
|
+
p1 = Curve.multiply_generator_vt(u1)
|
|
86
|
+
p2 = Curve.multiply_point_vt(r_point, u2)
|
|
87
87
|
q = Curve.add_points(p1, p2)
|
|
88
88
|
|
|
89
89
|
raise ArgumentError, 'recovered point is at infinity' if q.infinity?
|
|
@@ -112,8 +112,8 @@ module BSV
|
|
|
112
112
|
u2 = (r * s_inv) % n
|
|
113
113
|
|
|
114
114
|
# R' = u1*G + u2*Q
|
|
115
|
-
point1 = Curve.
|
|
116
|
-
point2 = Curve.
|
|
115
|
+
point1 = Curve.multiply_generator_vt(u1)
|
|
116
|
+
point2 = Curve.multiply_point_vt(public_key_point, u2)
|
|
117
117
|
result_point = Curve.add_points(point1, point2)
|
|
118
118
|
|
|
119
119
|
return false if result_point.infinity?
|
|
@@ -66,6 +66,22 @@ class BSVShimECPoint
|
|
|
66
66
|
pt
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Scalar multiplication: self * scalar (constant-time, Montgomery ladder).
|
|
70
|
+
#
|
|
71
|
+
# Matches OpenSSL convention where +EC_POINT_mul+ is always constant-time.
|
|
72
|
+
# Safe for both secret and public scalars.
|
|
73
|
+
#
|
|
74
|
+
# Also supports the multi-scalar form: +mul(bns, points)+ computes
|
|
75
|
+
# <tt>bns[0]*self + bns[1]*points[0] + ...</tt>
|
|
76
|
+
# where +bns.length == points.length + 1+.
|
|
77
|
+
#
|
|
78
|
+
# @overload mul(scalar_bn)
|
|
79
|
+
# @param scalar_bn [OpenSSL::BN, Integer] the scalar multiplier
|
|
80
|
+
# @overload mul(bns, points)
|
|
81
|
+
# @param bns [Array<OpenSSL::BN>] scalars; must have +points.length + 1+ elements
|
|
82
|
+
# @param points [Array<BSVShimECPoint>] additional points
|
|
83
|
+
# @raise [NoMethodError] if +bns+ and +points+ lengths are mismatched
|
|
84
|
+
# @return [BSVShimECPoint]
|
|
69
85
|
def mul(*args)
|
|
70
86
|
if args.length == 1
|
|
71
87
|
scalar = bn_to_int(args[0])
|
|
@@ -85,16 +101,27 @@ class BSVShimECPoint
|
|
|
85
101
|
end
|
|
86
102
|
end
|
|
87
103
|
|
|
88
|
-
# Constant-time scalar multiplication
|
|
104
|
+
# Constant-time scalar multiplication (alias for {#mul}).
|
|
89
105
|
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
106
|
+
# Retained for backward compatibility and expressiveness. Delegates
|
|
107
|
+
# to {#mul}, which is constant-time by default.
|
|
92
108
|
#
|
|
93
|
-
# @param scalar_bn [OpenSSL::BN, Integer] the
|
|
109
|
+
# @param scalar_bn [OpenSSL::BN, Integer] the scalar multiplier
|
|
94
110
|
# @return [BSVShimECPoint]
|
|
95
111
|
def mul_ct(scalar_bn)
|
|
112
|
+
mul(scalar_bn)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Variable-time scalar multiplication (wNAF).
|
|
116
|
+
#
|
|
117
|
+
# Faster than {#mul} but leaks timing information about the scalar.
|
|
118
|
+
# Use only for public scalars (e.g. signature verification).
|
|
119
|
+
#
|
|
120
|
+
# @param scalar_bn [OpenSSL::BN, Integer] the public scalar multiplier
|
|
121
|
+
# @return [BSVShimECPoint]
|
|
122
|
+
def mul_vt(scalar_bn)
|
|
96
123
|
scalar = bn_to_int(scalar_bn)
|
|
97
|
-
result = @secp_point.
|
|
124
|
+
result = @secp_point.mul_vt(scalar)
|
|
98
125
|
self.class.from_secp_point(@group, result)
|
|
99
126
|
end
|
|
100
127
|
|
|
@@ -165,7 +165,7 @@ module BSV
|
|
|
165
165
|
def derive_child(public_key, invoice_number)
|
|
166
166
|
shared = derive_shared_secret(public_key)
|
|
167
167
|
hmac = Digest.hmac_sha256(shared.compressed, invoice_number.encode('UTF-8'))
|
|
168
|
-
hmac_bn = OpenSSL::BN.new(hmac
|
|
168
|
+
hmac_bn = OpenSSL::BN.new(hmac, 2)
|
|
169
169
|
PrivateKey.new(@bn.mod_add(hmac_bn, Curve::N))
|
|
170
170
|
end
|
|
171
171
|
|
|
@@ -207,7 +207,7 @@ module BSV
|
|
|
207
207
|
loop do
|
|
208
208
|
counter_bytes = [i, attempts].pack('N*') + SecureRandom.random_bytes(32)
|
|
209
209
|
h = Digest.hmac_sha512(seed, counter_bytes)
|
|
210
|
-
candidate = OpenSSL::BN.new(h
|
|
210
|
+
candidate = OpenSSL::BN.new(h, 2) % PointInFiniteField::P
|
|
211
211
|
|
|
212
212
|
attempts += 1
|
|
213
213
|
raise ArgumentError, 'failed to generate unique x-coordinate after 5 attempts' if attempts > 5
|
|
@@ -132,7 +132,7 @@ module BSV
|
|
|
132
132
|
def derive_child(private_key, invoice_number)
|
|
133
133
|
shared = derive_shared_secret(private_key)
|
|
134
134
|
hmac = Digest.hmac_sha256(shared.compressed, invoice_number.encode('UTF-8'))
|
|
135
|
-
hmac_bn = OpenSSL::BN.new(hmac
|
|
135
|
+
hmac_bn = OpenSSL::BN.new(hmac, 2)
|
|
136
136
|
hmac_point = Curve.multiply_generator_ct(hmac_bn)
|
|
137
137
|
child_point = Curve.add_points(@point, hmac_point)
|
|
138
138
|
PublicKey.new(child_point)
|
|
@@ -97,15 +97,15 @@ module BSV
|
|
|
97
97
|
e = compute_challenge(public_key_a, public_key_b, shared_secret, proof.s_prime, proof.r)
|
|
98
98
|
|
|
99
99
|
# Equation 1: z·G == R + e·A
|
|
100
|
-
z_g = Curve.
|
|
101
|
-
e_a = Curve.
|
|
100
|
+
z_g = Curve.multiply_generator_vt(proof.z)
|
|
101
|
+
e_a = Curve.multiply_point_vt(public_key_a.point, e)
|
|
102
102
|
r_plus_ea = Curve.add_points(proof.r.point, e_a)
|
|
103
103
|
|
|
104
104
|
return false unless points_equal?(z_g, r_plus_ea)
|
|
105
105
|
|
|
106
106
|
# Equation 2: z·B == S' + e·S
|
|
107
|
-
z_b = Curve.
|
|
108
|
-
e_s = Curve.
|
|
107
|
+
z_b = Curve.multiply_point_vt(public_key_b.point, proof.z)
|
|
108
|
+
e_s = Curve.multiply_point_vt(shared_secret.point, e)
|
|
109
109
|
sp_plus_es = Curve.add_points(proof.s_prime.point, e_s)
|
|
110
110
|
|
|
111
111
|
points_equal?(z_b, sp_plus_es)
|
|
@@ -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
|
|
Binary file
|
data/lib/bsv/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsv-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.12'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: secp256k1-native
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.16'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.16'
|
|
26
40
|
description: A Ruby library for interacting with the BSV Blockchain — keys, scripts,
|
|
27
41
|
transactions, and more.
|
|
28
42
|
executables:
|
|
@@ -142,6 +156,7 @@ files:
|
|
|
142
156
|
- lib/bsv/script/opcodes.rb
|
|
143
157
|
- lib/bsv/script/push_drop_template.rb
|
|
144
158
|
- lib/bsv/script/script.rb
|
|
159
|
+
- lib/bsv/secp256k1_native.bundle
|
|
145
160
|
- lib/bsv/transaction.rb
|
|
146
161
|
- lib/bsv/transaction/beef.rb
|
|
147
162
|
- lib/bsv/transaction/chain_tracker.rb
|