bsv-sdk 0.16.0 → 0.18.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 +4 -4
- data/CHANGELOG.md +53 -0
- data/lib/bsv/auth/certificate.rb +6 -2
- data/lib/bsv/auth/get_verifiable_certificates.rb +6 -6
- data/lib/bsv/auth/peer.rb +10 -4
- data/lib/bsv/auth/session_manager.rb +81 -5
- data/lib/bsv/identity/client.rb +5 -2
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +4 -4
- data/lib/bsv/mcp/tools/check_balance.rb +2 -2
- data/lib/bsv/mcp/tools/fetch_utxos.rb +2 -2
- data/lib/bsv/mcp/tools/helpers.rb +2 -2
- data/lib/bsv/network/broadcast_error.rb +2 -0
- data/lib/bsv/network/broadcast_response.rb +4 -1
- data/lib/bsv/network/protocol.rb +56 -4
- data/lib/bsv/network/protocols/arc.rb +10 -6
- data/lib/bsv/network/protocols/chaintracks.rb +6 -2
- data/lib/bsv/network/protocols/jungle_bus.rb +52 -0
- data/lib/bsv/network/protocols/ordinals.rb +110 -8
- data/lib/bsv/network/protocols/taal_binary.rb +18 -4
- data/lib/bsv/network/protocols/woc_rest.rb +166 -85
- data/lib/bsv/network/protocols.rb +1 -0
- data/lib/bsv/network/provider.rb +36 -5
- data/lib/bsv/network/providers/gorilla_pool.rb +42 -20
- data/lib/bsv/network/providers/taal.rb +38 -15
- data/lib/bsv/network/providers/whats_on_chain.rb +42 -21
- data/lib/bsv/network/utxo.rb +8 -2
- data/lib/bsv/overlay/lookup_resolver.rb +5 -4
- data/lib/bsv/overlay/topic_broadcaster.rb +2 -2
- data/lib/bsv/overlay/types.rb +2 -0
- data/lib/bsv/primitives/hex.rb +64 -0
- data/lib/bsv/registry/client.rb +10 -8
- data/lib/bsv/registry/types.rb +2 -0
- data/lib/bsv/script/interpreter/interpreter.rb +7 -0
- data/lib/bsv/script/interpreter/operations/crypto.rb +7 -1
- data/lib/bsv/transaction/beef.rb +223 -147
- data/lib/bsv/transaction/merkle_path.rb +54 -38
- data/lib/bsv/transaction/transaction.rb +103 -40
- data/lib/bsv/transaction/transaction_input.rb +23 -18
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/interface/brc100.rb +5 -2
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +2 -0
- data/lib/bsv/wallet/proto_wallet.rb +6 -0
- data/lib/bsv/wire_format.rb +40 -14
- data/lib/bsv-sdk.rb +14 -0
- metadata +4 -3
|
@@ -21,7 +21,8 @@ module BSV
|
|
|
21
21
|
# @!attribute [r] hash
|
|
22
22
|
# @return [String, nil] 32-byte hash (nil when duplicate)
|
|
23
23
|
# @!attribute [r] txid
|
|
24
|
-
# @return [Boolean] whether this leaf
|
|
24
|
+
# @return [Boolean] BRC-74 flag — whether this leaf represents a transaction in
|
|
25
|
+
# the merkle tree (the 0x02 bit in the BRC-74 serialisation). Not a txid value.
|
|
25
26
|
# @!attribute [r] duplicate
|
|
26
27
|
# @return [Boolean] whether this leaf duplicates its sibling
|
|
27
28
|
class PathElement
|
|
@@ -29,7 +30,9 @@ module BSV
|
|
|
29
30
|
|
|
30
31
|
# @param offset [Integer] position index within the tree level
|
|
31
32
|
# @param hash [String, nil] 32-byte hash (nil when duplicate)
|
|
32
|
-
# @param txid [Boolean]
|
|
33
|
+
# @param txid [Boolean] BRC-74 flag — true when this leaf represents a transaction
|
|
34
|
+
# in the tree (the 0x02 bit in the BRC-74 serialisation). This is NOT a txid
|
|
35
|
+
# value; it is a boolean presence flag mandated by the BRC-74 specification.
|
|
33
36
|
# @param duplicate [Boolean] whether this leaf duplicates its sibling
|
|
34
37
|
def initialize(offset:, hash: nil, txid: false, duplicate: false)
|
|
35
38
|
@offset = offset
|
|
@@ -147,25 +150,26 @@ module BSV
|
|
|
147
150
|
# @example Convert a WoC TSC proof
|
|
148
151
|
# tsc = JSON.parse(woc_response).first
|
|
149
152
|
# mp = BSV::Transaction::MerklePath.from_tsc(
|
|
150
|
-
#
|
|
153
|
+
# dtxid_hex: tsc['txOrId'],
|
|
151
154
|
# index: tsc['index'],
|
|
152
155
|
# nodes: tsc['nodes'],
|
|
153
156
|
# block_height: 612_251
|
|
154
157
|
# )
|
|
155
158
|
# mp.compute_root_hex(tsc['txOrId']) #=> the block's merkle root
|
|
156
159
|
#
|
|
157
|
-
# @param
|
|
160
|
+
# @param dtxid_hex [String] hex-encoded transaction ID in display byte order
|
|
158
161
|
# @param index [Integer] the transaction's position in the block
|
|
159
162
|
# @param nodes [Array<String>] sibling hashes leaf-to-root, each a 32-byte
|
|
160
163
|
# hex string in display byte order, or +"*"+ for a duplicate node
|
|
161
164
|
# @param block_height [Integer] the block's height (TSC carries the block
|
|
162
165
|
# hash; the caller must look up the height separately)
|
|
163
166
|
# @return [MerklePath] a BRC-74 merkle path equivalent to the TSC proof
|
|
164
|
-
def self.from_tsc(
|
|
165
|
-
|
|
167
|
+
def self.from_tsc(dtxid_hex:, index:, nodes:, block_height:)
|
|
168
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
|
|
169
|
+
wtxid = [dtxid_hex].pack('H*').reverse
|
|
166
170
|
|
|
167
171
|
# Level 0 always contains the txid leaf.
|
|
168
|
-
level0 = [PathElement.new(offset: index, hash:
|
|
172
|
+
level0 = [PathElement.new(offset: index, hash: wtxid, txid: true)]
|
|
169
173
|
|
|
170
174
|
# A single-tx block has no siblings — the txid IS the merkle root.
|
|
171
175
|
return new(block_height: block_height, path: [level0]) if nodes.empty?
|
|
@@ -193,6 +197,7 @@ module BSV
|
|
|
193
197
|
if node == '*'
|
|
194
198
|
PathElement.new(offset: offset, duplicate: true)
|
|
195
199
|
else
|
|
200
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(node, name: "TSC merkle node at offset #{offset}")
|
|
196
201
|
PathElement.new(offset: offset, hash: [node].pack('H*').reverse)
|
|
197
202
|
end
|
|
198
203
|
end
|
|
@@ -240,17 +245,22 @@ module BSV
|
|
|
240
245
|
|
|
241
246
|
# Recompute the merkle root from this path and a transaction ID.
|
|
242
247
|
#
|
|
243
|
-
# @param
|
|
244
|
-
# @return [String] 32-byte merkle root in
|
|
245
|
-
# @raise [ArgumentError] if the
|
|
246
|
-
def compute_root(
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
# @param wtxid [String, nil] 32-byte txid in wire byte order (auto-detected if nil)
|
|
249
|
+
# @return [String] 32-byte merkle root in wire byte order
|
|
250
|
+
# @raise [ArgumentError] if the wtxid is not found in the path
|
|
251
|
+
def compute_root(wtxid = nil)
|
|
252
|
+
wtxid ||= @path[0].find(&:hash)&.hash
|
|
253
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid') if wtxid
|
|
254
|
+
BSV.logger&.debug do
|
|
255
|
+
dtxid = wtxid&.reverse&.unpack1('H*')
|
|
256
|
+
"[MerklePath] compute_root: dtxid=#{dtxid} block_height=#{@block_height} levels=#{@path.length}"
|
|
257
|
+
end
|
|
258
|
+
return wtxid if @path.length == 1 && @path[0].length == 1
|
|
249
259
|
|
|
250
260
|
indexed = build_indexed_path
|
|
251
261
|
|
|
252
|
-
tx_leaf = @path[0].find { |l| l.hash ==
|
|
253
|
-
raise ArgumentError, 'the BUMP does not contain the
|
|
262
|
+
tx_leaf = @path[0].find { |l| l.hash == wtxid }
|
|
263
|
+
raise ArgumentError, 'the BUMP does not contain the given wtxid' unless tx_leaf
|
|
254
264
|
|
|
255
265
|
working = tx_leaf.hash
|
|
256
266
|
index = tx_leaf.offset
|
|
@@ -289,11 +299,12 @@ module BSV
|
|
|
289
299
|
|
|
290
300
|
# Recompute the merkle root and return it as a hex string.
|
|
291
301
|
#
|
|
292
|
-
# @param
|
|
302
|
+
# @param dtxid_hex [String, nil] hex-encoded txid (display order)
|
|
293
303
|
# @return [String] hex-encoded merkle root (display order)
|
|
294
|
-
def compute_root_hex(
|
|
295
|
-
|
|
296
|
-
|
|
304
|
+
def compute_root_hex(dtxid_hex = nil)
|
|
305
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'compute_root_hex dtxid_hex') if dtxid_hex
|
|
306
|
+
wtxid = dtxid_hex ? [dtxid_hex].pack('H*').reverse : nil
|
|
307
|
+
compute_root(wtxid).reverse.unpack1('H*')
|
|
297
308
|
end
|
|
298
309
|
|
|
299
310
|
# --- Verification ---
|
|
@@ -312,22 +323,27 @@ module BSV
|
|
|
312
323
|
# and accepts immature ones — the opposite of the intended behaviour. The correct
|
|
313
324
|
# logic is: reject when `current_height - block_height < 100` (immature).
|
|
314
325
|
#
|
|
315
|
-
# @param
|
|
326
|
+
# @param dtxid_hex [String] hex-encoded transaction ID (display order)
|
|
316
327
|
# @param chain_tracker [ChainTracker] chain tracker to verify the root against
|
|
317
328
|
# @return [Boolean] true if the computed root matches the block at this height
|
|
318
|
-
def verify(
|
|
319
|
-
|
|
320
|
-
|
|
329
|
+
def verify(dtxid_hex, chain_tracker)
|
|
330
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
|
|
331
|
+
wtxid = [dtxid_hex].pack('H*').reverse
|
|
332
|
+
tx_leaf = @path[0].find { |l| l.hash == wtxid }
|
|
321
333
|
|
|
322
334
|
# Offset 0 in a block's merkle tree is always the coinbase transaction —
|
|
323
335
|
# a Bitcoin protocol invariant. Apply the 100-block maturity check.
|
|
324
|
-
if
|
|
336
|
+
if tx_leaf&.offset&.zero?
|
|
325
337
|
current = chain_tracker.current_height
|
|
326
338
|
return false if current - @block_height < 100
|
|
327
339
|
end
|
|
328
340
|
|
|
329
|
-
root_hex = compute_root_hex(
|
|
330
|
-
chain_tracker.valid_root_for_height?(root_hex, @block_height)
|
|
341
|
+
root_hex = compute_root_hex(dtxid_hex)
|
|
342
|
+
valid = chain_tracker.valid_root_for_height?(root_hex, @block_height)
|
|
343
|
+
BSV.logger&.debug do
|
|
344
|
+
"[MerklePath] verify: dtxid=#{dtxid_hex} height=#{@block_height} root=#{root_hex} valid=#{valid}"
|
|
345
|
+
end
|
|
346
|
+
valid
|
|
331
347
|
end
|
|
332
348
|
|
|
333
349
|
# --- Combine ---
|
|
@@ -445,24 +461,24 @@ module BSV
|
|
|
445
461
|
#
|
|
446
462
|
# Matches the TS SDK's +MerklePath.extract+ behaviour.
|
|
447
463
|
#
|
|
448
|
-
# @param
|
|
464
|
+
# @param wtxid_hashes [Array<String>] 32-byte txids in wire byte
|
|
449
465
|
# order (reverse of display order). To pass hex strings, use
|
|
450
|
-
# +
|
|
466
|
+
# +dtxid_hexes.map { |h| [h].pack('H*').reverse }+.
|
|
451
467
|
# @return [MerklePath] a new trimmed compound path proving only the
|
|
452
468
|
# requested txids
|
|
453
|
-
# @raise [ArgumentError] if +
|
|
469
|
+
# @raise [ArgumentError] if +wtxid_hashes+ is empty, any requested
|
|
454
470
|
# txid is not present in the source path's level 0, or the
|
|
455
471
|
# extracted path's root does not match the source root
|
|
456
|
-
def extract(
|
|
457
|
-
raise ArgumentError, 'at least one
|
|
472
|
+
def extract(wtxid_hashes)
|
|
473
|
+
raise ArgumentError, 'at least one wtxid must be provided to extract' if wtxid_hashes.empty?
|
|
458
474
|
|
|
459
475
|
original_root = compute_root
|
|
460
476
|
indexed = build_indexed_path
|
|
461
477
|
|
|
462
478
|
# Build a level-0 hash → offset lookup
|
|
463
|
-
|
|
479
|
+
wtxid_to_offset = {}
|
|
464
480
|
@path[0].each do |leaf|
|
|
465
|
-
|
|
481
|
+
wtxid_to_offset[leaf.hash] = leaf.offset if leaf.hash
|
|
466
482
|
end
|
|
467
483
|
|
|
468
484
|
max_offset = @path[0].map(&:offset).max || 0
|
|
@@ -470,15 +486,15 @@ module BSV
|
|
|
470
486
|
|
|
471
487
|
needed = Array.new(tree_height) { {} }
|
|
472
488
|
|
|
473
|
-
|
|
474
|
-
tx_offset =
|
|
489
|
+
wtxid_hashes.each do |wtxid_hash|
|
|
490
|
+
tx_offset = wtxid_to_offset[wtxid_hash]
|
|
475
491
|
if tx_offset.nil?
|
|
476
492
|
raise ArgumentError,
|
|
477
|
-
"
|
|
493
|
+
"wtxid #{wtxid_hash.reverse.unpack1('H*')} not found in the Merkle Path"
|
|
478
494
|
end
|
|
479
495
|
|
|
480
|
-
# Level 0: the
|
|
481
|
-
needed[0][tx_offset] = PathElement.new(offset: tx_offset, hash:
|
|
496
|
+
# Level 0: the transaction leaf itself + its tree sibling
|
|
497
|
+
needed[0][tx_offset] = PathElement.new(offset: tx_offset, hash: wtxid_hash, txid: true)
|
|
482
498
|
sib0_offset = tx_offset ^ 1
|
|
483
499
|
unless needed[0].key?(sib0_offset)
|
|
484
500
|
sib = offset_leaf(indexed, 0, sib0_offset)
|
|
@@ -329,16 +329,20 @@ module BSV
|
|
|
329
329
|
ancestors = collect_ancestors
|
|
330
330
|
|
|
331
331
|
bump_index_by_height = build_beef_bumps(beef, ancestors)
|
|
332
|
+
BSV.logger&.debug do
|
|
333
|
+
proven = ancestors.count(&:merkle_path)
|
|
334
|
+
"[Transaction] BEEF: #{ancestors.length} ancestors, #{proven} proven " \
|
|
335
|
+
"across #{bump_index_by_height.length} block heights"
|
|
336
|
+
end
|
|
332
337
|
|
|
333
338
|
ancestors.each do |tx|
|
|
334
339
|
entry = if tx.merkle_path
|
|
335
|
-
Beef::
|
|
336
|
-
format: Beef::FORMAT_RAW_TX_AND_BUMP,
|
|
340
|
+
Beef::ProvenTxEntry.new(
|
|
337
341
|
transaction: tx,
|
|
338
342
|
bump_index: bump_index_by_height.fetch(tx.merkle_path.block_height)
|
|
339
343
|
)
|
|
340
344
|
else
|
|
341
|
-
Beef::
|
|
345
|
+
Beef::RawTxEntry.new(transaction: tx)
|
|
342
346
|
end
|
|
343
347
|
beef.transactions << entry
|
|
344
348
|
end
|
|
@@ -357,11 +361,11 @@ module BSV
|
|
|
357
361
|
# full ancestry wired, including late-bound BUMP attachment.
|
|
358
362
|
#
|
|
359
363
|
# For Atomic BEEFs (BRC-95), the subject transaction is identified by
|
|
360
|
-
# the embedded
|
|
361
|
-
# with a raw tx entry is used as the subject.
|
|
364
|
+
# the embedded +subject_wtxid+ field. For plain BEEFs, the last
|
|
365
|
+
# transaction with a raw tx entry is used as the subject.
|
|
362
366
|
#
|
|
363
367
|
# Uses +find_atomic_transaction+ so that FORMAT_RAW_TX ancestors whose
|
|
364
|
-
#
|
|
368
|
+
# wtxid appears as a leaf in a separately-stored BUMP get their
|
|
365
369
|
# +merkle_path+ wired correctly — a gap not covered by the initial
|
|
366
370
|
# +wire_source_transactions+ pass in +Beef.from_binary+.
|
|
367
371
|
#
|
|
@@ -370,11 +374,11 @@ module BSV
|
|
|
370
374
|
# or nil if the BEEF is empty or contains no raw transaction entries
|
|
371
375
|
def self.from_beef(data)
|
|
372
376
|
beef = Beef.from_binary(data)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return nil unless
|
|
377
|
+
subject_wtxid = beef.subject_wtxid ||
|
|
378
|
+
beef.transactions.reverse.find { |bt| !bt.is_a?(Beef::TxidOnlyEntry) }&.wtxid
|
|
379
|
+
return nil unless subject_wtxid
|
|
376
380
|
|
|
377
|
-
beef.find_atomic_transaction(
|
|
381
|
+
beef.find_atomic_transaction(subject_wtxid)
|
|
378
382
|
end
|
|
379
383
|
|
|
380
384
|
# Parse a BEEF hex string and return the subject transaction.
|
|
@@ -388,24 +392,38 @@ module BSV
|
|
|
388
392
|
|
|
389
393
|
# --- Transaction ID ---
|
|
390
394
|
|
|
391
|
-
#
|
|
395
|
+
# Wire-order transaction ID (raw SHA-256d of the serialised tx).
|
|
392
396
|
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
# order (natural hash). Use +.reverse+ to convert between the two.
|
|
397
|
+
# Used by BEEF, BUMPs, and merkle paths, which all work in wire byte order
|
|
398
|
+
# to match {TransactionInput#prev_wtxid}.
|
|
396
399
|
#
|
|
397
|
-
# @return [String] 32-byte transaction ID in
|
|
398
|
-
def
|
|
399
|
-
BSV::Primitives::Digest.sha256d(to_binary)
|
|
400
|
+
# @return [String] 32-byte transaction ID in wire byte order
|
|
401
|
+
def wtxid
|
|
402
|
+
id = BSV::Primitives::Digest.sha256d(to_binary)
|
|
403
|
+
BSV.logger&.debug { "[Transaction] wtxid computed (dtxid=#{id.reverse.unpack1('H*')})" }
|
|
404
|
+
id
|
|
400
405
|
end
|
|
401
406
|
|
|
402
407
|
# The transaction ID as a hex string (display byte order).
|
|
403
408
|
#
|
|
404
|
-
# @return [String] hex-encoded transaction ID
|
|
409
|
+
# @return [String] 64-char hex-encoded transaction ID (display order)
|
|
405
410
|
def txid_hex
|
|
406
|
-
|
|
411
|
+
wtxid.reverse.unpack1('H*')
|
|
407
412
|
end
|
|
408
413
|
|
|
414
|
+
# Display-order transaction ID as a hex string.
|
|
415
|
+
#
|
|
416
|
+
# Mirrors the wallet gem's +DisplayTxid+ pattern. +dtxid+ always returns
|
|
417
|
+
# a 64-char hex string suitable for JSON and UI boundaries.
|
|
418
|
+
#
|
|
419
|
+
# @return [String] hex-encoded transaction ID (display order)
|
|
420
|
+
alias dtxid txid_hex
|
|
421
|
+
|
|
422
|
+
# Display-order transaction ID as a hex string (alias for {#dtxid}).
|
|
423
|
+
#
|
|
424
|
+
# @return [String] hex-encoded transaction ID (display order)
|
|
425
|
+
alias dtxid_hex txid_hex
|
|
426
|
+
|
|
409
427
|
# --- Sighash (BIP-143 with FORKID) ---
|
|
410
428
|
|
|
411
429
|
# Build the BIP-143 sighash preimage for an input.
|
|
@@ -421,6 +439,29 @@ module BSV
|
|
|
421
439
|
raise ArgumentError, 'only SIGHASH_FORKID types are supported' unless sighash_type & Sighash::FORK_ID != 0
|
|
422
440
|
|
|
423
441
|
input = @inputs[input_index]
|
|
442
|
+
raise ArgumentError, "no input at index #{input_index}" if input.nil?
|
|
443
|
+
|
|
444
|
+
# Resolve source data from wired source_transaction when not explicitly set.
|
|
445
|
+
if input.source_transaction
|
|
446
|
+
source_output = input.source_transaction.outputs[input.prev_tx_out_index]
|
|
447
|
+
if source_output
|
|
448
|
+
input.source_satoshis ||= source_output.satoshis
|
|
449
|
+
input.source_locking_script ||= source_output.locking_script
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
if input.source_satoshis.nil?
|
|
454
|
+
raise ArgumentError,
|
|
455
|
+
"input #{input_index} has nil source_satoshis — " \
|
|
456
|
+
'set it or wire source_transaction before computing sighash'
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
unless subscript || input.source_locking_script
|
|
460
|
+
raise ArgumentError,
|
|
461
|
+
"input #{input_index} has nil source_locking_script — " \
|
|
462
|
+
'set it or wire source_transaction before computing sighash'
|
|
463
|
+
end
|
|
464
|
+
|
|
424
465
|
base_type = sighash_type & Sighash::MASK
|
|
425
466
|
anyone = sighash_type.anybits?(Sighash::ANYONE_CAN_PAY)
|
|
426
467
|
|
|
@@ -456,6 +497,19 @@ module BSV
|
|
|
456
497
|
# 10. sighash type (4 LE) — includes FORKID flag
|
|
457
498
|
buf << [sighash_type].pack('V')
|
|
458
499
|
|
|
500
|
+
BSV.logger&.debug do
|
|
501
|
+
hp = buf.byteslice(4, 32).unpack1('H*')
|
|
502
|
+
hs = buf.byteslice(36, 32).unpack1('H*')
|
|
503
|
+
op = input.outpoint_binary.unpack1('H*')
|
|
504
|
+
sc = script_bytes.unpack1('H*')
|
|
505
|
+
ho = buf.byteslice(-40, 32).unpack1('H*')
|
|
506
|
+
"[Sighash] input=#{input_index} type=0x#{format('%02x', sighash_type)} " \
|
|
507
|
+
"version=#{@version} hashPrevouts=#{hp} hashSequence=#{hs} " \
|
|
508
|
+
"outpoint=#{op} scriptCode=#{sc[0, 40]}#{'...' if sc.length > 40} " \
|
|
509
|
+
"value=#{input.source_satoshis} seq=#{input.sequence} " \
|
|
510
|
+
"hashOutputs=#{ho} locktime=#{@lock_time}"
|
|
511
|
+
end
|
|
512
|
+
|
|
459
513
|
buf
|
|
460
514
|
end
|
|
461
515
|
|
|
@@ -466,7 +520,9 @@ module BSV
|
|
|
466
520
|
# @param subscript [Script::Script, nil] override locking script for the input
|
|
467
521
|
# @return [String] 32-byte sighash digest
|
|
468
522
|
def sighash(input_index, sighash_type = Sighash::ALL_FORK_ID, subscript: nil)
|
|
469
|
-
BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
|
|
523
|
+
digest = BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
|
|
524
|
+
BSV.logger&.debug { "[Sighash] digest=#{digest.unpack1('H*')}" }
|
|
525
|
+
digest
|
|
470
526
|
end
|
|
471
527
|
|
|
472
528
|
# --- Signing ---
|
|
@@ -580,17 +636,17 @@ module BSV
|
|
|
580
636
|
|
|
581
637
|
until queue.empty?
|
|
582
638
|
tx = queue.shift
|
|
583
|
-
|
|
584
|
-
next if verified[
|
|
639
|
+
wtxid = tx.wtxid
|
|
640
|
+
next if verified[wtxid]
|
|
585
641
|
|
|
586
642
|
# Merkle path short-circuit: proven transaction needs no input verification
|
|
587
643
|
if tx.merkle_path
|
|
588
|
-
unless tx.merkle_path.verify(
|
|
644
|
+
unless tx.merkle_path.verify(tx.txid_hex, chain_tracker)
|
|
589
645
|
raise VerificationError.new(:invalid_merkle_proof,
|
|
590
|
-
"invalid merkle proof for transaction #{
|
|
646
|
+
"invalid merkle proof for transaction #{tx.txid_hex}")
|
|
591
647
|
end
|
|
592
648
|
|
|
593
|
-
verified[
|
|
649
|
+
verified[wtxid] = true
|
|
594
650
|
next
|
|
595
651
|
end
|
|
596
652
|
|
|
@@ -622,13 +678,13 @@ module BSV
|
|
|
622
678
|
|
|
623
679
|
# Enqueue source transaction for verification if not yet verified
|
|
624
680
|
source_tx = input.source_transaction
|
|
625
|
-
queue << source_tx if source_tx && !verified[source_tx.
|
|
681
|
+
queue << source_tx if source_tx && !verified[source_tx.wtxid]
|
|
626
682
|
end
|
|
627
683
|
|
|
628
684
|
# Output ≤ input check
|
|
629
685
|
verify_output_constraint(tx)
|
|
630
686
|
|
|
631
|
-
verified[
|
|
687
|
+
verified[wtxid] = true
|
|
632
688
|
end
|
|
633
689
|
|
|
634
690
|
true
|
|
@@ -777,19 +833,19 @@ module BSV
|
|
|
777
833
|
end
|
|
778
834
|
|
|
779
835
|
def verify_input_requirements(tx, input, index)
|
|
780
|
-
|
|
836
|
+
dtxid_hex = tx.txid_hex
|
|
781
837
|
if input.unlocking_script.nil?
|
|
782
838
|
raise VerificationError.new(:missing_source,
|
|
783
|
-
"input #{index} of transaction #{
|
|
839
|
+
"input #{index} of transaction #{dtxid_hex} has no unlocking script")
|
|
784
840
|
end
|
|
785
841
|
if input.source_locking_script.nil?
|
|
786
842
|
raise VerificationError.new(:missing_source,
|
|
787
|
-
"input #{index} of transaction #{
|
|
843
|
+
"input #{index} of transaction #{dtxid_hex} has no source locking script")
|
|
788
844
|
end
|
|
789
845
|
return unless input.source_satoshis.nil?
|
|
790
846
|
|
|
791
847
|
raise VerificationError.new(:missing_source,
|
|
792
|
-
"input #{index} of transaction #{
|
|
848
|
+
"input #{index} of transaction #{dtxid_hex} has no source satoshis")
|
|
793
849
|
end
|
|
794
850
|
|
|
795
851
|
def verify_fee(fee_model)
|
|
@@ -843,7 +899,7 @@ module BSV
|
|
|
843
899
|
|
|
844
900
|
# Collect this transaction and all its ancestors in dependency order
|
|
845
901
|
# (ancestors first, self last). Stops recursion at transactions with
|
|
846
|
-
# a merkle_path (proven leaves). Deduplicates by
|
|
902
|
+
# a merkle_path (proven leaves). Deduplicates by wtxid (wire-order bytes).
|
|
847
903
|
def collect_ancestors
|
|
848
904
|
seen = {}
|
|
849
905
|
result = []
|
|
@@ -852,18 +908,25 @@ module BSV
|
|
|
852
908
|
end
|
|
853
909
|
|
|
854
910
|
def collect_ancestors_recursive(tx, seen, result)
|
|
855
|
-
|
|
856
|
-
return if seen.key?(
|
|
911
|
+
wtxid = tx.wtxid
|
|
912
|
+
return if seen.key?(wtxid)
|
|
857
913
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
914
|
+
if tx.merkle_path
|
|
915
|
+
BSV.logger&.debug do
|
|
916
|
+
"[Transaction] ancestor: #{tx.dtxid_hex} proven at height #{tx.merkle_path.block_height} (leaf stop)"
|
|
917
|
+
end
|
|
918
|
+
else
|
|
919
|
+
tx.inputs.each_with_index do |input, idx|
|
|
920
|
+
unless input.source_transaction
|
|
921
|
+
BSV.logger&.debug { "[Transaction] ancestor: #{tx.dtxid_hex} input #{idx} has no source_transaction (skipped)" }
|
|
922
|
+
next
|
|
923
|
+
end
|
|
861
924
|
|
|
862
925
|
collect_ancestors_recursive(input.source_transaction, seen, result)
|
|
863
926
|
end
|
|
864
927
|
end
|
|
865
928
|
|
|
866
|
-
seen[
|
|
929
|
+
seen[wtxid] = true
|
|
867
930
|
result << tx
|
|
868
931
|
end
|
|
869
932
|
|
|
@@ -886,8 +949,8 @@ module BSV
|
|
|
886
949
|
merged = txs.first.merkle_path.dup
|
|
887
950
|
txs.drop(1).each { |t| merged.combine(t.merkle_path) }
|
|
888
951
|
|
|
889
|
-
|
|
890
|
-
clean = merged.extract(
|
|
952
|
+
wtxid_hashes = txs.map(&:wtxid)
|
|
953
|
+
clean = merged.extract(wtxid_hashes)
|
|
891
954
|
|
|
892
955
|
bump_index_by_height[height] = beef.bumps.length
|
|
893
956
|
beef.bumps << clean
|
|
@@ -8,8 +8,8 @@ module BSV
|
|
|
8
8
|
# output index (the "outpoint"), and provide an unlocking script to
|
|
9
9
|
# satisfy the locking script conditions.
|
|
10
10
|
class TransactionInput
|
|
11
|
-
# @return [String] 32-byte transaction ID of the output being spent
|
|
12
|
-
attr_reader :
|
|
11
|
+
# @return [String] 32-byte wire-order transaction ID of the output being spent
|
|
12
|
+
attr_reader :prev_wtxid
|
|
13
13
|
|
|
14
14
|
# @return [Integer] index of the output within the previous transaction
|
|
15
15
|
attr_reader :prev_tx_out_index
|
|
@@ -32,15 +32,17 @@ module BSV
|
|
|
32
32
|
# @return [UnlockingScriptTemplate, nil] template for deferred signing
|
|
33
33
|
attr_accessor :unlocking_script_template
|
|
34
34
|
|
|
35
|
-
# @param
|
|
35
|
+
# @param prev_wtxid [String] 32-byte wire-order transaction ID
|
|
36
36
|
# @param prev_tx_out_index [Integer] output index in the previous transaction
|
|
37
37
|
# @param unlocking_script [Script::Script, nil] unlocking script (nil if unsigned)
|
|
38
38
|
# @param sequence [Integer] sequence number
|
|
39
|
-
def initialize(
|
|
40
|
-
|
|
39
|
+
def initialize(prev_wtxid:, prev_tx_out_index:, unlocking_script: nil, sequence: 0xFFFFFFFF)
|
|
40
|
+
BSV::Primitives::Hex.validate_wtxid!(prev_wtxid, name: 'prev_wtxid')
|
|
41
|
+
@prev_wtxid = prev_wtxid.b
|
|
41
42
|
@prev_tx_out_index = prev_tx_out_index
|
|
42
43
|
@unlocking_script = unlocking_script
|
|
43
44
|
@sequence = sequence
|
|
45
|
+
BSV.logger&.debug { "[TransactionInput] prev_wtxid set: #{dtxid_hex}:#{@prev_tx_out_index}" }
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# Serialise the input to its binary wire format.
|
|
@@ -48,7 +50,7 @@ module BSV
|
|
|
48
50
|
# @return [String] binary input (outpoint + varint + script + sequence)
|
|
49
51
|
def to_binary
|
|
50
52
|
script_bytes = @unlocking_script ? @unlocking_script.to_binary : ''.b
|
|
51
|
-
@
|
|
53
|
+
@prev_wtxid +
|
|
52
54
|
[@prev_tx_out_index].pack('V') +
|
|
53
55
|
VarInt.encode(script_bytes.bytesize) +
|
|
54
56
|
script_bytes +
|
|
@@ -66,7 +68,7 @@ module BSV
|
|
|
66
68
|
"truncated input: need 36 bytes for outpoint at offset #{offset}, got #{data.bytesize - offset}"
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
prev_wtxid = data.byteslice(offset, 32)
|
|
70
72
|
prev_tx_out_index = data.byteslice(offset + 32, 4).unpack1('V')
|
|
71
73
|
offset += 36
|
|
72
74
|
|
|
@@ -90,7 +92,7 @@ module BSV
|
|
|
90
92
|
|
|
91
93
|
total = 36 + vi_size + script_len + 4
|
|
92
94
|
input = new(
|
|
93
|
-
|
|
95
|
+
prev_wtxid: prev_wtxid,
|
|
94
96
|
prev_tx_out_index: prev_tx_out_index,
|
|
95
97
|
unlocking_script: unlocking_script,
|
|
96
98
|
sequence: sequence
|
|
@@ -98,26 +100,29 @@ module BSV
|
|
|
98
100
|
[input, total]
|
|
99
101
|
end
|
|
100
102
|
|
|
101
|
-
# Convert a hex transaction ID to
|
|
103
|
+
# Convert a display-order hex transaction ID to wire-order bytes.
|
|
102
104
|
#
|
|
103
105
|
# @param hex [String] hex-encoded transaction ID (display order)
|
|
104
|
-
# @return [String] 32-byte transaction ID in
|
|
105
|
-
def self.
|
|
106
|
-
|
|
106
|
+
# @return [String] 32-byte transaction ID in wire byte order
|
|
107
|
+
def self.wtxid_from_hex(hex)
|
|
108
|
+
BSV::Primitives::Hex.validate_dtxid_hex!(hex, name: 'wtxid_from_hex input')
|
|
109
|
+
wtxid = [hex].pack('H*').reverse
|
|
110
|
+
BSV.logger&.debug { "[TransactionInput] wtxid_from_hex: #{hex} -> #{wtxid.bytesize}B wire-order" }
|
|
111
|
+
wtxid
|
|
107
112
|
end
|
|
108
113
|
|
|
109
|
-
# Serialise the outpoint (
|
|
114
|
+
# Serialise the outpoint (prev_wtxid + output index) as binary.
|
|
110
115
|
#
|
|
111
116
|
# @return [String] 36-byte outpoint
|
|
112
117
|
def outpoint_binary
|
|
113
|
-
@
|
|
118
|
+
@prev_wtxid + [@prev_tx_out_index].pack('V')
|
|
114
119
|
end
|
|
115
120
|
|
|
116
|
-
# The previous transaction ID in
|
|
121
|
+
# The previous transaction ID in display-order hex.
|
|
117
122
|
#
|
|
118
|
-
# @return [String] hex-encoded transaction ID
|
|
119
|
-
def
|
|
120
|
-
@
|
|
123
|
+
# @return [String] hex-encoded transaction ID (display order)
|
|
124
|
+
def dtxid_hex
|
|
125
|
+
@prev_wtxid.reverse.unpack1('H*')
|
|
121
126
|
end
|
|
122
127
|
end
|
|
123
128
|
end
|
data/lib/bsv/version.rb
CHANGED
|
@@ -38,10 +38,12 @@ module BSV
|
|
|
38
38
|
# - :basket [String] optional basket name for UTXO tracking
|
|
39
39
|
# - :custom_instructions [String] application-specific context
|
|
40
40
|
# - :tags [Array<String>] output tags for filtering
|
|
41
|
-
# @return [Hash] :txid, :tx, :no_send_change,
|
|
41
|
+
# @return [Hash] BRC-100 spec-mandated keys: :txid (display-order hex), :tx, :no_send_change,
|
|
42
|
+
# :send_with_results, :signable_transaction
|
|
42
43
|
def create_action(description:, input_beef: nil, inputs: nil, outputs: nil,
|
|
43
44
|
lock_time: nil, version: nil, labels: nil,
|
|
44
45
|
sign_and_process: true, accept_delayed_broadcast: true,
|
|
46
|
+
# BRC-100 spec-mandated parameter names — display-order hex txids
|
|
45
47
|
trust_self: nil, known_txids: nil, return_txid_only: false,
|
|
46
48
|
no_send: false, no_send_change: nil, send_with: nil,
|
|
47
49
|
randomize_outputs: true, originator: nil)
|
|
@@ -53,7 +55,8 @@ module BSV
|
|
|
53
55
|
# @param spends [Hash{Integer => Hash}] input index => { unlocking_script:, sequence_number: }
|
|
54
56
|
# @param reference [String] reference returned by {#create_action}
|
|
55
57
|
def sign_action(spends:, reference:,
|
|
56
|
-
accept_delayed_broadcast: true,
|
|
58
|
+
accept_delayed_broadcast: true,
|
|
59
|
+
return_txid_only: false, # BRC-100 spec-mandated parameter name
|
|
57
60
|
no_send: false, send_with: nil, originator: nil)
|
|
58
61
|
raise NotImplementedError
|
|
59
62
|
end
|
|
@@ -45,6 +45,7 @@ module BSV
|
|
|
45
45
|
Validators.validate_protocol_id!(protocol_id)
|
|
46
46
|
Validators.validate_key_id!(key_id)
|
|
47
47
|
invoice = compute_invoice_number(protocol_id, key_id)
|
|
48
|
+
BSV.logger&.debug { "[KeyDeriver] derive_public_key: invoice=#{invoice.inspect} for_self=#{for_self}" }
|
|
48
49
|
counterparty_pub = resolve_counterparty(counterparty)
|
|
49
50
|
|
|
50
51
|
if for_self
|
|
@@ -64,6 +65,7 @@ module BSV
|
|
|
64
65
|
Validators.validate_protocol_id!(protocol_id)
|
|
65
66
|
Validators.validate_key_id!(key_id)
|
|
66
67
|
invoice = compute_invoice_number(protocol_id, key_id)
|
|
68
|
+
BSV.logger&.debug { "[KeyDeriver] derive_private_key: invoice=#{invoice.inspect}" }
|
|
67
69
|
counterparty_pub = resolve_counterparty(counterparty)
|
|
68
70
|
@root_key.derive_child(counterparty_pub, invoice)
|
|
69
71
|
end
|
|
@@ -127,6 +127,7 @@ module BSV
|
|
|
127
127
|
counterparty: nil, privileged: false, privileged_reason: nil,
|
|
128
128
|
seek_permission: true, originator: nil)
|
|
129
129
|
counterparty ||= 'anyone'
|
|
130
|
+
BSV.logger&.debug { "[ProtoWallet] create_signature: protocol=#{protocol_id} key_id=#{key_id.inspect} counterparty=#{counterparty}" }
|
|
130
131
|
priv_key = @key_deriver.derive_private_key(protocol_id, key_id, counterparty)
|
|
131
132
|
|
|
132
133
|
hash = if hash_to_directly_sign
|
|
@@ -156,6 +157,10 @@ module BSV
|
|
|
156
157
|
privileged: false, privileged_reason: nil,
|
|
157
158
|
seek_permission: true, originator: nil)
|
|
158
159
|
counterparty ||= 'self'
|
|
160
|
+
BSV.logger&.debug do
|
|
161
|
+
"[ProtoWallet] verify_signature: protocol=#{protocol_id} key_id=#{key_id.inspect} " \
|
|
162
|
+
"counterparty=#{counterparty} for_self=#{for_self}"
|
|
163
|
+
end
|
|
159
164
|
|
|
160
165
|
pub_key = @key_deriver.derive_public_key(
|
|
161
166
|
protocol_id, key_id, counterparty, for_self: for_self
|
|
@@ -169,6 +174,7 @@ module BSV
|
|
|
169
174
|
|
|
170
175
|
sig = BSV::Primitives::Signature.from_der(bytes_to_string(signature))
|
|
171
176
|
valid = pub_key.verify(hash, sig)
|
|
177
|
+
BSV.logger&.debug { "[ProtoWallet] verify_signature result=#{valid}" }
|
|
172
178
|
|
|
173
179
|
raise InvalidSignatureError unless valid
|
|
174
180
|
|