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 +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/bsv/auth/certificate.rb +6 -2
- data/lib/bsv/identity/client.rb +1 -0
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +2 -2
- data/lib/bsv/mcp/tools/helpers.rb +2 -2
- data/lib/bsv/network/broadcast_error.rb +1 -0
- data/lib/bsv/network/broadcast_response.rb +1 -0
- data/lib/bsv/network/protocols/arc.rb +4 -3
- data/lib/bsv/network/protocols/taal_binary.rb +1 -0
- data/lib/bsv/network/protocols/woc_rest.rb +2 -1
- data/lib/bsv/overlay/lookup_resolver.rb +1 -0
- data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
- data/lib/bsv/overlay/types.rb +1 -0
- data/lib/bsv/primitives/hex.rb +64 -0
- data/lib/bsv/registry/client.rb +3 -1
- data/lib/bsv/registry/types.rb +1 -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 +122 -83
- data/lib/bsv/transaction/merkle_path.rb +54 -38
- data/lib/bsv/transaction/transaction.rb +81 -30
- 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-sdk.rb +14 -0
- metadata +1 -1
data/lib/bsv/transaction/beef.rb
CHANGED
|
@@ -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(
|
|
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
|
|
48
|
-
attr_reader :
|
|
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
|
|
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,
|
|
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
|
-
@
|
|
64
|
+
@known_wtxid = known_wtxid
|
|
64
65
|
@bump_index = bump_index
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
|
|
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
|
-
@
|
|
73
|
+
@known_wtxid
|
|
74
74
|
else
|
|
75
|
-
@transaction&.
|
|
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 :
|
|
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
|
-
@
|
|
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
|
|
137
|
-
#
|
|
138
|
-
beef.instance_variable_set(:@
|
|
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
|
|
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(
|
|
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
|
-
#
|
|
231
|
-
|
|
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
|
|
271
|
+
# @param wtxid [String] 32-byte wire-order wtxid
|
|
243
272
|
# @return [Transaction, nil] the matching transaction, or nil
|
|
244
|
-
def find_transaction(
|
|
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.
|
|
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
|
|
285
|
+
# for a BUMP whose level-0 leaves contain the wtxid.
|
|
255
286
|
#
|
|
256
|
-
# @param
|
|
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(
|
|
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.
|
|
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
|
|
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 ==
|
|
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
|
|
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(
|
|
275
|
-
|
|
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
|
|
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(
|
|
288
|
-
|
|
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
|
|
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(
|
|
300
|
-
to_atomic_binary(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
484
|
+
next if @transactions.any? { |bt| bt.wtxid == beef_tx.known_wtxid }
|
|
452
485
|
|
|
453
|
-
@transactions << BeefTx.new(format: FORMAT_TXID_ONLY,
|
|
486
|
+
@transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_wtxid: beef_tx.known_wtxid)
|
|
454
487
|
else
|
|
455
|
-
next if @transactions.any? { |bt| bt.
|
|
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
|
|
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(
|
|
488
|
-
|
|
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,
|
|
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.
|
|
557
|
+
bump.compute_root(bt.transaction.wtxid)
|
|
524
558
|
rescue ArgumentError
|
|
525
559
|
return false
|
|
526
560
|
end
|
|
527
561
|
end
|
|
528
562
|
|
|
529
|
-
|
|
563
|
+
known_wtxids = build_known_wtxids(allow_txid_only)
|
|
530
564
|
|
|
531
|
-
pending = @transactions.select { |bt| bt.transaction && !
|
|
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
|
-
|
|
573
|
+
known_wtxids.include?(input.prev_wtxid)
|
|
540
574
|
end
|
|
541
575
|
if all_inputs_known
|
|
542
|
-
|
|
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
|
-
|
|
588
|
-
@transactions.each_with_index { |bt, 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 =
|
|
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(&:
|
|
622
|
-
@txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.
|
|
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
|
-
#
|
|
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
|
-
|
|
702
|
+
known_wtxid = data.byteslice(offset, 32)
|
|
670
703
|
offset += 32
|
|
671
|
-
beef.transactions << BeefTx.new(format: FORMAT_TXID_ONLY,
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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.
|
|
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
|
|
772
|
-
def
|
|
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.
|
|
816
|
+
known.add(bt.wtxid)
|
|
778
817
|
when FORMAT_TXID_ONLY
|
|
779
|
-
known.add(bt.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
824
|
-
buf << beef_tx.
|
|
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)
|
|
@@ -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)
|