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
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
|
|
|
@@ -36,44 +36,119 @@ module BSV
|
|
|
36
36
|
|
|
37
37
|
# @!endgroup
|
|
38
38
|
|
|
39
|
-
#
|
|
39
|
+
# Abstract base class for a single entry in a BEEF bundle.
|
|
40
|
+
#
|
|
41
|
+
# Subclasses represent the three wire formats:
|
|
42
|
+
# - {RawTxEntry} — raw transaction without a merkle proof
|
|
43
|
+
# - {ProvenTxEntry} — raw transaction with an associated BUMP index
|
|
44
|
+
# - {TxidOnlyEntry} — transaction ID only (no raw data)
|
|
45
|
+
#
|
|
46
|
+
# @abstract Subclass and implement {#wtxid} and {#format_flag}.
|
|
40
47
|
class BeefTx
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
def initialize; end
|
|
49
|
+
|
|
50
|
+
# Wire-order transaction ID.
|
|
51
|
+
# @return [String, nil] 32-byte wtxid
|
|
52
|
+
# @abstract
|
|
53
|
+
def wtxid
|
|
54
|
+
raise NotImplementedError, "#{self.class}#wtxid is not implemented"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Display-order transaction ID as a hex string.
|
|
58
|
+
#
|
|
59
|
+
# +dtxid+ always returns a 64-char hex string suitable for JSON
|
|
60
|
+
# and UI boundaries.
|
|
61
|
+
#
|
|
62
|
+
# @return [String, nil] hex-encoded transaction ID (display order)
|
|
63
|
+
def dtxid
|
|
64
|
+
wtxid&.reverse&.unpack1('H*')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Wire-protocol format integer for serialisation.
|
|
68
|
+
# @return [Integer]
|
|
69
|
+
# @abstract
|
|
70
|
+
def format_flag
|
|
71
|
+
raise NotImplementedError, "#{self.class}#format_flag is not implemented"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
43
74
|
|
|
44
|
-
|
|
75
|
+
# A BEEF entry containing a raw transaction without a merkle proof.
|
|
76
|
+
class RawTxEntry < BeefTx
|
|
77
|
+
# @return [Transaction] the transaction
|
|
45
78
|
attr_reader :transaction
|
|
46
79
|
|
|
47
|
-
# @
|
|
48
|
-
|
|
80
|
+
# @param transaction [Transaction] the transaction
|
|
81
|
+
# @raise [ArgumentError] if transaction is nil
|
|
82
|
+
def initialize(transaction:)
|
|
83
|
+
raise ArgumentError, 'RawTxEntry requires a transaction' if transaction.nil?
|
|
84
|
+
|
|
85
|
+
super()
|
|
86
|
+
@transaction = transaction
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [String] wire-order wtxid delegated to the transaction
|
|
90
|
+
def wtxid
|
|
91
|
+
@transaction.wtxid
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Integer] FORMAT_RAW_TX wire-protocol flag
|
|
95
|
+
def format_flag
|
|
96
|
+
FORMAT_RAW_TX
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A BEEF entry containing a raw transaction with an associated BUMP index.
|
|
101
|
+
class ProvenTxEntry < BeefTx
|
|
102
|
+
# @return [Transaction] the transaction
|
|
103
|
+
attr_reader :transaction
|
|
49
104
|
|
|
50
|
-
# @return [Integer
|
|
105
|
+
# @return [Integer] index into the BEEF bumps array
|
|
51
106
|
attr_reader :bump_index
|
|
52
107
|
|
|
53
|
-
# @param
|
|
54
|
-
# @param
|
|
55
|
-
# @
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
raise ArgumentError, 'FORMAT_RAW_TX_AND_BUMP requires a bump_index' if format == FORMAT_RAW_TX_AND_BUMP && bump_index.nil?
|
|
108
|
+
# @param transaction [Transaction] the transaction
|
|
109
|
+
# @param bump_index [Integer] index into the bumps array
|
|
110
|
+
# @raise [ArgumentError] if transaction or bump_index is nil
|
|
111
|
+
def initialize(transaction:, bump_index:)
|
|
112
|
+
raise ArgumentError, 'ProvenTxEntry requires a transaction' if transaction.nil?
|
|
113
|
+
raise ArgumentError, 'ProvenTxEntry requires a bump_index' if bump_index.nil?
|
|
60
114
|
|
|
61
|
-
|
|
115
|
+
super()
|
|
62
116
|
@transaction = transaction
|
|
63
|
-
@known_txid = known_txid
|
|
64
117
|
@bump_index = bump_index
|
|
65
118
|
end
|
|
66
119
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
120
|
+
# @return [String] wire-order wtxid delegated to the transaction
|
|
121
|
+
def wtxid
|
|
122
|
+
@transaction.wtxid
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [Integer] FORMAT_RAW_TX_AND_BUMP wire-protocol flag
|
|
126
|
+
def format_flag
|
|
127
|
+
FORMAT_RAW_TX_AND_BUMP
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A BEEF entry containing only a transaction ID (no raw data).
|
|
132
|
+
class TxidOnlyEntry < BeefTx
|
|
133
|
+
# @return [String] 32-byte wire-order wtxid
|
|
134
|
+
attr_reader :known_wtxid
|
|
135
|
+
|
|
136
|
+
# @param known_wtxid [String] 32-byte wire-order wtxid
|
|
137
|
+
# @raise [ArgumentError] if known_wtxid is invalid
|
|
138
|
+
def initialize(known_wtxid:)
|
|
139
|
+
BSV::Primitives::Hex.validate_wtxid!(known_wtxid, name: 'known_wtxid')
|
|
140
|
+
super()
|
|
141
|
+
@known_wtxid = known_wtxid
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @return [String] the stored wire-order wtxid
|
|
145
|
+
def wtxid
|
|
146
|
+
@known_wtxid
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @return [Integer] FORMAT_TXID_ONLY wire-protocol flag
|
|
150
|
+
def format_flag
|
|
151
|
+
FORMAT_TXID_ONLY
|
|
77
152
|
end
|
|
78
153
|
end
|
|
79
154
|
|
|
@@ -86,8 +161,15 @@ module BSV
|
|
|
86
161
|
# @return [Array<BeefTx>] the transactions in dependency order
|
|
87
162
|
attr_reader :transactions
|
|
88
163
|
|
|
89
|
-
# @return [String, nil] 32-byte subject txid (Atomic BEEF only)
|
|
90
|
-
attr_reader :
|
|
164
|
+
# @return [String, nil] 32-byte wire-order subject txid (Atomic BEEF only)
|
|
165
|
+
attr_reader :subject_wtxid
|
|
166
|
+
|
|
167
|
+
# Display-order subject txid as a hex string (Atomic BEEF only).
|
|
168
|
+
#
|
|
169
|
+
# @return [String, nil] hex-encoded display-order txid, or nil
|
|
170
|
+
def subject_dtxid
|
|
171
|
+
@subject_wtxid&.reverse&.unpack1('H*')
|
|
172
|
+
end
|
|
91
173
|
|
|
92
174
|
# @param version [Integer] BEEF version constant (default: BEEF_V1, matching to_binary's
|
|
93
175
|
# default for ARC compatibility; from_binary overwrites this with the parsed version)
|
|
@@ -97,7 +179,7 @@ module BSV
|
|
|
97
179
|
@version = version
|
|
98
180
|
@bumps = bumps
|
|
99
181
|
@transactions = transactions
|
|
100
|
-
@
|
|
182
|
+
@subject_wtxid = nil
|
|
101
183
|
end
|
|
102
184
|
|
|
103
185
|
# --- Deserialisation ---
|
|
@@ -133,9 +215,9 @@ module BSV
|
|
|
133
215
|
raise ArgumentError, "truncated Atomic BEEF: need 36 bytes at offset #{offset}, got #{remaining}"
|
|
134
216
|
end
|
|
135
217
|
|
|
136
|
-
# Atomic BEEF stores the subject txid in internal
|
|
137
|
-
#
|
|
138
|
-
beef.instance_variable_set(:@
|
|
218
|
+
# Atomic BEEF stores the subject txid in wire (internal / little-endian) byte order,
|
|
219
|
+
# matching JS and Go SDKs. Store as-is in @subject_wtxid (wire-order).
|
|
220
|
+
beef.instance_variable_set(:@subject_wtxid, data.byteslice(offset, 32))
|
|
139
221
|
offset += 32
|
|
140
222
|
inner_version = data.byteslice(offset, 4).unpack1('V')
|
|
141
223
|
offset += 4
|
|
@@ -185,7 +267,7 @@ module BSV
|
|
|
185
267
|
# any FORMAT_TXID_ONLY entries (V1 / BRC-62 has no TXID-only format;
|
|
186
268
|
# pass +version: BEEF_V2+ to serialise such bundles)
|
|
187
269
|
def to_binary(version: BEEF_V1)
|
|
188
|
-
if version == BEEF_V1 && @transactions.any?
|
|
270
|
+
if version == BEEF_V1 && @transactions.any?(TxidOnlyEntry)
|
|
189
271
|
raise ArgumentError,
|
|
190
272
|
'BEEF V1 (BRC-62) does not support FORMAT_TXID_ONLY entries; pass version: BEEF_V2 to serialise this bundle'
|
|
191
273
|
end
|
|
@@ -223,13 +305,13 @@ module BSV
|
|
|
223
305
|
|
|
224
306
|
# Serialise as Atomic BEEF (BRC-95), wrapping V2 data with a subject txid.
|
|
225
307
|
#
|
|
226
|
-
# @param
|
|
308
|
+
# @param subject_wtxid [String] 32-byte wire-order subject transaction ID
|
|
227
309
|
# @return [String] raw Atomic BEEF binary
|
|
228
|
-
def to_atomic_binary(
|
|
310
|
+
def to_atomic_binary(subject_wtxid)
|
|
311
|
+
BSV::Primitives::Hex.validate_wtxid!(subject_wtxid, name: 'subject_wtxid')
|
|
229
312
|
buf = [ATOMIC_BEEF].pack('V')
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
buf << subject_txid.b.reverse
|
|
313
|
+
# subject_wtxid is already in wire (internal) byte order — write as-is.
|
|
314
|
+
buf << subject_wtxid.b
|
|
233
315
|
# BRC-95: inner envelope is always V2
|
|
234
316
|
buf << to_binary(version: BEEF_V2)
|
|
235
317
|
buf
|
|
@@ -237,42 +319,46 @@ module BSV
|
|
|
237
319
|
|
|
238
320
|
# --- Lookup ---
|
|
239
321
|
|
|
240
|
-
# Find a transaction in the bundle by its transaction ID.
|
|
322
|
+
# Find a transaction in the bundle by its wire-order transaction ID.
|
|
241
323
|
#
|
|
242
|
-
# @param
|
|
324
|
+
# @param wtxid [String] 32-byte wire-order wtxid
|
|
243
325
|
# @return [Transaction, nil] the matching transaction, or nil
|
|
244
|
-
def find_transaction(
|
|
326
|
+
def find_transaction(wtxid)
|
|
327
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
328
|
+
BSV.logger&.debug { "[Beef] find_transaction: #{wtxid.reverse.unpack1('H*')} in #{@transactions.length} entries" }
|
|
245
329
|
@transactions.each do |beef_tx|
|
|
246
|
-
|
|
330
|
+
next if beef_tx.is_a?(TxidOnlyEntry)
|
|
331
|
+
return beef_tx.transaction if beef_tx.wtxid == wtxid
|
|
247
332
|
end
|
|
248
333
|
nil
|
|
249
334
|
end
|
|
250
335
|
|
|
251
|
-
# Find the merkle path (BUMP) for a transaction by its txid.
|
|
336
|
+
# Find the merkle path (BUMP) for a transaction by its wire-order txid.
|
|
252
337
|
#
|
|
253
338
|
# First checks the transaction-table entries, then scans @bumps directly
|
|
254
|
-
# for a BUMP whose level-0 leaves contain the
|
|
339
|
+
# for a BUMP whose level-0 leaves contain the wtxid.
|
|
255
340
|
#
|
|
256
|
-
# @param
|
|
341
|
+
# @param wtxid [String] 32-byte wire-order wtxid
|
|
257
342
|
# @return [MerklePath, nil] the merkle path, or nil if not found
|
|
258
|
-
def find_bump(
|
|
343
|
+
def find_bump(wtxid)
|
|
344
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
259
345
|
# Check transaction-table entries first (fast path)
|
|
260
|
-
bt = @transactions.find { |entry| entry.
|
|
261
|
-
return bt.transaction
|
|
346
|
+
bt = @transactions.find { |entry| entry.wtxid == wtxid && entry.is_a?(ProvenTxEntry) }
|
|
347
|
+
return bt.transaction.merkle_path || @bumps[bt.bump_index] if bt
|
|
262
348
|
|
|
263
|
-
# F5.8: also scan @bumps directly for a path containing the
|
|
264
|
-
txid_internal = txid.reverse
|
|
349
|
+
# F5.8: also scan @bumps directly for a path containing the wtxid leaf
|
|
265
350
|
@bumps.find do |bump|
|
|
266
|
-
bump.path[0]&.any? { |leaf| leaf.hash ==
|
|
351
|
+
bump.path[0]&.any? { |leaf| leaf.hash == wtxid }
|
|
267
352
|
end
|
|
268
353
|
end
|
|
269
354
|
|
|
270
355
|
# Find a transaction with all source_transactions wired for signing.
|
|
271
356
|
#
|
|
272
|
-
# @param
|
|
357
|
+
# @param wtxid [String] 32-byte wire-order wtxid
|
|
273
358
|
# @return [Transaction, nil] the transaction with wired inputs, or nil
|
|
274
|
-
def find_transaction_for_signing(
|
|
275
|
-
|
|
359
|
+
def find_transaction_for_signing(wtxid)
|
|
360
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
361
|
+
tx = find_transaction(wtxid)
|
|
276
362
|
return unless tx
|
|
277
363
|
|
|
278
364
|
wire_inputs(tx)
|
|
@@ -282,10 +368,11 @@ module BSV
|
|
|
282
368
|
# Find a transaction and recursively wire its ancestry (source transactions
|
|
283
369
|
# and merkle paths) for atomic proof validation.
|
|
284
370
|
#
|
|
285
|
-
# @param
|
|
371
|
+
# @param wtxid [String] 32-byte wire-order wtxid
|
|
286
372
|
# @return [Transaction, nil] the transaction with full proof tree, or nil
|
|
287
|
-
def find_atomic_transaction(
|
|
288
|
-
|
|
373
|
+
def find_atomic_transaction(wtxid)
|
|
374
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
375
|
+
tx = find_transaction(wtxid)
|
|
289
376
|
return unless tx
|
|
290
377
|
|
|
291
378
|
wire_ancestry(tx)
|
|
@@ -294,10 +381,10 @@ module BSV
|
|
|
294
381
|
|
|
295
382
|
# Serialise as Atomic BEEF (BRC-95) hex string.
|
|
296
383
|
#
|
|
297
|
-
# @param
|
|
384
|
+
# @param subject_wtxid [String] 32-byte wire-order subject transaction ID
|
|
298
385
|
# @return [String] hex-encoded Atomic BEEF
|
|
299
|
-
def to_atomic_hex(
|
|
300
|
-
to_atomic_binary(
|
|
386
|
+
def to_atomic_hex(subject_wtxid)
|
|
387
|
+
to_atomic_binary(subject_wtxid).unpack1('H*')
|
|
301
388
|
end
|
|
302
389
|
|
|
303
390
|
# --- Merge operations ---
|
|
@@ -338,15 +425,11 @@ module BSV
|
|
|
338
425
|
level0_leaves = bump.path[0] || []
|
|
339
426
|
level0_internal = level0_leaves.map(&:hash).compact.to_set
|
|
340
427
|
@transactions.each_with_index do |bt, i|
|
|
341
|
-
next unless bt.
|
|
342
|
-
next unless level0_internal.include?(bt.
|
|
428
|
+
next unless bt.is_a?(RawTxEntry)
|
|
429
|
+
next unless level0_internal.include?(bt.wtxid)
|
|
343
430
|
|
|
344
431
|
bt.transaction.merkle_path ||= bump
|
|
345
|
-
@transactions[i] =
|
|
346
|
-
format: FORMAT_RAW_TX_AND_BUMP,
|
|
347
|
-
transaction: bt.transaction,
|
|
348
|
-
bump_index: idx
|
|
349
|
-
)
|
|
432
|
+
@transactions[i] = ProvenTxEntry.new(transaction: bt.transaction, bump_index: idx)
|
|
350
433
|
end
|
|
351
434
|
|
|
352
435
|
idx
|
|
@@ -362,10 +445,10 @@ module BSV
|
|
|
362
445
|
# @param tx [Transaction] the transaction to merge
|
|
363
446
|
# @return [BeefTx] the (possibly existing or upgraded) BeefTx entry
|
|
364
447
|
def merge_transaction(tx)
|
|
365
|
-
|
|
448
|
+
wtxid = tx.wtxid
|
|
366
449
|
|
|
367
450
|
# Check for existing entry and upgrade if a stronger format is available
|
|
368
|
-
existing_idx = @transactions.index { |bt| bt.
|
|
451
|
+
existing_idx = @transactions.index { |bt| bt.wtxid == wtxid }
|
|
369
452
|
if existing_idx
|
|
370
453
|
existing = @transactions[existing_idx]
|
|
371
454
|
upgraded = upgrade_beef_tx(existing, tx)
|
|
@@ -381,9 +464,9 @@ module BSV
|
|
|
381
464
|
# Merge this transaction's BUMP if it has one
|
|
382
465
|
entry = if tx.merkle_path
|
|
383
466
|
bump_idx = merge_bump(tx.merkle_path)
|
|
384
|
-
|
|
467
|
+
ProvenTxEntry.new(transaction: tx, bump_index: bump_idx)
|
|
385
468
|
else
|
|
386
|
-
|
|
469
|
+
RawTxEntry.new(transaction: tx)
|
|
387
470
|
end
|
|
388
471
|
@transactions << entry
|
|
389
472
|
entry
|
|
@@ -409,7 +492,7 @@ module BSV
|
|
|
409
492
|
tx.merkle_path = @bumps[bump_index]
|
|
410
493
|
end
|
|
411
494
|
|
|
412
|
-
existing_idx = @transactions.index { |bt| bt.
|
|
495
|
+
existing_idx = @transactions.index { |bt| bt.wtxid == tx.wtxid }
|
|
413
496
|
if existing_idx
|
|
414
497
|
existing = @transactions[existing_idx]
|
|
415
498
|
upgraded = upgrade_beef_tx(existing, tx, bump_index: bump_index)
|
|
@@ -418,9 +501,9 @@ module BSV
|
|
|
418
501
|
end
|
|
419
502
|
|
|
420
503
|
entry = if bump_index
|
|
421
|
-
|
|
504
|
+
ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
|
|
422
505
|
else
|
|
423
|
-
|
|
506
|
+
RawTxEntry.new(transaction: tx)
|
|
424
507
|
end
|
|
425
508
|
@transactions << entry
|
|
426
509
|
entry
|
|
@@ -446,15 +529,15 @@ module BSV
|
|
|
446
529
|
# Merge transactions with remapped BUMP indices, constructing new
|
|
447
530
|
# BeefTx instances rather than sharing source references (F5.9).
|
|
448
531
|
other.transactions.each do |beef_tx|
|
|
449
|
-
case beef_tx
|
|
450
|
-
when
|
|
451
|
-
next if @transactions.any? { |bt| bt.
|
|
532
|
+
case beef_tx
|
|
533
|
+
when TxidOnlyEntry
|
|
534
|
+
next if @transactions.any? { |bt| bt.wtxid == beef_tx.known_wtxid }
|
|
452
535
|
|
|
453
|
-
@transactions <<
|
|
536
|
+
@transactions << TxidOnlyEntry.new(known_wtxid: beef_tx.known_wtxid)
|
|
454
537
|
else
|
|
455
|
-
next if @transactions.any? { |bt| bt.
|
|
538
|
+
next if @transactions.any? { |bt| bt.wtxid == beef_tx.wtxid }
|
|
456
539
|
|
|
457
|
-
if beef_tx.
|
|
540
|
+
if beef_tx.is_a?(ProvenTxEntry) && beef_tx.bump_index
|
|
458
541
|
new_idx = bump_remap[beef_tx.bump_index]
|
|
459
542
|
if new_idx.nil?
|
|
460
543
|
raise ArgumentError,
|
|
@@ -466,13 +549,9 @@ module BSV
|
|
|
466
549
|
# so mutations to the merged bundle don't affect the source.
|
|
467
550
|
tx = beef_tx.transaction.dup
|
|
468
551
|
tx.merkle_path = @bumps[new_idx]
|
|
469
|
-
@transactions <<
|
|
470
|
-
format: FORMAT_RAW_TX_AND_BUMP,
|
|
471
|
-
transaction: tx,
|
|
472
|
-
bump_index: new_idx
|
|
473
|
-
)
|
|
552
|
+
@transactions << ProvenTxEntry.new(transaction: tx, bump_index: new_idx)
|
|
474
553
|
else
|
|
475
|
-
@transactions <<
|
|
554
|
+
@transactions << RawTxEntry.new(transaction: beef_tx.transaction.dup)
|
|
476
555
|
end
|
|
477
556
|
end
|
|
478
557
|
end
|
|
@@ -482,13 +561,14 @@ module BSV
|
|
|
482
561
|
|
|
483
562
|
# Convert a transaction entry to TXID-only format.
|
|
484
563
|
#
|
|
485
|
-
# @param
|
|
564
|
+
# @param wtxid [String] 32-byte wire-order wtxid
|
|
486
565
|
# @return [BeefTx, nil] the converted entry, or nil if not found
|
|
487
|
-
def make_txid_only(
|
|
488
|
-
|
|
566
|
+
def make_txid_only(wtxid)
|
|
567
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
568
|
+
idx = @transactions.index { |bt| bt.wtxid == wtxid }
|
|
489
569
|
return unless idx
|
|
490
570
|
|
|
491
|
-
@transactions[idx] =
|
|
571
|
+
@transactions[idx] = TxidOnlyEntry.new(known_wtxid: wtxid)
|
|
492
572
|
end
|
|
493
573
|
|
|
494
574
|
# --- Validation ---
|
|
@@ -507,28 +587,28 @@ module BSV
|
|
|
507
587
|
# @return [Boolean] true if structurally valid
|
|
508
588
|
def valid?(allow_txid_only: false)
|
|
509
589
|
# TXID-only entries are invalid unless explicitly allowed
|
|
510
|
-
has_txid_only = @transactions.any?
|
|
590
|
+
has_txid_only = @transactions.any?(TxidOnlyEntry)
|
|
511
591
|
return false if has_txid_only && !allow_txid_only
|
|
512
592
|
|
|
513
593
|
# F5.4: verify BUMP linkage and computed root for each proven transaction
|
|
514
594
|
@transactions.each do |bt|
|
|
515
|
-
next unless bt.
|
|
595
|
+
next unless bt.is_a?(ProvenTxEntry)
|
|
516
596
|
|
|
517
597
|
# Must have a BUMP
|
|
518
|
-
bump = bt.transaction
|
|
598
|
+
bump = bt.transaction.merkle_path || @bumps[bt.bump_index]
|
|
519
599
|
return false unless bump
|
|
520
600
|
|
|
521
601
|
# The txid must appear as a leaf in the BUMP and compute a valid root
|
|
522
602
|
begin
|
|
523
|
-
bump.compute_root(bt.
|
|
603
|
+
bump.compute_root(bt.wtxid)
|
|
524
604
|
rescue ArgumentError
|
|
525
605
|
return false
|
|
526
606
|
end
|
|
527
607
|
end
|
|
528
608
|
|
|
529
|
-
|
|
609
|
+
known_wtxids = build_known_wtxids(allow_txid_only)
|
|
530
610
|
|
|
531
|
-
pending = @transactions.
|
|
611
|
+
pending = @transactions.reject { |bt| bt.is_a?(TxidOnlyEntry) || known_wtxids.include?(bt.wtxid) }
|
|
532
612
|
|
|
533
613
|
# Iteratively resolve: if all inputs of a tx are known, it becomes known
|
|
534
614
|
changed = true
|
|
@@ -536,10 +616,10 @@ module BSV
|
|
|
536
616
|
changed = false
|
|
537
617
|
pending.reject! do |bt|
|
|
538
618
|
all_inputs_known = bt.transaction.inputs.all? do |input|
|
|
539
|
-
|
|
619
|
+
known_wtxids.include?(input.prev_wtxid)
|
|
540
620
|
end
|
|
541
621
|
if all_inputs_known
|
|
542
|
-
|
|
622
|
+
known_wtxids.add(bt.wtxid)
|
|
543
623
|
changed = true
|
|
544
624
|
end
|
|
545
625
|
all_inputs_known
|
|
@@ -584,18 +664,18 @@ module BSV
|
|
|
584
664
|
def sort_transactions!
|
|
585
665
|
return self if @transactions.length <= 1
|
|
586
666
|
|
|
587
|
-
|
|
588
|
-
@transactions.each_with_index { |bt, i|
|
|
667
|
+
wtxid_index = {}
|
|
668
|
+
@transactions.each_with_index { |bt, i| wtxid_index[bt.wtxid] = i }
|
|
589
669
|
|
|
590
670
|
# Build adjacency: for each tx, which other txs must come before it?
|
|
591
671
|
in_degree = Array.new(@transactions.length, 0)
|
|
592
672
|
dependents = Array.new(@transactions.length) { [] }
|
|
593
673
|
|
|
594
674
|
@transactions.each_with_index do |bt, i|
|
|
595
|
-
next
|
|
675
|
+
next if bt.is_a?(TxidOnlyEntry)
|
|
596
676
|
|
|
597
677
|
bt.transaction.inputs.each do |input|
|
|
598
|
-
dep_idx =
|
|
678
|
+
dep_idx = wtxid_index[input.prev_wtxid]
|
|
599
679
|
next unless dep_idx
|
|
600
680
|
|
|
601
681
|
dependents[dep_idx] << i
|
|
@@ -618,8 +698,8 @@ module BSV
|
|
|
618
698
|
|
|
619
699
|
# F5.5: preserve unsortable (cyclic) transactions rather than silently dropping them
|
|
620
700
|
if sorted.length < @transactions.length
|
|
621
|
-
sorted_set = sorted.to_set(&:
|
|
622
|
-
@txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.
|
|
701
|
+
sorted_set = sorted.to_set(&:wtxid)
|
|
702
|
+
@txs_not_valid = @transactions.reject { |bt| sorted_set.include?(bt.wtxid) }
|
|
623
703
|
end
|
|
624
704
|
|
|
625
705
|
@transactions = sorted
|
|
@@ -661,27 +741,24 @@ module BSV
|
|
|
661
741
|
|
|
662
742
|
case format
|
|
663
743
|
when FORMAT_TXID_ONLY
|
|
664
|
-
# Wire stores txid in internal (little-endian) byte order;
|
|
665
|
-
#
|
|
666
|
-
# Transaction#txid across all format types.
|
|
744
|
+
# Wire stores txid in internal (little-endian / wire) byte order;
|
|
745
|
+
# store as-is in known_wtxid so it matches Transaction#wtxid.
|
|
667
746
|
raise ArgumentError, 'truncated BEEF: not enough bytes for TXID_ONLY entry' if offset + 32 > data.bytesize
|
|
668
747
|
|
|
669
|
-
|
|
748
|
+
known_wtxid = data.byteslice(offset, 32)
|
|
670
749
|
offset += 32
|
|
671
|
-
beef.transactions <<
|
|
750
|
+
beef.transactions << TxidOnlyEntry.new(known_wtxid: known_wtxid)
|
|
672
751
|
when FORMAT_RAW_TX_AND_BUMP
|
|
673
752
|
bump_index, vi_size = VarInt.decode(data, offset)
|
|
674
753
|
offset += vi_size
|
|
675
754
|
tx, consumed = Transaction.from_binary_with_offset(data, offset)
|
|
676
755
|
offset += consumed
|
|
677
756
|
tx.merkle_path = beef.bumps[bump_index] if bump_index < beef.bumps.length
|
|
678
|
-
beef.transactions <<
|
|
679
|
-
format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_index
|
|
680
|
-
)
|
|
757
|
+
beef.transactions << ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
|
|
681
758
|
when FORMAT_RAW_TX
|
|
682
759
|
tx, consumed = Transaction.from_binary_with_offset(data, offset)
|
|
683
760
|
offset += consumed
|
|
684
|
-
beef.transactions <<
|
|
761
|
+
beef.transactions << RawTxEntry.new(transaction: tx)
|
|
685
762
|
end
|
|
686
763
|
end
|
|
687
764
|
|
|
@@ -700,14 +777,12 @@ module BSV
|
|
|
700
777
|
offset += 1
|
|
701
778
|
|
|
702
779
|
if has_bump.zero?
|
|
703
|
-
beef.transactions <<
|
|
780
|
+
beef.transactions << RawTxEntry.new(transaction: tx)
|
|
704
781
|
else
|
|
705
782
|
bump_index, vi_size = VarInt.decode(data, offset)
|
|
706
783
|
offset += vi_size
|
|
707
784
|
tx.merkle_path = beef.bumps[bump_index] if bump_index < beef.bumps.length
|
|
708
|
-
beef.transactions <<
|
|
709
|
-
format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_index
|
|
710
|
-
)
|
|
785
|
+
beef.transactions << ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
|
|
711
786
|
end
|
|
712
787
|
end
|
|
713
788
|
|
|
@@ -717,16 +792,22 @@ module BSV
|
|
|
717
792
|
def wire_source_transactions(beef)
|
|
718
793
|
tx_map = {}
|
|
719
794
|
beef.transactions.each do |beef_tx|
|
|
720
|
-
next
|
|
795
|
+
next if beef_tx.is_a?(TxidOnlyEntry)
|
|
721
796
|
|
|
722
|
-
# Wire inputs to ancestors already in the map (BEEF is dependency-ordered)
|
|
797
|
+
# Wire inputs to ancestors already in the map (BEEF is dependency-ordered).
|
|
798
|
+
# Both prev_wtxid and wtxid are wire-order — no conversion needed.
|
|
723
799
|
beef_tx.transaction.inputs.each do |input|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
800
|
+
source = tx_map[input.prev_wtxid]
|
|
801
|
+
next unless source
|
|
802
|
+
|
|
803
|
+
input.source_transaction = source
|
|
804
|
+
BSV.logger&.debug do
|
|
805
|
+
"[Beef] wired input #{input.prev_wtxid.reverse.unpack1('H*')}:#{input.prev_tx_out_index} " \
|
|
806
|
+
"-> source #{source.wtxid.reverse.unpack1('H*')}"
|
|
807
|
+
end
|
|
727
808
|
end
|
|
728
809
|
|
|
729
|
-
tx_map[beef_tx.
|
|
810
|
+
tx_map[beef_tx.wtxid] = beef_tx.transaction
|
|
730
811
|
end
|
|
731
812
|
end
|
|
732
813
|
end
|
|
@@ -751,33 +832,28 @@ module BSV
|
|
|
751
832
|
effective_bump_idx = bump_index
|
|
752
833
|
effective_bump_idx = merge_bump(tx.merkle_path) if effective_bump_idx.nil? && tx&.merkle_path
|
|
753
834
|
|
|
754
|
-
case existing
|
|
755
|
-
when
|
|
835
|
+
case existing
|
|
836
|
+
when TxidOnlyEntry
|
|
756
837
|
if effective_bump_idx
|
|
757
|
-
|
|
838
|
+
ProvenTxEntry.new(transaction: tx, bump_index: effective_bump_idx)
|
|
758
839
|
elsif tx
|
|
759
|
-
|
|
840
|
+
RawTxEntry.new(transaction: tx)
|
|
760
841
|
end
|
|
761
|
-
when
|
|
842
|
+
when RawTxEntry
|
|
762
843
|
if effective_bump_idx
|
|
763
844
|
tx_to_use = tx || existing.transaction
|
|
764
845
|
tx_to_use.merkle_path ||= @bumps[effective_bump_idx]
|
|
765
|
-
|
|
846
|
+
ProvenTxEntry.new(transaction: tx_to_use, bump_index: effective_bump_idx)
|
|
766
847
|
end
|
|
767
848
|
end
|
|
768
|
-
#
|
|
849
|
+
# ProvenTxEntry is already the strongest — no upgrade needed
|
|
769
850
|
end
|
|
770
851
|
|
|
771
|
-
# Build a set of
|
|
772
|
-
def
|
|
852
|
+
# Build a set of wire-order wtxids that are "known" (proven or txid-only).
|
|
853
|
+
def build_known_wtxids(allow_txid_only)
|
|
773
854
|
known = Set.new
|
|
774
855
|
@transactions.each do |bt|
|
|
775
|
-
|
|
776
|
-
when FORMAT_RAW_TX_AND_BUMP
|
|
777
|
-
known.add(bt.txid)
|
|
778
|
-
when FORMAT_TXID_ONLY
|
|
779
|
-
known.add(bt.txid) if allow_txid_only
|
|
780
|
-
end
|
|
856
|
+
known.add(bt.wtxid) if bt.is_a?(ProvenTxEntry) || (bt.is_a?(TxidOnlyEntry) && allow_txid_only)
|
|
781
857
|
end
|
|
782
858
|
known
|
|
783
859
|
end
|
|
@@ -787,7 +863,7 @@ module BSV
|
|
|
787
863
|
tx.inputs.each do |input|
|
|
788
864
|
next if input.source_transaction
|
|
789
865
|
|
|
790
|
-
source = find_transaction(input.
|
|
866
|
+
source = find_transaction(input.prev_wtxid)
|
|
791
867
|
input.source_transaction = source if source
|
|
792
868
|
end
|
|
793
869
|
end
|
|
@@ -799,7 +875,7 @@ module BSV
|
|
|
799
875
|
next unless input.source_transaction
|
|
800
876
|
|
|
801
877
|
source = input.source_transaction
|
|
802
|
-
source.merkle_path ||= find_bump(source.
|
|
878
|
+
source.merkle_path ||= find_bump(source.wtxid)
|
|
803
879
|
wire_ancestry(source)
|
|
804
880
|
end
|
|
805
881
|
end
|
|
@@ -807,7 +883,7 @@ module BSV
|
|
|
807
883
|
# V1 (BRC-62): raw_tx + has_bump(byte) [+ bump_index(varint)]
|
|
808
884
|
def write_v1_tx(buf, beef_tx)
|
|
809
885
|
buf << beef_tx.transaction.to_binary
|
|
810
|
-
if beef_tx.
|
|
886
|
+
if beef_tx.is_a?(ProvenTxEntry)
|
|
811
887
|
buf << [1].pack('C')
|
|
812
888
|
buf << VarInt.encode(beef_tx.bump_index)
|
|
813
889
|
else
|
|
@@ -817,12 +893,12 @@ module BSV
|
|
|
817
893
|
|
|
818
894
|
# V2 (BRC-96): format_byte [+ bump_index(varint)] + raw_tx
|
|
819
895
|
def write_v2_tx(buf, beef_tx)
|
|
820
|
-
case beef_tx
|
|
821
|
-
when
|
|
896
|
+
case beef_tx
|
|
897
|
+
when TxidOnlyEntry
|
|
822
898
|
buf << [FORMAT_TXID_ONLY].pack('C')
|
|
823
|
-
#
|
|
824
|
-
buf << beef_tx.
|
|
825
|
-
when
|
|
899
|
+
# known_wtxid is already wire (internal) byte order.
|
|
900
|
+
buf << beef_tx.known_wtxid
|
|
901
|
+
when ProvenTxEntry
|
|
826
902
|
buf << [FORMAT_RAW_TX_AND_BUMP].pack('C')
|
|
827
903
|
buf << VarInt.encode(beef_tx.bump_index)
|
|
828
904
|
buf << beef_tx.transaction.to_binary
|