bsv-sdk 0.15.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/lib/bsv/auth/auth_middleware.rb +6 -6
  4. data/lib/bsv/auth/certificate.rb +22 -18
  5. data/lib/bsv/auth/master_certificate.rb +5 -5
  6. data/lib/bsv/auth/nonce.rb +13 -13
  7. data/lib/bsv/auth/peer.rb +53 -53
  8. data/lib/bsv/auth/verifiable_certificate.rb +1 -1
  9. data/lib/bsv/identity/client.rb +27 -32
  10. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +18 -12
  11. data/lib/bsv/mcp/tools/check_balance.rb +16 -4
  12. data/lib/bsv/mcp/tools/fetch_tx.rb +11 -4
  13. data/lib/bsv/mcp/tools/fetch_utxos.rb +16 -4
  14. data/lib/bsv/mcp/tools/helpers.rb +2 -2
  15. data/lib/bsv/network/arc.rb +13 -153
  16. data/lib/bsv/network/broadcast_error.rb +1 -0
  17. data/lib/bsv/network/broadcast_response.rb +1 -0
  18. data/lib/bsv/network/protocols/arc.rb +4 -3
  19. data/lib/bsv/network/protocols/taal_binary.rb +1 -0
  20. data/lib/bsv/network/protocols/woc_rest.rb +2 -1
  21. data/lib/bsv/network/whats_on_chain.rb +13 -107
  22. data/lib/bsv/overlay/admin_token_template.rb +4 -4
  23. data/lib/bsv/overlay/lookup_resolver.rb +1 -0
  24. data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
  25. data/lib/bsv/overlay/types.rb +1 -0
  26. data/lib/bsv/primitives/hex.rb +64 -0
  27. data/lib/bsv/registry/client.rb +26 -28
  28. data/lib/bsv/registry/types.rb +1 -0
  29. data/lib/bsv/script/interpreter/interpreter.rb +7 -0
  30. data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
  31. data/lib/bsv/script/push_drop_template.rb +4 -4
  32. data/lib/bsv/transaction/beef.rb +122 -83
  33. data/lib/bsv/transaction/merkle_path.rb +54 -38
  34. data/lib/bsv/transaction/transaction.rb +81 -30
  35. data/lib/bsv/transaction/transaction_input.rb +23 -18
  36. data/lib/bsv/version.rb +1 -1
  37. data/lib/bsv/wallet/errors.rb +47 -0
  38. data/lib/bsv/wallet/interface/brc100.rb +270 -0
  39. data/lib/bsv/wallet/interface.rb +9 -0
  40. data/lib/bsv/wallet/proto_wallet/key_deriver.rb +152 -0
  41. data/lib/bsv/wallet/proto_wallet/validators.rb +74 -0
  42. data/lib/bsv/wallet/proto_wallet.rb +327 -0
  43. data/lib/bsv/wallet.rb +16 -0
  44. data/lib/bsv-sdk.rb +18 -1
  45. metadata +22 -1
@@ -66,18 +66,16 @@ module BSV
66
66
 
67
67
  basket_name = basket_name_for(definition_type)
68
68
  create_result = @wallet.create_action(
69
- {
70
- description: "Register a new #{definition_type} definition",
71
- outputs: [
72
- {
73
- satoshis: Constants::TOKEN_AMOUNT,
74
- locking_script: locking_script.to_hex,
75
- output_description: "New #{definition_type} registration token",
76
- basket: basket_name
77
- }
78
- ],
79
- options: { randomize_outputs: false }
80
- },
69
+ description: "Register a new #{definition_type} definition",
70
+ outputs: [
71
+ {
72
+ satoshis: Constants::TOKEN_AMOUNT,
73
+ locking_script: locking_script.to_hex,
74
+ output_description: "New #{definition_type} registration token",
75
+ basket: basket_name
76
+ }
77
+ ],
78
+ options: { randomize_outputs: false },
81
79
  originator: @originator
82
80
  )
83
81
 
@@ -122,7 +120,7 @@ module BSV
122
120
  def list_own_registry_entries(definition_type)
