bsv-sdk 0.8.1 → 0.8.2

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: d6ca8a4c5f04ec956d46b46d18286a7dad889496c1ae2683ff79210c01e10529
4
- data.tar.gz: d2c327e4b61a979909dd31f4755f852c3d0c6153f00c135c446ec64386f5709a
3
+ metadata.gz: 534417702a1c6fe35494ff3d7bc028d302452fc21312747cd83167137fab43c7
4
+ data.tar.gz: d7aca5b47d7f853f41142d2ae9087351bf0160beb0b25959330e7e380bbb5d92
5
5
  SHA512:
6
- metadata.gz: 015f6727214704a6a972f1fd6f8cc7c9597f21df21ab794698d9862f7c9a4df702f33be01cec402dc5726240137ae22137f74e08cfc820dd25204ffa92b32768
7
- data.tar.gz: 53517f95c64d850fa7b164f4b785020d81866525d9bda7623e22eeb89ba7a55d61f9d8c17abadf8cadcee9a5a4469516bbe9f87102f50bbc2f32559963ff5fd0
6
+ metadata.gz: 109f3d7d92f210bb3bc5df4e2defaec5322be06b33a467023109fef27d1c9ffe8ca2f876b865b9a4e9f21ef69e9f5d19dd8fde5b02cefdd58f3ee2ac58cfae41
7
+ data.tar.gz: 70145605156016e9fbcd6a4c397a317358ad3142c97637d6ad8a849934ac8d356f40c555e55af178280693c3c4d6b1d3f76af8f10df5526e6fde6fefdef928c5
data/CHANGELOG.md CHANGED
@@ -21,6 +21,124 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
21
21
  and each gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
22
22
  independently.
23
23
 
