bsv-sdk 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1ead96687ff2a6118fde4ab72a7dc0b11c7d82db8b4b883910120bcc0cee0dc
4
- data.tar.gz: 5a089f6b80111ba4abf5472bfbd8067a028b35a73f90e504e2541376d0d00d55
3
+ metadata.gz: 27489947d80a203e32fad08b53cfee4edabca7df61f244dff2ca12c5fa45e261
4
+ data.tar.gz: d3bd5ead19a4fa67f4111e9bd3a203a1cad3c083ad4b8e174ee114b5b7fdb4bd
5
5
  SHA512:
6
- metadata.gz: d05f8b3b8a584706323c631fb9ff005420c6d5cdc5081f76fbf6a73b15c12323b87d30f469e5fa186bfa30b51905ffe4bc0785b57bd77d72286ee3f8ce03dc1f
7
- data.tar.gz: b892a8add5e5faaad6bc837ab26de879419cc457d909be4f5b1fb812fd4a69fb5356529689081dffe1195caffdb2e259da6a073ea1ce6ff2f0d08068c73a67f1
6
+ metadata.gz: 5ebdd5176602d584debb675d470dc31079cb16a29bb858c806123227efe03783c7c7f77eccf38174e254b7579fae9bad71d17c9e71791c21a694653647e15ad6
7
+ data.tar.gz: cedddebaec0eeb9c2fa037af4227ad7307492146c2b4dc18d3a7f4dfc7f3179c20428245424d0a8542db97bbad08831a334d7821f17fd3c05264d38289271552
data/CHANGELOG.md CHANGED
@@ -5,6 +5,59 @@ 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
+
38
+ ## 0.14.0 — 2026-04-22
39
+
40
+ ### Added
41
+ - `BSV::Network::WhatsOnChain` expanded with `current_height`,
42
+ `get_block_header(height)`, and `valid_root_for_height?(root, height)` —
43
+ a single WhatsOnChain instance now serves as a complete chain data source
44
+ (#596)
45
+ - BEEF-based SPV verification conformance tests (#607)
46
+
47
+ ### Changed
48
+ - `Transaction#verify` now raises `VerificationError` for all failure modes:
49
+ `:script_failure` (wraps `ScriptError` with cause chaining),
50
+ `:missing_source` (replaces `ArgumentError`), `:invalid_merkle_proof`,
51
+ `:insufficient_fee`, and `:output_overflow`. Code rescuing `ArgumentError`
52
+ or `ScriptError` from `verify` must switch to `VerificationError` (#608)
53
+ - Removed dead code: `BSV::Wallet::Wallet` (superseded by bsv-wallet gem's
54
+ `Client`), `BSV::Messages` (unused re-export alias), and duplicate
55
+ `BSV::Wallet::InsufficientFundsError` (#594)
56
+
57
+ ### Fixed
58
+ - WhatsOnChain `valid_root_for_height?` YARD doc now correctly documents
59
+ that 404 returns `false` rather than raising
60
+
8
61
  ## 0.13.0 — 2026-04-21
9
62
 
10
63
  ### 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. Rather than wrapping FFI bindings or shelling out to other languages, the SDK implements everything in pure Ruby. Elliptic curve operations (secp256k1) use a native Ruby implementation ported from the TypeScript reference SDK; OpenSSL is used only for hashing, HMAC, and symmetric encryption.
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 use a [pure Ruby secp256k1 implementation](docs/about/secp256k1.md) ported from the TypeScript reference SDK.
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.
@@ -176,7 +176,7 @@ module BSV
176
176
  def verify(verifier_wallet = nil)
177
177
  raise ArgumentError, 'certificate has no signature to verify' if @signature.nil? || @signature.empty?
178
178
 
179
- verifier_wallet ||= BSV::Wallet::Client.new('anyone', storage: BSV::Wallet::Store::Memory.new)
179
+ verifier_wallet ||= BSV::Wallet::Client.new('anyone', storage: BSV::Wallet::Store::Memory.new, allow_memory_store: true)
180
180
  preimage = to_binary(include_signature: false)
181
181
  sig_bytes = [@signature].pack('H*').unpack('C*')
182
182
 
@@ -5,8 +5,11 @@ module BSV
5
5
  # WhatsOnChain chain data provider for reading transactions and UTXOs
6
6
  # from the BSV network.
7
7
  #
8
- # Any object responding to #fetch_utxos(address) and
9
- # #fetch_transaction(txid) can serve as a chain data provider;
8
+ # Any object responding to #fetch_utxos(address),
9
+ # #fetch_transaction(txid), #current_height,
10
+ # #get_block_header(height), and optionally
11
+ # #valid_root_for_height?(root_hex, height) can serve as a chain
12
+ # data source;
10
13
  # this class implements that contract by delegating to
11
14
  # Protocols::WoCREST.
12
15
  #
@@ -62,6 +65,42 @@ module BSV
62
65
  BSV::Transaction::Transaction.from_hex(result.data)
63
66
  end
64
67
 
68
+ # Return the current blockchain height.
69
+ # @return [Integer]
70
+ # @raise [BSV::Network::ChainProviderError] on network or API error
71
+ def current_height
72
+ result = @protocol.call(:current_height)
73
+ raise_on_error(result)
74
+
75
+ result.data
76
+ end
77
+
78
+ # Fetch the block header for a given height.
79
+ # @param height [Integer] block height
80
+ # @return [Hash] parsed block header JSON
81
+ # @raise [BSV::Network::ChainProviderError] on network or API error
82
+ def get_block_header(height)
83
+ result = @protocol.call(:get_block_header, height)
84
+ raise_on_error(result)
85
+
86
+ result.data
87
+ end
88
+
89
+ # Verify that a merkle root is valid for the given block height.
90
+ # Returns +false+ when the block is not found (404); raises on other errors.
91
+ # @param root [String] expected merkle root as hex
92
+ # @param height [Integer] block height
93
+ # @return [Boolean]
94
+ # @raise [BSV::Network::ChainProviderError] on network or non-404 API error
95
+ def valid_root_for_height?(root, height)
96
+ result = @protocol.call(:valid_root, root, height)
97
+ return false if result.not_found?
98
+
99
+ raise_on_error(result)
100
+
101
+ result.data == true
102
+ end
103
+
65
104
  private
66
105
 
67
106
  # Translates a non-success Protocol result into a raised ChainProviderError.
@@ -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*')
@@ -25,10 +25,11 @@ module BSV
25
25
 
26
26
  module_function
27
27
 
28
- # Multiply the generator point by a scalar (variable-time, wNAF).
28
+ # Multiply the generator point by a scalar (constant-time).
29
29
  #
30
- # Suitable for public scalars only (e.g. verify paths). For secret
31
- # scalars use {multiply_generator_ct}.
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
- # Uses the Montgomery ladder to avoid timing side-channels on the
42
- # scalar. Use for key generation and signing.
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.mul_ct(scalar_bn)
48
+ G.mul(scalar_bn)
48
49
  end
49
50
 
50
- # Multiply an arbitrary curve point by a scalar (variable-time, wNAF).
51
+ # Multiply the generator point by a public scalar (variable-time, wNAF).
51
52
  #
52
- # Suitable for public scalars only. For secret scalars use
53
- # {multiply_point_ct}.
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
- # Uses the Montgomery ladder to avoid timing side-channels on the
65
- # scalar. Use for ECDH shared-secret derivation.
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.mul_ct(scalar_bn)
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.
@@ -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.multiply_generator(u1)
86
- p2 = Curve.multiply_point(r_point, u2)
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.multiply_generator(u1)
116
- point2 = Curve.multiply_point(public_key_point, u2)
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 via the Montgomery ladder.
104
+ # Constant-time scalar multiplication (alias for {#mul}).
89
105
  #
90
- # Delegates to {BSV::Primitives::Secp256k1::Point#mul_ct} to ensure
91
- # secret-scalar paths execute in constant time.
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 secret scalar
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.mul_ct(scalar)
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.unpack1('H*'), 16)
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.unpack1('H*'), 16) % PointInFiniteField::P
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.unpack1('H*'), 16)
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.multiply_generator(proof.z)
101
- e_a = Curve.multiply_point(public_key_a.point, e)
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.multiply_point(public_key_b.point, proof.z)
108
- e_s = Curve.multiply_point(shared_secret.point, e)
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)