123
121
  basket_name = basket_name_for(definition_type)
124
122
  list_result = @wallet.list_outputs(
125
- { basket: basket_name, include: 'entire transactions' },
123
+ basket: basket_name, include: 'entire transactions',
126
124
  originator: @originator
127
125
  )
128
126
 
@@ -157,21 +155,20 @@ module BSV
157
155
  verify_ownership(registered_definition)
158
156
 
159
157
  definition_type = registered_definition.definition_type
158
+ # Registry API boundary: outpoint uses display-order hex txid from RegisteredDefinition
160
159
  outpoint = "#{registered_definition.txid}.#{registered_definition.output_index}"
161
160
 
162
161
  create_result = @wallet.create_action(
163
- {
164
- description: "Revoke #{definition_type} definition: #{item_identifier(registered_definition)}",
165
- input_beef: registered_definition.beef,
166
- inputs: [
167
- {
168
- outpoint: outpoint,
169
- unlocking_script_length: BSV::Script::PushDropTemplate::Unlocker::ESTIMATED_LENGTH,
170
- input_description: "Revoking #{definition_type} token"
171
- }
172
- ],
173
- options: { randomize_outputs: false, no_send: true }
174
- },
162
+ description: "Revoke #{definition_type} definition: #{item_identifier(registered_definition)}",
163
+ input_beef: registered_definition.beef,
164
+ inputs: [
165
+ {
166
+ outpoint: outpoint,
167
+ unlocking_script_length: BSV::Script::PushDropTemplate::Unlocker::ESTIMATED_LENGTH,
168
+ input_description: "Revoking #{definition_type} token"
169
+ }
170
+ ],
171
+ options: { randomize_outputs: false, no_send: true },
175
172
  originator: @originator
176
173
  )
177
174
 
@@ -210,7 +207,7 @@ module BSV
210
207
  def identity_key
211
208
  return @identity_key if defined?(@identity_key)
212
209
 
213
- result = @wallet.get_public_key({ identity_key: true }, originator: @originator)
210
+ result = @wallet.get_public_key(identity_key: true, originator: @originator)
214
211
  @identity_key = result[:public_key] || result['public_key'] || result['publicKey'] || result[:publicKey]
215
212
  end
216
213
 
@@ -419,7 +416,7 @@ module BSV
419
416
 
420
417
  RegisteredDefinition.new(
421
418
  definition_data: definition_data,
422
- txid: tx.txid_hex,
419
+ txid: tx.txid_hex, # Registry API boundary: display-order hex txid
423
420
  output_index: output_idx,
424
421
  locking_script: locking_script.to_hex,
425
422
  beef: beef_raw,
@@ -438,6 +435,7 @@ module BSV
438
435
  outpoint_str = output[:outpoint] || output['outpoint']
439
436
  return nil unless outpoint_str
440
437
 
438
+ # Registry API boundary: outpoint string uses display-order hex txid by convention
441
439
  txid, output_idx_str = outpoint_str.split('.')
442
440
  output_idx = output_idx_str.to_i
443
441
 
@@ -561,7 +559,7 @@ module BSV
561
559
  #
562
560
  # @return [Symbol] :mainnet or :testnet
563
561
  def wallet_network
564
- result = @wallet.get_network({}, originator: @originator)
562
+ result = @wallet.get_network(originator: @originator)
565
563
  net_str = result[:network] || result['network'] || 'mainnet'
566
564
  net_str.to_sym
567
565
  end
@@ -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
@@ -108,14 +108,14 @@ module BSV
108
108
  counterparty: @counterparty
109
109
  }
110
110
  orig_kw = @originator ? { originator: @originator } : {}
111
- result = @wallet.create_signature(sig_args, **orig_kw)
111
+ result = @wallet.create_signature(**sig_args, **orig_kw)
112
112
 
113
113
  sig_bytes = result[:signature].pack('C*')
114
114
  sig_with_hashtype = sig_bytes + [sighash_type].pack('C')
115
115
 
116
116
  # Fetch the derived public key so the P2PKH unlock can include it