24
+ ## sdk-0.8.2 / wallet-0.3.4 — 2026-04-08
25
+
26
+ Paired security patch release. Three P0 findings from the
27
+ [2026-04-08 cross-SDK compliance review](.architecture/reviews/20260408-cross-sdk-compliance-review.md)
28
+ plus follow-up hardening from the PR review pass. Must be installed
29
+ together — the `bsv-wallet` gemspec now pins its `bsv-sdk` dependency
30
+ to `>= 0.8.2, < 1.0` to enforce the paired upgrade and prevent a stale
31
+ pair where one gem has its fixes and the other doesn't.
32
+
33
+ Two GitHub Security Advisories accompany this release (draft until
34
+ CVE IDs return from MITRE):
35
+
36
+ - [GHSA-hc36-c89j-5f4j](.security/advisories/2026-0001-acquire-certificate-signature-bypass.md) — F8.15 / F8.16 partial — `acquire_certificate` persists unverified certifier signatures (CWE-347, CVSS 8.1 HIGH)
37
+ - [GHSA-9hfr-gw99-8rhx](.security/advisories/2026-0002-arc-broadcaster-failure-statuses.md) — F5.13 — ARC broadcaster treats failure statuses as success (CWE-754, CVSS 7.5 HIGH)
38
+
39
+ ### Security
40
+
41
+ - [wallet] **`acquire_certificate` now verifies certifier signatures** before
42
+ persisting (BRC-52). Both the `'direct'` and `'issuance'` acquisition
43
+ paths previously wrote user-supplied `signature:` values to storage
44
+ without any verification — a caller could forge a certificate that
45
+ `list_certificates` / `prove_certificate` would later treat as
46
+ authentic. This was a credential forgery primitive masquerading as
47
+ an API finding. The new `BSV::Wallet::CertificateSignature` module
48
+ builds the canonical BRC-52 preimage (matching the TS reference
49
+ `Certificate#toBinary(false)` byte-for-byte) and delegates to
50
+ `ProtoWallet#verify_signature`. Invalid certificates raise
51
+ `BSV::Wallet::CertificateSignature::InvalidError` and are not
52
+ persisted. Closes F8.15 (and the verification aspect of F8.16).
53
+
54
+ - [sdk] **`VarInt.encode` now rejects negative integers** and values
55
+ above 2^64 − 1. Previously `VarInt.encode(-1)` fell into the single-
56
+ byte branch and emitted `0xFF` (the marker for a 9-byte encoding),
57
+ silently corrupting the transaction stream with no exception raised.
58
+ The docstring already required a non-negative integer; the
59
+ implementation did not enforce it. Closes F1.3.
60
+
61
+ - [sdk] **ARC broadcaster recognises the full failure status set**. The
62
+ previous `REJECTED_STATUSES` contained only `REJECTED` and
63
+ `DOUBLE_SPEND_ATTEMPTED`; responses with txStatus `INVALID`,
64
+ `MALFORMED`, `MINED_IN_STALE_BLOCK`, or any `ORPHAN`-containing
65
+ `txStatus` / `extraInfo` were silently treated as successful
66
+ broadcasts. Callers relying on `broadcast()` to signal failure would
67
+ trust transactions that were never actually accepted by the network.
68
+ The new failure set matches the TypeScript reference broadcaster
69
+ exactly, and case-insensitive matching defends against ARC's
70
+ documented history of emitting values outside its own OpenAPI enum
71
+ (TS issue #105). Malformed 2xx responses without a `txid` field
72
+ also raise, closing the same silent-success class for shape
73
+ corruption. Closes F5.13.
74
+
75
+ ### Changed
76
+
77
+ - [sdk] **ARC broadcaster HTTP wire format** brought into line with the
78
+ TypeScript reference:
79
+ - Content-Type is now `application/json` (was `application/octet-stream`)
80
+ - Body is `{"rawTx": hex}` — Extended Format (BRC-30) hex when every
81
+ input has `source_satoshis` / `source_locking_script` populated
82
+ (so ARC can validate sighashes without fetching parents), falling
83
+ back to plain raw-tx hex otherwise
84
+ - New `XDeployment-ID` header (default: `bsv-ruby-sdk-<random hex>`,
85
+ overridable via `deployment_id:` constructor kwarg)
86
+ - New optional `X-CallbackUrl` and `X-CallbackToken` constructor
87
+ kwargs for ARC status callbacks
88
+
89
+ - [wallet] **`bsv-wallet.gemspec` bsv-sdk dependency pinned** to
90
+ `>= 0.8.2, < 1.0`. The previous `~> 0.4` constraint was stale (wallet
91
+ hasn't been tested against bsv-sdk 0.4.x in months) and would have
92
+ let a user install `bsv-wallet 0.3.4` against an old `bsv-sdk` that
93
+ was missing F1.3 and F5.13. Technically breaking — any consumer
94
+ pinned to `bsv-sdk < 0.8.2` must upgrade — but un-breaking in
95
+ practice: it forces users to the known-good pair rather than a
96
+ silently-broken combination.
97
+
98
+ ### Internal
99
+
100
+ - [sdk] `lib/bsv/network/**/*` added to `Metrics/ClassLength` and
101
+ `Metrics/ParameterLists` RuboCop exclusion lists to match the
102
+ existing treatment of `lib/bsv/wallet_interface/**/*`. ARC is
103
+ HTTP-client boilerplate in the same shape.
104
+ - [wallet] `lib/bsv/wallet_interface/**/*` added to the
105
+ `Metrics/ModuleLength` exclusion list (was previously only excluded
106
+ from `Metrics/ClassLength`). The new `CertificateSignature` module
107
+ triggered the discrepancy.
108
+ - [sdk, wallet] Review-feedback hardening bundled into the same PR to
109
+ keep the security-patch window small: case-insensitive ARC failure
110
+ matching, `Base64.strict_decode64` on BRC-52 preimage fields,
111
+ `EncodingError` rescue in `CertificateSignature.verify!`, rejection
112
+ of mixed string / symbol duplicate field names, malformed 2xx
113
+ rejection in ARC, and even-length guard on hex signatures.
114
+
115
+ ### Migration notes
116
+
117
+ - **Existing `bsv-wallet` users** pinned to `bsv-sdk ~> 0.4` will need
118
+ to relax their constraint or upgrade. Anything installed before
119
+ `bsv-wallet 0.3.4` is vulnerable to the F8.15 certificate forgery
120
+ primitive.
121
+ - **Callers passing negative integers to `VarInt.encode`** (unlikely —
122
+ the docstring already disallowed it) will now get an `ArgumentError`
123
+ instead of silent corruption. Fix: pass non-negative values.
124
+ - **Callers relying on ARC broadcaster silently succeeding for INVALID
125
+ / MALFORMED / MINED_IN_STALE_BLOCK / ORPHAN responses** will now see
126
+ `BroadcastError` raised. Fix: handle the error — the previous
127
+ behaviour was objectively wrong and any downstream logic that
128
+ tolerated it was silently corrupt.
129
+ - **Callers of `acquire_certificate` with a fake or untrusted
130
+ `signature:` field** will now see
131
+ `BSV::Wallet::CertificateSignature::InvalidError`. Fix: ensure the
132
+ certificate has been properly signed by the declared certifier.
133
+
134
+ ### Test suite
135
+
136
+ - 3112 examples, 0 failures (up from 3080 on 0.8.1)
137
+ - 16 new regression tests for F1.3, F5.13, and F8.15
138
+ - 16 further regression tests for the review-feedback hardening
139
+ - Ruby 2.7 — 3.4 matrix green
140
+ - CodeQL clean; RuboCop clean across 266 files
141
+
24
142
  ## sdk-0.8.1 — 2026-04-08
25
143
 
26
144
  ### Fixed
@@ -3,6 +3,7 @@
3
3
  require 'net/http'
4
4
  require 'json'
5
5
  require 'uri'
6
+ require 'securerandom'
6
7
 
7
8
  module BSV
8
9
  module Network
@@ -14,16 +15,48 @@ module BSV
14
15
  # The HTTP client is injectable for testability. It must respond to
15
16
  # #request(uri, request) and return an object with #code and #body.
16
17
  class ARC
17
- REJECTED_STATUSES = %w[REJECTED DOUBLE_SPEND_ATTEMPTED].freeze
18
-
19
- def initialize(url, api_key: nil, http_client: nil)
18
+ # ARC response statuses that indicate the transaction was NOT accepted.
19
+ # Matches the TypeScript SDK's ARC broadcaster failure set (issue #305,
20
+ # finding F5.13). Prior to this fix, Ruby only recognised REJECTED and
21
+ # DOUBLE_SPEND_ATTEMPTED, silently treating INVALID / MALFORMED /
22
+ # MINED_IN_STALE_BLOCK responses as successful broadcasts.
23
+ REJECTED_STATUSES = %w[
24
+ REJECTED
25
+ DOUBLE_SPEND_ATTEMPTED
26
+ INVALID
27
+ MALFORMED
28
+ MINED_IN_STALE_BLOCK
29
+ ].freeze
30
+
31
+ # Substring match for orphan detection in txStatus or extraInfo fields.
32
+ ORPHAN_MARKER = 'ORPHAN'
33
+
34
+ # @param url [String] ARC base URL (without trailing slash)
35
+ # @param api_key [String, nil] optional bearer token for Authorization
36
+ # @param deployment_id [String, nil] optional deployment identifier for
37
+ # the +XDeployment-ID+ header; defaults to a per-instance random value
38
+ # @param callback_url [String, nil] optional +X-CallbackUrl+ for ARC
39
+ # status callbacks
40
+ # @param callback_token [String, nil] optional +X-CallbackToken+ for
41
+ # ARC status callback authentication
42
+ # @param http_client [#request, nil] injectable HTTP client for testing
43
+ def initialize(url, api_key: nil, deployment_id: nil, callback_url: nil,
44
+ callback_token: nil, http_client: nil)
20
45
  @url = url.chomp('/')
21
46
  @api_key = api_key
47
+ @deployment_id = deployment_id || "bsv-ruby-sdk-#{SecureRandom.hex(8)}"
48
+ @callback_url = callback_url
49
+ @callback_token = callback_token
22
50
  @http_client = http_client
23
51
  end
24
52
 
25
53
  # Submit a transaction to ARC.
26
54
  #
55
+ # The transaction is encoded as Extended Format (BRC-30) hex when every
56
+ # input has +source_satoshis+ and +source_locking_script+ populated,
57
+ # which lets ARC validate sighashes without fetching parents. Falls back
58
+ # to plain raw-tx hex when EF is unavailable.
59
+ #
27
60
  # @param tx [Transaction] the transaction to broadcast
28
61
  # @param wait_for [String, nil] ARC wait condition — one of
29
62
  # 'RECEIVED', 'STORED', 'ANNOUNCED_TO_NETWORK',
@@ -31,14 +64,18 @@ module BSV
31
64
  # connection open until the transaction reaches the requested
32
65
  # state (or times out). Defaults to nil (no wait).
33
66
  # @return [BroadcastResponse]
34
- # @raise [BroadcastError]
67
+ # @raise [BroadcastError] when ARC returns a non-2xx HTTP status or a
68
+ # rejected/orphan +txStatus+
35
69
  def broadcast(tx, wait_for: nil)
36
70
  uri = URI("#{@url}/v1/tx")
37
71
  request = Net::HTTP::Post.new(uri)
38
- request['Content-Type'] = 'application/octet-stream'
72
+ request['Content-Type'] = 'application/json'
73
+ request['XDeployment-ID'] = @deployment_id
39
74
  request['X-WaitFor'] = wait_for if wait_for
75
+ request['X-CallbackUrl'] = @callback_url if @callback_url
76
+ request['X-CallbackToken'] = @callback_token if @callback_token
40
77
  apply_auth_header(request)
41
- request.body = tx.to_binary
78
+ request.body = JSON.generate(rawTx: raw_tx_hex(tx))
42
79
 
43
80
  response = execute(uri, request)
44
81
  handle_broadcast_response(response)
@@ -49,6 +86,7 @@ module BSV
49
86
  def status(txid)
50
87
  uri = URI("#{@url}/v1/tx/#{txid}")
51
88
  request = Net::HTTP::Get.new(uri)
89
+ request['XDeployment-ID'] = @deployment_id
52
90
  apply_auth_header(request)
53
91
 
54
92
  response = execute(uri, request)
@@ -57,6 +95,15 @@ module BSV
57
95
 
58
96
  private
59
97
 
98
+ # Prefer Extended Format (BRC-30) hex so ARC can validate sighashes
99
+ # without fetching parent transactions. Falls back to plain raw-tx hex
100
+ # when any input lacks source_satoshis / source_locking_script.
101
+ def raw_tx_hex(tx)
102
+ tx.to_ef_hex
103
+ rescue ArgumentError
104
+ tx.to_hex
105
+ end
106
+
60
107
  def apply_auth_header(request)
61
108
  request['Authorization'] = "Bearer #{@api_key}" if @api_key
62
109
  end
@@ -83,20 +130,45 @@ module BSV
83
130
  )
84
131
  end
85
132
 
86
- tx_status = body['txStatus']
87
- if rejected_status?(tx_status)
133
+ if rejected_status?(body)
88
134
  raise BroadcastError.new(
89
- body['detail'] || body['title'] || tx_status,
135
+ body['detail'] || body['title'] || body['txStatus'],
90
136
  status_code: code,
91
137
  txid: body['txid']
92
138
  )
93
139
  end
94
140
 
141
+ # A 2xx response without a txid is a malformed ARC reply —
142
+ # `parse_json` falls back to `{'detail' => raw}` on non-JSON,
143
+ # which would otherwise produce a `BroadcastResponse` full of
144
+ # `nil`s and `success? => true`. That's the same silent
145
+ # success-as-failure class of bug F5.13 closed for explicit
146
+ # error statuses; closing it here for shape corruption too.
147
+ unless body['txid']
148
+ raise BroadcastError.new(
149
+ 'ARC returned a malformed 2xx response',
150
+ status_code: code
151
+ )
152
+ end
153
+
95
154
  build_response(body)
96
155
  end
97
156
 
98
- def rejected_status?(tx_status)
99
- REJECTED_STATUSES.include?(tx_status)
157
+ def rejected_status?(body)
158
+ # Case-insensitive match — the TypeScript reference
159
+ # (`ts-sdk/src/transaction/broadcasters/ARC.ts:155-166`) explicitly
160
+ # `.toUpperCase()`s both fields before membership / substring checks.
161
+ # ARC has a documented history of emitting values outside its own
162
+ # OpenAPI enum (e.g. `txStatus: "success"` for orphans in TS issue
163
+ # #105), so case normalisation is the defensive choice.
164
+ tx_status = body['txStatus'].to_s.upcase
165
+ return true if REJECTED_STATUSES.include?(tx_status)
166
+ return true if tx_status.include?(ORPHAN_MARKER)
167
+
168
+ extra_info = body['extraInfo'].to_s.upcase
169
+ return true if extra_info.include?(ORPHAN_MARKER)
170
+
171
+ false
100
172
  end
101
173
 
102
174
  def parse_json(raw)
@@ -10,11 +10,18 @@ module BSV
10
10
  module VarInt
11
11
  module_function
12
12
 
13
+ # Maximum value representable by a Bitcoin VarInt (unsigned 64-bit).
14
+ MAX_UINT64 = 0xFFFF_FFFF_FFFF_FFFF
15
+
13
16
  # Encode an integer as a Bitcoin VarInt.
14
17
  #
15
- # @param value [Integer] non-negative integer to encode
18
+ # @param value [Integer] non-negative integer to encode (0..2^64-1)
16
19
  # @return [String] encoded binary bytes
20
+ # @raise [ArgumentError] if +value+ is negative or exceeds 2^64-1
17
21
  def encode(value)
22
+ raise ArgumentError, "varint requires non-negative integer, got #{value}" if value.negative?
23
+ raise ArgumentError, "varint value #{value} exceeds uint64 max (#{MAX_UINT64})" if value > MAX_UINT64
24
+
18
25
  if value < 0xFD
19
26
  [value].pack('C')
20
27
  elsif value <= 0xFFFF
data/lib/bsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- VERSION = '0.8.1'
4
+ VERSION = '0.8.2'
5
5
  end
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.8.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison