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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/lib/bsv/auth/auth_middleware.rb +6 -6
- data/lib/bsv/auth/certificate.rb +22 -18
- data/lib/bsv/auth/master_certificate.rb +5 -5
- data/lib/bsv/auth/nonce.rb +13 -13
- data/lib/bsv/auth/peer.rb +53 -53
- data/lib/bsv/auth/verifiable_certificate.rb +1 -1
- data/lib/bsv/identity/client.rb +27 -32
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +18 -12
- data/lib/bsv/mcp/tools/check_balance.rb +16 -4
- data/lib/bsv/mcp/tools/fetch_tx.rb +11 -4
- data/lib/bsv/mcp/tools/fetch_utxos.rb +16 -4
- data/lib/bsv/mcp/tools/helpers.rb +2 -2
- data/lib/bsv/network/arc.rb +13 -153
- 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/network/whats_on_chain.rb +13 -107
- data/lib/bsv/overlay/admin_token_template.rb +4 -4
- 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 +26 -28
- 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/script/push_drop_template.rb +4 -4
- 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/errors.rb +47 -0
- data/lib/bsv/wallet/interface/brc100.rb +270 -0
- data/lib/bsv/wallet/interface.rb +9 -0
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +152 -0
- data/lib/bsv/wallet/proto_wallet/validators.rb +74 -0
- data/lib/bsv/wallet/proto_wallet.rb +327 -0
- data/lib/bsv/wallet.rb +16 -0
- data/lib/bsv-sdk.rb +18 -1
- metadata +22 -1
|
@@ -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,6 +329,11 @@ 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
|
|
@@ -370,11 +375,11 @@ module BSV
|
|
|
370
375
|
# or nil if the BEEF is empty or contains no raw transaction entries
|
|
371
376
|
def self.from_beef(data)
|
|
372
377
|
beef = Beef.from_binary(data)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return nil unless
|
|
378
|
+
subject_wtxid = beef.subject_wtxid ||
|
|
379
|
+
beef.transactions.reverse.find(&:transaction)&.transaction&.wtxid
|
|
380
|
+
return nil unless subject_wtxid
|
|
376
381
|
|
|
377
|
-
beef.find_atomic_transaction(
|
|
382
|
+
beef.find_atomic_transaction(subject_wtxid)
|
|
378
383
|
end
|
|
379
384
|
|
|
380
385
|
# Parse a BEEF hex string and return the subject transaction.
|
|
@@ -388,15 +393,26 @@ module BSV
|
|
|
388
393
|
|
|
389
394
|
# --- Transaction ID ---
|
|
390
395
|
|
|
391
|
-
#
|
|
396
|
+
# Wire-order transaction ID (raw SHA-256d of the serialised tx).
|
|
397
|
+
#
|
|
398
|
+
# Used by BEEF, BUMPs, and merkle paths, which all work in wire byte order
|
|
399
|
+
# to match {TransactionInput#prev_wtxid}.
|
|
400
|
+
#
|
|
401
|
+
# @return [String] 32-byte transaction ID in wire byte order
|
|
402
|
+
def wtxid
|
|
403
|
+
id = BSV::Primitives::Digest.sha256d(to_binary)
|
|
404
|
+
BSV.logger&.debug { "[Transaction] wtxid computed (dtxid=#{id.reverse.unpack1('H*')})" }
|
|
405
|
+
id
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Display-order transaction ID (reversed from the natural SHA-256d hash).
|
|
392
409
|
#
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
# order (natural hash). Use +.reverse+ to convert between the two.
|
|
410
|
+
# This is the conventional human-readable representation used in block
|
|
411
|
+
# explorers, wallets, and all user-facing contexts.
|
|
396
412
|
#
|
|
397
413
|
# @return [String] 32-byte transaction ID in display byte order
|
|
398
414
|
def txid
|
|
399
|
-
|
|
415
|
+
wtxid.reverse
|
|
400
416
|
end
|
|
401
417
|
|
|
402
418
|
# The transaction ID as a hex string (display byte order).
|
|
@@ -406,6 +422,19 @@ module BSV
|
|
|
406
422
|
txid.unpack1('H*')
|
|
407
423
|
end
|
|
408
424
|
|
|
425
|
+
# Display-order transaction ID as a hex string.
|
|
426
|
+
#
|
|
427
|
+
# Mirrors the wallet gem's +DisplayTxid+ pattern. +dtxid+ always returns
|
|
428
|
+
# a 64-char hex string suitable for JSON and UI boundaries.
|
|
429
|
+
#
|
|
430
|
+
# @return [String] hex-encoded transaction ID (display order)
|
|
431
|
+
alias dtxid txid_hex
|
|
432
|
+
|
|
433
|
+
# Display-order transaction ID as a hex string (alias for {#dtxid}).
|
|
434
|
+
#
|
|
435
|
+
# @return [String] hex-encoded transaction ID (display order)
|
|
436
|
+
alias dtxid_hex txid_hex
|
|
437
|
+
|
|
409
438
|
# --- Sighash (BIP-143 with FORKID) ---
|
|
410
439
|
|
|
411
440
|
# Build the BIP-143 sighash preimage for an input.
|
|
@@ -456,6 +485,19 @@ module BSV
|
|
|
456
485
|
# 10. sighash type (4 LE) — includes FORKID flag
|
|
457
486
|
buf << [sighash_type].pack('V')
|
|
458
487
|
|
|
488
|
+
BSV.logger&.debug do
|
|
489
|
+
hp = buf.byteslice(4, 32).unpack1('H*')
|
|
490
|
+
hs = buf.byteslice(36, 32).unpack1('H*')
|
|
491
|
+
op = input.outpoint_binary.unpack1('H*')
|
|
492
|
+
sc = script_bytes.unpack1('H*')
|
|
493
|
+
ho = buf.byteslice(-40, 32).unpack1('H*')
|
|
494
|
+
"[Sighash] input=#{input_index} type=0x#{format('%02x', sighash_type)} " \
|
|
495
|
+
"version=#{@version} hashPrevouts=#{hp} hashSequence=#{hs} " \
|
|
496
|
+
"outpoint=#{op} scriptCode=#{sc[0, 40]}#{'...' if sc.length > 40} " \
|
|
497
|
+
"value=#{input.source_satoshis} seq=#{input.sequence} " \
|
|
498
|
+
"hashOutputs=#{ho} locktime=#{@lock_time}"
|
|
499
|
+
end
|
|
500
|
+
|
|
459
501
|
buf
|
|
460
502
|
end
|
|
461
503
|
|
|
@@ -466,7 +508,9 @@ module BSV
|
|
|
466
508
|
# @param subscript [Script::Script, nil] override locking script for the input
|
|
467
509
|
# @return [String] 32-byte sighash digest
|
|
468
510
|
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))
|
|
511
|
+
digest = BSV::Primitives::Digest.sha256d(sighash_preimage(input_index, sighash_type, subscript: subscript))
|
|
512
|
+
BSV.logger&.debug { "[Sighash] digest=#{digest.unpack1('H*')}" }
|
|
513
|
+
digest
|
|
470
514
|
end
|
|
471
515
|
|
|
472
516
|
# --- Signing ---
|
|
@@ -580,17 +624,17 @@ module BSV
|
|
|
580
624
|
|
|
581
625
|
until queue.empty?
|
|
582
626
|
tx = queue.shift
|
|
583
|
-
|
|
584
|
-
next if verified[
|
|
627
|
+
wtxid = tx.wtxid
|
|
628
|
+
next if verified[wtxid]
|
|
585
629
|
|
|
586
630
|
# Merkle path short-circuit: proven transaction needs no input verification
|
|
587
631
|
if tx.merkle_path
|
|
588
|
-
unless tx.merkle_path.verify(
|
|
632
|
+
unless tx.merkle_path.verify(tx.txid_hex, chain_tracker)
|
|
589
633
|
raise VerificationError.new(:invalid_merkle_proof,
|
|
590
|
-
"invalid merkle proof for transaction #{
|
|
634
|
+
"invalid merkle proof for transaction #{tx.txid_hex}")
|
|
591
635
|
end
|
|
592
636
|
|
|
593
|
-
verified[
|
|
637
|
+
verified[wtxid] = true
|
|
594
638
|
next
|
|
595
639
|
end
|
|
596
640
|
|
|
@@ -622,13 +666,13 @@ module BSV
|
|
|
622
666
|
|
|
623
667
|
# Enqueue source transaction for verification if not yet verified
|
|
624
668
|
source_tx = input.source_transaction
|
|
625
|
-
queue << source_tx if source_tx && !verified[source_tx.
|
|
669
|
+
queue << source_tx if source_tx && !verified[source_tx.wtxid]
|
|
626
670
|
end
|
|
627
671
|
|
|
628
672
|
# Output ≤ input check
|
|
629
673
|
verify_output_constraint(tx)
|
|
630
674
|
|
|
631
|
-
verified[
|
|
675
|
+
verified[wtxid] = true
|
|
632
676
|
end
|
|
633
677
|
|
|
634
678
|
true
|
|
@@ -777,19 +821,19 @@ module BSV
|
|
|
777
821
|
end
|
|
778
822
|
|
|
779
823
|
def verify_input_requirements(tx, input, index)
|
|
780
|
-
|
|
824
|
+
dtxid_hex = tx.txid_hex
|
|
781
825
|
if input.unlocking_script.nil?
|
|
782
826
|
raise VerificationError.new(:missing_source,
|
|
783
|
-
"input #{index} of transaction #{
|
|
827
|
+
"input #{index} of transaction #{dtxid_hex} has no unlocking script")
|
|
784
828
|
end
|
|
785
829
|
if input.source_locking_script.nil?
|
|
786
830
|
raise VerificationError.new(:missing_source,
|
|
787
|
-
"input #{index} of transaction #{
|
|
831
|
+
"input #{index} of transaction #{dtxid_hex} has no source locking script")
|
|
788
832
|
end
|
|
789
833
|
return unless input.source_satoshis.nil?
|
|
790
834
|
|
|
791
835
|
raise VerificationError.new(:missing_source,
|
|
792
|
-
"input #{index} of transaction #{
|
|
836
|
+
"input #{index} of transaction #{dtxid_hex} has no source satoshis")
|
|
793
837
|
end
|
|
794
838
|
|
|
795
839
|
def verify_fee(fee_model)
|
|
@@ -843,7 +887,7 @@ module BSV
|
|
|
843
887
|
|
|
844
888
|
# Collect this transaction and all its ancestors in dependency order
|
|
845
889
|
# (ancestors first, self last). Stops recursion at transactions with
|
|
846
|
-
# a merkle_path (proven leaves). Deduplicates by
|
|
890
|
+
# a merkle_path (proven leaves). Deduplicates by wtxid (wire-order bytes).
|
|
847
891
|
def collect_ancestors
|
|
848
892
|
seen = {}
|
|
849
893
|
result = []
|
|
@@ -852,18 +896,25 @@ module BSV
|
|
|
852
896
|
end
|
|
853
897
|
|
|
854
898
|
def collect_ancestors_recursive(tx, seen, result)
|
|
855
|
-
|
|
856
|
-
return if seen.key?(
|
|
899
|
+
wtxid = tx.wtxid
|
|
900
|
+
return if seen.key?(wtxid)
|
|
857
901
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
902
|
+
if tx.merkle_path
|
|
903
|
+
BSV.logger&.debug do
|
|
904
|
+
"[Transaction] ancestor: #{tx.dtxid_hex} proven at height #{tx.merkle_path.block_height} (leaf stop)"
|
|
905
|
+
end
|
|
906
|
+
else
|
|
907
|
+
tx.inputs.each_with_index do |input, idx|
|
|
908
|
+
unless input.source_transaction
|
|
909
|
+
BSV.logger&.debug { "[Transaction] ancestor: #{tx.dtxid_hex} input #{idx} has no source_transaction (skipped)" }
|
|
910
|
+
next
|
|
911
|
+
end
|
|
861
912
|
|
|
862
913
|
collect_ancestors_recursive(input.source_transaction, seen, result)
|
|
863
914
|
end
|
|
864
915
|
end
|
|
865
916
|
|
|
866
|
-
seen[
|
|
917
|
+
seen[wtxid] = true
|
|
867
918
|
result << tx
|
|
868
919
|
end
|
|
869
920
|
|
|
@@ -886,8 +937,8 @@ module BSV
|
|
|
886
937
|
merged = txs.first.merkle_path.dup
|
|
887
938
|
txs.drop(1).each { |t| merged.combine(t.merkle_path) }
|
|
888
939
|
|
|
889
|
-
|
|
890
|
-
clean = merged.extract(
|
|
940
|
+
wtxid_hashes = txs.map(&:wtxid)
|
|
941
|
+
clean = merged.extract(wtxid_hashes)
|
|
891
942
|
|
|
892
943
|
bump_index_by_height[height] = beef.bumps.length
|
|
893
944
|
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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
# Base error for all wallet operations. Carries a machine-readable code
|
|
6
|
+
# per the BRC-100 error structure.
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
attr_reader :code
|
|
9
|
+
|
|
10
|
+
def initialize(message, code = 1)
|
|
11
|
+
@code = code
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Raised when a required parameter is missing or invalid.
|
|
17
|
+
class InvalidParameterError < Error
|
|
18
|
+
attr_reader :parameter
|
|
19
|
+
|
|
20
|
+
def initialize(parameter, must_be = 'valid')
|
|
21
|
+
@parameter = parameter
|
|
22
|
+
super("the #{parameter} parameter must be #{must_be}", 6)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Raised when an HMAC fails to verify.
|
|
27
|
+
class InvalidHmacError < Error
|
|
28
|
+
def initialize(message = 'the provided HMAC is invalid')
|
|
29
|
+
super(message, 3)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Raised when a signature fails to verify.
|
|
34
|
+
class InvalidSignatureError < Error
|
|
35
|
+
def initialize(message = 'the provided signature is invalid')
|
|
36
|
+
super(message, 4)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Raised when an operation is not supported by this wallet implementation.
|
|
41
|
+
class UnsupportedActionError < Error
|
|
42
|
+
def initialize(method_name = 'this method')
|
|
43
|
+
super("#{method_name} is not supported by this wallet implementation", 2)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|