117
117
  pub_args = { protocol_id: @protocol_id, key_id: @key_id, counterparty: @counterparty }
118
- pub_result = @wallet.get_public_key(pub_args, **orig_kw)
118
+ pub_result = @wallet.get_public_key(**pub_args, **orig_kw)
119
119
  pubkey_bytes = [pub_result[:public_key]].pack('H*')
120
120
 
121
121
  BSV::Script::Script.pushdrop_unlock(
@@ -176,7 +176,7 @@ module BSV
176
176
  else
177
177
  pub_args = { protocol_id: protocol_id, key_id: key_id, counterparty: counterparty }
178
178
  orig_kw = @originator ? { originator: @originator } : {}
179
- pub_result = @wallet.get_public_key(pub_args, **orig_kw)
179
+ pub_result = @wallet.get_public_key(**pub_args, **orig_kw)
180
180
  [pub_result[:public_key]].pack('H*')
181
181
  end
182
182
 
@@ -187,7 +187,7 @@ module BSV
187
187
  data_to_sign = all_fields.reduce(''.b) { |acc, f| acc + f }.unpack('C*')
188
188
  sig_args = { data: data_to_sign, protocol_id: protocol_id, key_id: key_id, counterparty: counterparty }
189
189
  orig_kw = @originator ? { originator: @originator } : {}
190
- sig_result = @wallet.create_signature(sig_args, **orig_kw)
190
+ sig_result = @wallet.create_signature(**sig_args, **orig_kw)
191
191
  all_fields << sig_result[:signature].pack('C*')
192
192
  end
193
193
 
@@ -13,7 +13,7 @@ module BSV
13
13
  #
14
14
  # @example Parse a BEEF bundle and find a transaction
15
15
  # beef = BSV::Transaction::Beef.from_hex(beef_hex)
16
- # tx = beef.find_transaction(txid_bytes)
16
+ # tx = beef.find_transaction(wtxid)
17
17
  class Beef
18
18
  # @!group Version constants
19
19
 
@@ -44,37 +44,53 @@ module BSV
44
44
  # @return [Transaction, nil] the transaction (nil for TXID-only entries)
45
45
  attr_reader :transaction
46
46
 
47
- # @return [String, nil] 32-byte txid for TXID-only entries
48
- attr_reader :known_txid
47
+ # @return [String, nil] 32-byte wire-order wtxid for TXID-only entries
48
+ attr_reader :known_wtxid
49
49
 
50
50
  # @return [Integer, nil] index into the BEEF bumps array
51
51
  attr_reader :bump_index
52
52
 
53
53
  # @param format [Integer] format flag
54
54
  # @param transaction [Transaction, nil] the transaction
55
- # @param known_txid [String, nil] 32-byte txid for TXID-only entries
55
+ # @param known_wtxid [String, nil] 32-byte wire-order wtxid for TXID-only entries
56
56
  # @param bump_index [Integer, nil] index into the bumps array
57
57
  # @raise [ArgumentError] if format is FORMAT_RAW_TX_AND_BUMP without a bump_index
58
- def initialize(format:, transaction: nil, known_txid: nil, bump_index: nil)
58
+ def initialize(format:, transaction: nil, known_wtxid: nil, bump_index: nil)
59
59
  raise ArgumentError, 'FORMAT_RAW_TX_AND_BUMP requires a bump_index' if format == FORMAT_RAW_TX_AND_BUMP && bump_index.nil?
60
60
 
61
+ BSV::Primitives::Hex.validate_wtxid!(known_wtxid, name: 'known_wtxid') if known_wtxid
61
62
  @format = format
62
63
  @transaction = transaction
63
- @known_txid = known_txid
64
+ @known_wtxid = known_wtxid
64
65
  @bump_index = bump_index
65
66
  end
66
67
 
67
- # The transaction ID for this entry.
68
- #
69
- # @return [String, nil] 32-byte txid in display byte order
70
- def txid
68
+ # Wire-order transaction ID.
69
+ # @return [String, nil] 32-byte wtxid
70
+ def wtxid
71
71
  case @format
72
72
  when FORMAT_TXID_ONLY
73
- @known_txid
73
+ @known_wtxid
74
74
  else
75
- @transaction&.txid
75
+ @transaction&.wtxid
76
76
  end
77
77
  end
78
+
79
+ # Display-order transaction ID as binary bytes.
80
+ # @return [String, nil] 32-byte display-order txid
81
+ def txid
82
+ wtxid&.reverse
83
+ end
84
+
85
+ # Display-order transaction ID as a hex string.
86
+ #
87
+ # +dtxid+ always returns a 64-char hex string suitable for JSON
88
+ # and UI boundaries.
89
+ #
90
+ # @return [String, nil] hex-encoded transaction ID (display order)
91
+ def dtxid
92
+ wtxid&.reverse&.unpack1('H*')
93
+ end
78
94
  end
79
95
 
80
96
  # @return [Integer] BEEF version constant
@@ -86,8 +102,21 @@ module BSV
86
102
  # @return [Array<BeefTx>] the transactions in dependency order
87
103
  attr_reader :transactions
88
104
 
89
- # @return [String, nil] 32-byte subject txid (Atomic BEEF only)
90
- attr_reader :subject_txid
105
+ # @return [String, nil] 32-byte wire-order subject txid (Atomic BEEF only)
106
+ attr_reader :subject_wtxid
107
+
108
+ # Display-order subject txid as binary bytes (Atomic BEEF only).
109
+ # @return [String, nil] 32-byte display-order txid, or nil
110
+ def subject_txid
111
+ @subject_wtxid&.reverse
112
+ end
113
+
114
+ # Display-order subject txid as a hex string (Atomic BEEF only).
115
+ #
116
+ # @return [String, nil] hex-encoded display-order txid, or nil
117
+ def subject_dtxid
118
+ @subject_wtxid&.reverse&.unpack1('H*')
119
+ end
91
120
 
92
121
  # @param version [Integer] BEEF version constant (default: BEEF_V1, matching to_binary's
93
122
  # default for ARC compatibility; from_binary overwrites this with the parsed version)
@@ -97,7 +126,7 @@ module BSV
97
126
  @version = version
98
127
  @bumps = bumps
99
128
  @transactions = transactions
100
- @subject_txid = nil
129
+ @subject_wtxid = nil
101
130
  end
102
131
 
103
132
  # --- Deserialisation ---
@@ -133,9 +162,9 @@ module BSV
133
162
  raise ArgumentError, "truncated Atomic BEEF: need 36 bytes at offset #{offset}, got #{remaining}"
134
163
  end
135
164
 
136
- # Atomic BEEF stores the subject txid in internal byte order (little-endian
137
- # hash order), matching JS and Go SDKs. Reverse to display order for internal use.
138
- beef.instance_variable_set(:@subject_txid, data.byteslice(offset, 32).reverse)
165
+ # Atomic BEEF stores the subject txid in wire (internal / little-endian) byte order,
166
+ # matching JS and Go SDKs. Store as-is in @subject_wtxid (wire-order).
167
+ beef.instance_variable_set(:@subject_wtxid, data.byteslice(offset, 32))
139
168
  offset += 32
140
169
  inner_version = data.byteslice(offset, 4).unpack1('V')
141
170
  offset += 4
@@ -223,13 +252,13 @@ module BSV
223
252
 
224
253
  # Serialise as Atomic BEEF (BRC-95), wrapping V2 data with a subject txid.
225
254
  #
226
- # @param subject_txid [String] 32-byte subject transaction ID
255
+ # @param subject_wtxid [String] 32-byte wire-order subject transaction ID
227
256
  # @return [String] raw Atomic BEEF binary
228
- def to_atomic_binary(subject_txid)
257
+ def to_atomic_binary(subject_wtxid)
258
+ BSV::Primitives::Hex.validate_wtxid!(subject_wtxid, name: 'subject_wtxid')
229
259
  buf = [ATOMIC_BEEF].pack('V')
230
- # Write subject txid in internal byte order (reverse of display order),
231
- # matching JS and Go SDK conventions for Bitcoin binary formats.
232
- buf << subject_txid.b.reverse
260
+ # subject_wtxid is already in wire (internal) byte order write as-is.
261
+ buf << subject_wtxid.b
233
262
  # BRC-95: inner envelope is always V2
234
263
  buf << to_binary(version: BEEF_V2)
235
264
  buf
@@ -237,42 +266,45 @@ module BSV
237
266
 
238
267
  # --- Lookup ---
239
268
 
240
- # Find a transaction in the bundle by its transaction ID.
269
+ # Find a transaction in the bundle by its wire-order transaction ID.
241
270
  #
242
- # @param txid [String] 32-byte txid in display byte order
271
+ # @param wtxid [String] 32-byte wire-order wtxid
243
272
  # @return [Transaction, nil] the matching transaction, or nil
244
- def find_transaction(txid)
273
+ def find_transaction(wtxid)
274
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
275
+ BSV.logger&.debug { "[Beef] find_transaction: #{wtxid.reverse.unpack1('H*')} in #{@transactions.length} entries" }
245
276
  @transactions.each do |beef_tx|
246
- return beef_tx.transaction if beef_tx.transaction&.txid == txid
277
+ return beef_tx.transaction if beef_tx.wtxid == wtxid
247
278
  end
248
279
  nil
249
280
  end
250
281
 
251
- # Find the merkle path (BUMP) for a transaction by its txid.
282
+ # Find the merkle path (BUMP) for a transaction by its wire-order txid.
252
283
  #
253
284
  # First checks the transaction-table entries, then scans @bumps directly
254
- # for a BUMP whose level-0 leaves contain the txid.
285
+ # for a BUMP whose level-0 leaves contain the wtxid.
255
286
  #
256
- # @param txid [String] 32-byte txid in display byte order
287
+ # @param wtxid [String] 32-byte wire-order wtxid
257
288
  # @return [MerklePath, nil] the merkle path, or nil if not found
258
- def find_bump(txid)
289
+ def find_bump(wtxid)
290
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
259
291
  # Check transaction-table entries first (fast path)
260
- bt = @transactions.find { |entry| entry.txid == txid && entry.format == FORMAT_RAW_TX_AND_BUMP }
292
+ bt = @transactions.find { |entry| entry.wtxid == wtxid && entry.format == FORMAT_RAW_TX_AND_BUMP }
261
293
  return bt.transaction&.merkle_path || (bt.bump_index && @bumps[bt.bump_index]) if bt
262
294
 
263
- # F5.8: also scan @bumps directly for a path containing the txid leaf
264
- txid_internal = txid.reverse
295
+ # F5.8: also scan @bumps directly for a path containing the wtxid leaf
265
296
  @bumps.find do |bump|
266
- bump.path[0]&.any? { |leaf| leaf.hash == txid_internal }
297
+ bump.path[0]&.any? { |leaf| leaf.hash == wtxid }
267
298
  end
268
299
  end
269
300
 
270
301
  # Find a transaction with all source_transactions wired for signing.
271
302
  #
272
- # @param txid [String] 32-byte txid in display byte order
303
+ # @param wtxid [String] 32-byte wire-order wtxid
273
304
  # @return [Transaction, nil] the transaction with wired inputs, or nil
274
- def find_transaction_for_signing(txid)
275
- tx = find_transaction(txid)
305
+ def find_transaction_for_signing(wtxid)
306
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
307
+ tx = find_transaction(wtxid)
276
308
  return unless tx
277
309
 
278
310
  wire_inputs(tx)
@@ -282,10 +314,11 @@ module BSV
282
314
  # Find a transaction and recursively wire its ancestry (source transactions
283
315
  # and merkle paths) for atomic proof validation.
284
316
  #
285
- # @param txid [String] 32-byte txid in display byte order
317
+ # @param wtxid [String] 32-byte wire-order wtxid
286
318
  # @return [Transaction, nil] the transaction with full proof tree, or nil
287
- def find_atomic_transaction(txid)
288
- tx = find_transaction(txid)
319
+ def find_atomic_transaction(wtxid)
320
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
321
+ tx = find_transaction(wtxid)
289
322
  return unless tx
290
323
 
291
324
  wire_ancestry(tx)
@@ -294,10 +327,10 @@ module BSV
294
327
 
295
328
  # Serialise as Atomic BEEF (BRC-95) hex string.
296
329
  #
297
- # @param subject_txid [String] 32-byte subject transaction ID
330
+ # @param subject_wtxid [String] 32-byte wire-order subject transaction ID
298
331
  # @return [String] hex-encoded Atomic BEEF
299
- def to_atomic_hex(subject_txid)
300
- to_atomic_binary(subject_txid).unpack1('H*')
332
+ def to_atomic_hex(subject_wtxid)
333
+ to_atomic_binary(subject_wtxid).unpack1('H*')
301
334
  end
302
335
 
303
336
  # --- Merge operations ---
@@ -339,7 +372,7 @@ module BSV
339
372
  level0_internal = level0_leaves.map(&:hash).compact.to_set
340
373
  @transactions.each_with_index do |bt, i|
341
374
  next unless bt.format == FORMAT_RAW_TX && bt.transaction
342
- next unless level0_internal.include?(bt.transaction.txid.reverse)
375
+ next unless level0_internal.include?(bt.transaction.wtxid)
343
376
 
344
377
  bt.transaction.merkle_path ||= bump
345
378
  @transactions[i] = BeefTx.new(
@@ -362,10 +395,10 @@ module BSV
362
395
  # @param tx [Transaction] the transaction to merge
363
396
  # @return [BeefTx] the (possibly existing or upgraded) BeefTx entry
364
397
  def merge_transaction(tx)
365
- txid = tx.txid
398
+ wtxid = tx.wtxid
366
399
 
367
400
  # Check for existing entry and upgrade if a stronger format is available
368
- existing_idx = @transactions.index { |bt| bt.txid == txid }
401
+ existing_idx = @transactions.index { |bt| bt.wtxid == wtxid }
369
402
  if existing_idx
370
403
  existing = @transactions[existing_idx]
371
404
  upgraded = upgrade_beef_tx(existing, tx)
@@ -409,7 +442,7 @@ module BSV
409
442
  tx.merkle_path = @bumps[bump_index]
410
443
  end
411
444
 
412
- existing_idx = @transactions.index { |bt| bt.txid == tx.txid }
445
+ existing_idx = @transactions.index { |bt| bt.wtxid == tx.wtxid }
413
446
  if existing_idx
414
447
  existing = @transactions[existing_idx]
415
448
  upgraded = upgrade_beef_tx(existing, tx, bump_index: bump_index)
@@ -448,11 +481,11 @@ module BSV
448
481
  other.transactions.each do |beef_tx|
449
482
  case beef_tx.format
450
483
  when FORMAT_TXID_ONLY
451
- next if @transactions.any? { |bt| bt.txid == beef_tx.known_txid }
484
+ next if @transactions.any? { |bt| bt.wtxid == beef_tx.known_wtxid }
452
485
 
453
- @transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: beef_tx.known_txid)
486
+ @transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_wtxid: beef_tx.known_wtxid)
454
487
  else
455
- next if @transactions.any? { |bt| bt.txid == beef_tx.txid }
488
+ next if @transactions.any? { |bt| bt.wtxid == beef_tx.wtxid }
456
489
 
457
490
  if beef_tx.format == FORMAT_RAW_TX_AND_BUMP && beef_tx.bump_index
458
491
  new_idx = bump_remap[beef_tx.bump_index]
@@ -482,13 +515,14 @@ module BSV
482
515
 
483
516
  # Convert a transaction entry to TXID-only format.
484
517
  #
485
- # @param txid [String] 32-byte txid in display byte order
518
+ # @param wtxid [String] 32-byte wire-order wtxid
486
519
  # @return [BeefTx, nil] the converted entry, or nil if not found
487
- def make_txid_only(txid)
488
- idx = @transactions.index { |bt| bt.txid == txid }
520
+ def make_txid_only(wtxid)
521
+ BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
522
+ idx = @transactions.index { |bt| bt.wtxid == wtxid }
489
523
  return unless idx
490
524
 
491
- @transactions[idx] = BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: txid)
525
+ @transactions[idx] = BeefTx.new(format: FORMAT_TXID_ONLY, known_wtxid: wtxid)
492
526
  end
493
527
 
494
528
  # --- Validation ---
@@ -520,15 +554,15 @@ module BSV
520
554
 
