bsv-sdk 0.16.0 → 0.17.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: a76170f9d5dcdfc76945c20c541b556a02f3fb5a7214a8d6f777eb589f048461
4
- data.tar.gz: 57176276d41d430d654682dd22aef0f93bee9e2e3c57d4fb76d435dbd8c3e705
3
+ metadata.gz: 0c528b336af4735e247429423231a5c6a0dd681718e94d7a3d9598b65b75a6a0
4
+ data.tar.gz: 21efe7a43b3ba356953af1668cb35227d9c72bd3b17a1f08a54fac2f55f0d577
5
5
  SHA512:
6
- metadata.gz: fb7257bda0b14c14ab55d8da3a19fdc27a8eca113cfeb857487079d4e0f042ebbaef184e81382d50aed7a9efd69dc4d2cb2d77594b6e1ae5f3f6d047c01d2ab9
7
- data.tar.gz: 20f9e8fe53476ddd87900d1b58d00811da403703d5ae8cf6dc0f6d4a4836ab8a12e42ddce88c7323bd281e710770f4b7b6805cb842ab4f164c4b17559afa46e7
6
+ metadata.gz: 94b507d613413fbb96ab08f72d5db324ea33626f0f32af34f248b7d3133bd847aff1527de7ea1e68c1a1cbc1f5140f7654d4be99726348d85b852483ea00519a
7
+ data.tar.gz: 71c1fd8a86414cce69c1c4ec4fe1df52bab863c624ba70b806f37e2e016968eca9ac0244c42b0d52407c1817f56d9133403d3aec04a41bb39766776eb4b8c108
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ 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.17.0 — 2026-05-02
9
+
10
+ ### Breaking Changes
11
+ - Renamed `TransactionInput#prev_tx_id` to `#prev_wtxid` — all call sites must update (#678)
12
+ - Renamed MerklePath parameters from `txid_hex` to `dtxid_hex` throughout (#681)
13
+
14
+ ### Added
15
+ - `Transaction#wtxid` — wire-order transaction ID (raw SHA-256d bytes) (#673)
16
+ - `Transaction#dtxid` / `#dtxid_hex` — display-order hex aliases (#680)
17
+ - `TransactionInput#dtxid_hex` — display-order hex of the referenced transaction (#678)
18
+ - `TransactionInput.wtxid_from_hex` — convert display-order hex to wire-order bytes (#678)
19
+ - `Hex.validate_wtxid!` / `Hex.validate_dtxid_hex!` — runtime validation for wire/display txid formats (#685)
20
+ - `Hex.validate_hash32!` — general-purpose 32-byte binary hash validator (#686)
21
+ - `BSV.logger` — opt-in debug instrumentation with zero overhead when unused (#686)
22
+ - Debug logging at txid conversions, sighash preimage, script interpreter, ARC broadcast, BEEF ancestry, and ProtoWallet key derivation (#686, #688)
23
+
24
+ ### Changed
25
+ - All internal txid variables renamed to `wtxid` (wire-order) convention (#679)
26
+ - BEEF internals enforce wire-order throughout — no byte-reversals inside the bundle (#675)
27
+ - MerklePath `compute_root_hex` and `from_tsc` now validate hex input (#686)
28
+
29
+ ### Fixed
30
+ - `BeefTx#dtxid` now returns hex string (was returning binary) (#677)
31
+ - Certificate field naming aligned with BRC-52 convention (#677)
32
+
8
33
  ## 0.16.0 — 2026-05-01
9
34
 
10
35
  ### Breaking Changes
@@ -81,6 +81,9 @@ module BSV
81
81
  buf << [@subject].pack('H*')
82
82
  buf << [@certifier].pack('H*')
83
83
 
84
+ # Certificate binary format: revocation outpoint txid stored in display byte
85
+ # order (matching TS and Go SDKs). Go encodes via WriteBytesReverse(wire_order),
86
+ # TS encodes via toArray(display_hex) — both produce identical display-order bytes.
84
87
  txid_hex, output_index_str = @revocation_outpoint.to_s.split('.', 2)
85
88
  buf << [txid_hex].pack('H*')
86
89
  buf << BSV::Transaction::VarInt.encode(output_index_str.to_i)
@@ -124,7 +127,8 @@ module BSV
124
127
  certifier_bytes = data.byteslice(pos, 33)
125
128
  pos += 33
126
129
 
127
- txid_bytes = data.byteslice(pos, 32)
130
+ # Outpoint txid bytes — stored in display byte order (matching TS and Go SDKs).
131
+ outpoint_txid = data.byteslice(pos, 32)
128
132
  pos += 32
129
133
  output_index, vi_len = BSV::Transaction::VarInt.decode(data, pos)
130
134
  pos += vi_len
@@ -158,7 +162,7 @@ module BSV
158
162
  serial_number: Base64.strict_encode64(serial_bytes),
159
163
  subject: subject_bytes.unpack1('H*'),
160
164
  certifier: certifier_bytes.unpack1('H*'),
161
- revocation_outpoint: "#{txid_bytes.unpack1('H*')}.#{output_index}",
165
+ revocation_outpoint: "#{outpoint_txid.unpack1('H*')}.#{output_index}",
162
166
  fields: fields,
163
167
  signature: signature
164
168
  )
@@ -207,6 +207,7 @@ module BSV
207
207
  raise 'Revoke failed: no transaction found in BEEF' unless tx
208
208
  raise 'Revoke failed: outputIndex out of range' if output_idx >= tx.outputs.length
209
209
 
210
+ # Overlay API boundary: outpoint uses display-order hex txid as per overlay convention
210
211
  txid = tx.txid_hex
211
212
  outpoint = "#{txid}.#{output_idx}"
212
213
 
@@ -116,7 +116,7 @@ module BSV
116
116
  return Helpers.error_response("Broadcast failed: #{arc_result.message}") unless arc_result.success?
117
117
 
118
118
  result = {
119
- txid: arc_result.data[:txid],
119
+ txid: arc_result.data[:txid], # MCP tool boundary: display-order hex from ARC response
120
120
  tx_status: arc_result.data[:tx_status],
121
121
  hex: tx.to_hex
122
122
  }
@@ -154,7 +154,7 @@ module BSV
154
154
  selected_utxos.each do |utxo|
155
155
  locking_script = p2pkh_lock_for(sender_address)
156
156
  input = BSV::Transaction::TransactionInput.new(
157
- prev_tx_id: BSV::Transaction::TransactionInput.txid_from_hex(utxo.tx_hash),
157
+ prev_wtxid: BSV::Transaction::TransactionInput.wtxid_from_hex(utxo.tx_hash),
158
158
  prev_tx_out_index: utxo.tx_pos
159
159
  )
160
160
  input.source_satoshis = utxo.satoshis
@@ -25,7 +25,7 @@ module BSV
25
25
  # @return [Hash]
26
26
  def self.transaction_to_h(tx)
27
27
  {
28
- txid: tx.txid_hex,
28
+ txid: tx.txid_hex, # MCP tool boundary: display-order hex for human consumption
29
29
  version: tx.version,
30
30
  lock_time: tx.lock_time,
31
31
  inputs: tx.inputs.each_with_index.map { |inp, i| input_to_h(inp, i) },
@@ -38,7 +38,7 @@ module BSV
38
38
  def self.input_to_h(input, _index)
39
39
  unlock_script = input.unlocking_script
40
40
  {
41
- prev_txid: input.prev_tx_id.reverse.unpack1('H*'),
41
+ prev_txid: input.dtxid_hex,
42
42
  vout: input.prev_tx_out_index,
43
43
  script_hex: unlock_script ? unlock_script.to_hex : '',
44
44
  script_asm: unlock_script ? unlock_script.to_asm : '',
@@ -3,6 +3,7 @@
3
3
  module BSV
4
4
  module Network
5
5
  class BroadcastError < StandardError
6
+ # ARC API boundary: display-order hex txid as returned by the ARC error response.
6
7
  attr_reader :status_code, :txid, :arc_status
7
8
 
8
9
  def initialize(message, status_code: nil, txid: nil, arc_status: nil)
@@ -3,6 +3,7 @@
3
3
  module BSV
4
4
  module Network
5
5
  class BroadcastResponse
6
+ # ARC API boundary: display-order hex txid as returned by the ARC broadcast endpoint.
6
7
  attr_reader :txid, :tx_status, :message, :extra_info, :block_hash, :block_height, :timestamp, :competing_txs
7
8
 
8
9
  def initialize(attrs = {})
@@ -138,7 +138,8 @@ module BSV
138
138
  # lacks source_satoshis / source_locking_script.
139
139
  def ef_hex_with_fallback(tx)
140
140
  tx.to_ef_hex
141
- rescue ArgumentError
141
+ rescue ArgumentError => e
142
+ BSV.logger&.debug { "[ARC] EF serialisation failed: #{e.message} — falling back to raw hex" }
142
143
  tx.to_hex
143
144
  end
144
145
 
@@ -269,7 +270,7 @@ module BSV
269
270
  # same field set as broadcast responses rather than the raw parsed JSON.
270
271
  # Also checks for rejection status and missing txid (malformed 2xx).
271
272
  #
272
- # @param txid [String] the transaction ID to query
273
+ # @param txid [String] ARC API boundary: display-order hex transaction ID to query
273
274
  # @return [Result::Success, Result::Error, Result::NotFound]
274
275
  def call_get_tx_status(txid, **)
275
276
  response = default_call(:get_tx_status, txid)
@@ -306,7 +307,7 @@ module BSV
306
307
  # @return [Hash]
307
308
  def arc_data_from(body)
308
309
  {
309
- txid: body['txid'],
310
+ txid: body['txid'], # ARC API boundary: display-order hex from the ARC JSON response
310
311
  tx_status: body['txStatus'],
311
312
  message: body['title'],
312
313
  extra_info: body['extraInfo'],
@@ -53,6 +53,7 @@ module BSV
53
53
  code = response.code.to_i
54
54
  body = parse_json_body(response.body)
55
55
 
56
+ # TAAL API boundary: display-order hex txid from the TAAL broadcast response
56
57
  return Result::Success.new(data: { txid: body['txid'] }) if already_known?(body) && body['txid']
57
58
 
58
59
  retryable = code == 429 || (500..599).cover?(code)
@@ -175,7 +175,7 @@ module BSV
175
175
  # The +script_hash:+ keyword is accepted for future fallback support
176
176
  # but not used in this implementation.
177
177
  #
178
- # @param txid [String] transaction ID
178
+ # @param txid [String] WoC API boundary: display-order hex transaction ID
179
179
  # @param vout [Integer] output index
180
180
  # @param script_hash [String, nil] ignored
181
181
  # @return [Result::Success<Boolean>, Result::Error, Result::NotFound]
@@ -242,6 +242,7 @@ module BSV
242
242
  return result unless result.success?
243
243
 
244
244
  # WoC returns plain-text txid — result.data is the raw body string
245
+ # WoC API boundary: display-order hex txid returned as plain text
245
246
  Result::Success.new(data: { txid: result.data.to_s.strip })
246
247
  end
247
248
 
@@ -334,6 +334,7 @@ module BSV
334
334
  beef_data = output['beef'] || output[:beef]
335
335
  output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
336
336
 
337
+ # Overlay API boundary: outpoint key uses display-order txid bytes (via Transaction#txid)
337
338
  txid =
338
339
  begin
339
340
  beef = parse_beef(beef_data)
@@ -110,7 +110,7 @@ module BSV
110
110
 
111
111
  OverlayBroadcastResult.new(
112
112
  status: 'success',
113
- txid: tx.txid_hex,
113
+ txid: tx.txid_hex, # Overlay API boundary: display-order hex txid for the broadcast result
114
114
  message: "Sent to #{successful.size} Overlay Service host(s)."
115
115
  )
116
116
  end
@@ -82,6 +82,7 @@ module BSV
82
82
  # @return [String] result status ('success' or 'error')
83
83
  attr_reader :status
84
84
 
85
+ # Overlay API boundary: display-order hex txid echoed from the broadcast response.
85
86
  # @return [String, nil] transaction identifier (present on success)
86
87
  attr_reader :txid
87
88
 
@@ -72,6 +72,70 @@ module BSV
72
72
  def self.encode(bytes)
73
73
  bytes.unpack1('H*')
74
74
  end
75
+
76
+ # Validate that +value+ is a 32-byte wire-order transaction ID.
77
+ #
78
+ # @param value [String] expected 32-byte binary string
79
+ # @param name [String] label for the error message (e.g. +'prev_wtxid'+)
80
+ # @return [String] the input value (pass-through for chaining)
81
+ # @raise [ArgumentError] if +value+ is not a 32-byte binary string
82
+ def self.validate_wtxid!(value, name: 'wtxid')
83
+ unless value.is_a?(String) && value.bytesize == 32
84
+ hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
85
+ ' (looks like a hex txid — use wtxid_from_hex to convert)'
86
+ else
87
+ ''
88
+ end
89
+ size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
90
+ raise ArgumentError,
91
+ "expected 32-byte wire-order wtxid for #{name}, got #{size}#{hint}"
92
+ end
93
+ value
94
+ end
95
+
96
+ # Validate that +value+ is a 32-byte binary hash.
97
+ #
98
+ # General-purpose validator for any 32-byte hash (merkle nodes, roots,
99
+ # etc.) — not specific to transaction IDs. For txid-specific validation
100
+ # use {.validate_wtxid!} or {.validate_dtxid_hex!} instead.
101
+ #
102
+ # @param value [String] expected 32-byte binary string
103
+ # @param name [String] label for the error message
104
+ # @return [String] the input value (pass-through for chaining)
105
+ # @raise [ArgumentError] if +value+ is not a 32-byte binary string
106
+ def self.validate_hash32!(value, name: 'hash')
107
+ unless value.is_a?(String) && value.bytesize == 32
108
+ hint = if value.is_a?(String) && value.bytesize == 64 && value.match?(HEX_RE)
109
+ ' (looks like hex — decode it first)'
110
+ else
111
+ ''
112
+ end
113
+ size = value.is_a?(String) ? "#{value.bytesize}-byte string" : value.class.to_s
114
+ raise ArgumentError,
115
+ "expected 32-byte hash for #{name}, got #{size}#{hint}"
116
+ end
117
+ value
118
+ end
119
+
120
+ # Validate that +value+ is a 64-character display-order hex transaction ID.
121
+ #
122
+ # @param value [String] expected 64-char hex string
123
+ # @param name [String] label for the error message (e.g. +'dtxid_hex'+)
124
+ # @return [String] the input value (pass-through for chaining)
125
+ # @raise [ArgumentError] if +value+ is not a 64-char hex string
126
+ def self.validate_dtxid_hex!(value, name: 'dtxid_hex')
127
+ unless value.is_a?(String) && value.length == 64 && value.match?(HEX_RE)
128
+ hint = if value.is_a?(String) && value.bytesize == 32 && !value.match?(HEX_RE)
129
+ ' (looks like binary bytes — use dtxid_hex or unpack to convert)'
130
+ else
131
+ ''
132
+ end
133
+ size = value.is_a?(String) ? "#{value.length}-char string" : value.class.to_s
134
+ raise ArgumentError,
135
+ "expected 64-char display-order hex for #{name}, got #{size}#{hint}"
136
+ end
137
+ value
138
+ end
75
139
  end
76
140
  end
77
141
  end
@@ -155,6 +155,7 @@ module BSV
155
155
  verify_ownership(registered_definition)
156
156
 
157
157
  definition_type = registered_definition.definition_type
158
+ # Registry API boundary: outpoint uses display-order hex txid from RegisteredDefinition
158
159
  outpoint = "#{registered_definition.txid}.#{registered_definition.output_index}"
159
160
 
160
161
  create_result = @wallet.create_action(
@@ -415,7 +416,7 @@ module BSV
415
416
 
416
417
  RegisteredDefinition.new(
417
418
  definition_data: definition_data,
418
- txid: tx.txid_hex,
419
+ txid: tx.txid_hex, # Registry API boundary: display-order hex txid
419
420
  output_index: output_idx,
420
421
  locking_script: locking_script.to_hex,
421
422
  beef: beef_raw,
@@ -434,6 +435,7 @@ module BSV
434
435
  outpoint_str = output[:outpoint] || output['outpoint']
435
436
  return nil unless outpoint_str
436
437
 
438
+ # Registry API boundary: outpoint string uses display-order hex txid by convention
437
439
  txid, output_idx_str = outpoint_str.split('.')
438
440
  output_idx = output_idx_str.to_i
439
441
 
@@ -188,6 +188,7 @@ module BSV
188
188
  # @return [String] the definition type (see {DefinitionType})
189
189
  attr_reader :definition_type
190
190
 
191
+ # Registry API boundary: display-order hex txid from the outpoint string held in the registry token.
191
192
  # @return [String] transaction ID of the containing UTXO
192
193
  attr_reader :txid
193
194
 
@@ -89,10 +89,12 @@ module BSV
89
89
 
90
90
  def execute
91
91
  scripts = [@unlock_script, @lock_script]
92
+ script_names = %w[unlock_script lock_script]
92
93
 
93
94
  scripts.each_with_index do |script, script_idx|
94
95
  @current_script = script
95
96
  chunks = script.chunks
97
+ BSV.logger&.debug { "[Interpreter] === #{script_names[script_idx]} (#{chunks.length} chunks) ===" }
96
98
 
97
99
  chunks.each_with_index do |chunk, chunk_idx|
98
100
  @current_chunk_idx = chunk_idx
@@ -115,6 +117,7 @@ module BSV
115
117
  end
116
118
 
117
119
  check_final_stack
120
+ BSV.logger&.debug { "[Interpreter] final stack: #{@dstack.length} items -> success" }
118
121
  true
119
122
  end
120
123
 
@@ -156,6 +159,10 @@ module BSV
156
159
  return
157
160
  end
158
161
 
162
+ BSV.logger&.debug do
163
+ name = Opcodes.name_for(opcode) || format('0x%02x', opcode)
164
+ "[Interpreter] #{name} (stack: #{@dstack.length})"
165
+ end
159
166
  dispatch_opcode(opcode, chunk)
160
167
  end
161
168
 
@@ -126,7 +126,13 @@ module BSV
126
126
 
127
127
  pubkey = BSV::Primitives::PublicKey.from_bytes(pubkey_bytes)
128
128
  hash = @tx.sighash(@input_index, sighash_type, subscript: sub_script)
129
- pubkey.verify(hash, sig)
129
+ result = pubkey.verify(hash, sig)
130
+ BSV.logger&.debug do
131
+ pk_hex = pubkey_bytes.unpack1('H*')
132
+ "[Interpreter] CHECKSIG: sighash_type=0x#{format('%02x', sighash_type)} " \
133
+ "pubkey=#{pk_hex[0, 8]}...#{pk_hex[-4..]} result=#{result}"
134
+ end
135
+ result
130
136
  rescue ArgumentError
131
137
  false
132
138
  end