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 +4 -4
- data/CHANGELOG.md +118 -0
- data/lib/bsv/network/arc.rb +83 -11
- data/lib/bsv/transaction/var_int.rb +8 -1
- data/lib/bsv/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 534417702a1c6fe35494ff3d7bc028d302452fc21312747cd83167137fab43c7
|
|
4
|
+
data.tar.gz: d7aca5b47d7f853f41142d2ae9087351bf0160beb0b25959330e7e380bbb5d92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/bsv/network/arc.rb
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
87
|
-
if rejected_status?(tx_status)
|
|
133
|
+
if rejected_status?(body)
|
|
88
134
|
raise BroadcastError.new(
|
|
89
|
-
body['detail'] || body['title'] ||
|
|
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?(
|
|
99
|
-
|
|
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