521
555
  # The txid must appear as a leaf in the BUMP and compute a valid root
522
556
  begin
523
- bump.compute_root(bt.transaction.txid.reverse)
557
+ bump.compute_root(bt.transaction.wtxid)
524
558
  rescue ArgumentError
525
559
  return false
526
560
  end
527
561
  end
528
562
 
529
- known_txids = build_known_txids(allow_txid_only)
563
+ known_wtxids = build_known_wtxids(allow_txid_only)
530
564
 
531
- pending = @transactions.select { |bt| bt.transaction && !known_txids.include?(bt.txid) }
565
+ pending = @transactions.select { |bt| bt.transaction && !known_wtxids.include?(bt.wtxid) }
532
566
 
533
567
  # Iteratively resolve: if all inputs of a tx are known, it becomes known
534
568
  changed = true
@@ -536,10 +570,10 @@ module BSV
536
570
  changed = false
537
571
  pending.reject! do |bt|
538
572
  all_inputs_known = bt.transaction.inputs.all? do |input|
539
- known_txids.include?(input.prev_tx_id.reverse)
573
+ known_wtxids.include?(input.prev_wtxid)
540
574
  end
541
575
  if all_inputs_known
542
- known_txids.add(bt.txid)
576
+ known_wtxids.add(bt.wtxid)
543
577
  changed = true
544
578
  end
545
579
  all_inputs_known
@@ -584,8 +618,8 @@ module BSV
584
618
  def sort_transactions!
585
619
  return self if @transactions.length <= 1
586
620
 
587
- txid_index = {}
588
- @transactions.each_with_index { |bt, i| txid_index[bt.txid] = i }
621
+ wtxid_index = {}
622
+ @transactions.each_with_index { |bt, i| wtxid_index[bt.wtxid] = i }
589
623
 
590
624
  # Build adjacency: for each tx, which other txs must come before it?
591
625
  in_degree = Array.new(@transactions.length, 0)
@@ -595,7 +629,7 @@ module BSV
595
629
  next unless bt.transaction
596
630
 
597
631
  bt.transaction.inputs.each do |input|
598
- dep_idx = txid_index[input.prev_tx_id.reverse]
632
+ dep_idx = wtxid_index[input.prev_wtxid]
599
633
  next unless dep_idx
600
634
 
601
635
  dependents[dep_idx] << i
@@ -618,8 +652,8 @@ module BSV
618
652
 
619
653
  # F5.5: preserve unsortable (cyclic) transactions rather than silently dropping them
620
654
  if sorted.length < @transactions.length
621
- sorted_set = sorted.to_set(&:txid)
622
- @txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.txid) }
655
+ sorted_set = sorted.to_set(&:wtxid)
656
+ @txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.wtxid) }
623
657
  end
624
658
 
625
659
  @transactions = sorted
@@ -661,14 +695,13 @@ module BSV
661
695
 
662
696
  case format
663
697
  when FORMAT_TXID_ONLY
664
- # Wire stores txid in internal (little-endian) byte order;
665
- # reverse to display order so BeefTx#txid is consistent with
666
- # Transaction#txid across all format types.
698
+ # Wire stores txid in internal (little-endian / wire) byte order;
699
+ # store as-is in known_wtxid so it matches Transaction#wtxid.
667
700
  raise ArgumentError, 'truncated BEEF: not enough bytes for TXID_ONLY entry' if offset + 32 > data.bytesize
668
701
 
669
- known_txid = data.byteslice(offset, 32).reverse
702
+ known_wtxid = data.byteslice(offset, 32)
670
703
  offset += 32
671
- beef.transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: known_txid)
704
+ beef.transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_wtxid: known_wtxid)
672
705
  when FORMAT_RAW_TX_AND_BUMP
673
706
  bump_index, vi_size = VarInt.decode(data, offset)
674
707
  offset += vi_size
@@ -719,14 +752,20 @@ module BSV
719
752
  beef.transactions.each do |beef_tx|
720
753
  next unless beef_tx.transaction
721
754
 
722
- # Wire inputs to ancestors already in the map (BEEF is dependency-ordered)
755
+ # Wire inputs to ancestors already in the map (BEEF is dependency-ordered).
756
+ # Both prev_wtxid and wtxid are wire-order — no conversion needed.
723
757
  beef_tx.transaction.inputs.each do |input|
724
- # prev_tx_id is wire byte order; txid keys are display byte order (reversed)
725
- source = tx_map[input.prev_tx_id.reverse]
726
- input.source_transaction = source if source
758
+ source = tx_map[input.prev_wtxid]
759
+ next unless source
760
+
761
+ input.source_transaction = source
762
+ BSV.logger&.debug do
763
+ "[Beef] wired input #{input.prev_wtxid.reverse.unpack1('H*')}:#{input.prev_tx_out_index} " \
764
+ "-> source #{source.wtxid.reverse.unpack1('H*')}"
765
+ end
727
766
  end
728
767
 
729
- tx_map[beef_tx.transaction.txid] = beef_tx.transaction
768
+ tx_map[beef_tx.transaction.wtxid] = beef_tx.transaction
730
769
  end
731
770
  end
732
771
  end
@@ -768,15 +807,15 @@ module BSV
768
807
  # FORMAT_RAW_TX_AND_BUMP is already the strongest — no upgrade needed
769
808
  end
770
809
 
771
- # Build a set of txids that are "known" (proven or txid-only).
772
- def build_known_txids(allow_txid_only)
810
+ # Build a set of wire-order wtxids that are "known" (proven or txid-only).
811
+ def build_known_wtxids(allow_txid_only)
773
812
  known = Set.new
774
813
  @transactions.each do |bt|
775
814
  case bt.format
776
815
  when FORMAT_RAW_TX_AND_BUMP
777
- known.add(bt.txid)
816
+ known.add(bt.wtxid)
778
817
  when FORMAT_TXID_ONLY
779
- known.add(bt.txid) if allow_txid_only
818
+ known.add(bt.wtxid) if allow_txid_only
780
819
  end
781
820
  end
782
821
  known
@@ -787,7 +826,7 @@ module BSV
787
826
  tx.inputs.each do |input|
788
827
  next if input.source_transaction
789
828
 
790
- source = find_transaction(input.prev_tx_id.reverse)
829
+ source = find_transaction(input.prev_wtxid)
791
830
  input.source_transaction = source if source
792
831
  end
793
832
  end
@@ -799,7 +838,7 @@ module BSV
799
838
  next unless input.source_transaction
800
839
 
801
840
  source = input.source_transaction
802
- source.merkle_path ||= find_bump(source.txid)
841
+ source.merkle_path ||= find_bump(source.wtxid)
803
842
  wire_ancestry(source)
804
843
  end
805
844
  end
@@ -820,8 +859,8 @@ module BSV
820
859
  case beef_tx.format
821
860
  when FORMAT_TXID_ONLY
822
861
  buf << [FORMAT_TXID_ONLY].pack('C')
823
- # Reverse display-order txid back to wire (internal) byte order.
824
- buf << beef_tx.known_txid.reverse
862
+ # known_wtxid is already wire (internal) byte order.
863
+ buf << beef_tx.known_wtxid
825
864
  when FORMAT_RAW_TX_AND_BUMP
826
865
  buf << [FORMAT_RAW_TX_AND_BUMP].pack('C')
827
866
  buf << VarInt.encode(beef_tx.bump_index)