bsv-sdk 0.23.1 → 0.25.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 +79 -0
- data/README.md +1 -1
- data/lib/bsv/auth/auth_payload.rb +5 -0
- data/lib/bsv/identity/client.rb +9 -5
- data/lib/bsv/kv_store/entry.rb +15 -0
- data/lib/bsv/kv_store/global.rb +210 -0
- data/lib/bsv/kv_store/interpreter.rb +109 -0
- data/lib/bsv/kv_store/token.rb +10 -0
- data/lib/bsv/kv_store.rb +10 -0
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -2
- data/lib/bsv/mcp/tools/decode_tx.rb +1 -1
- data/lib/bsv/mcp/tools/fetch_tx.rb +1 -1
- data/lib/bsv/mcp/tools/helpers.rb +3 -3
- data/lib/bsv/network/protocol.rb +12 -1
- data/lib/bsv/network/util.rb +13 -5
- data/lib/bsv/overlay/admin_token_template.rb +2 -2
- data/lib/bsv/overlay/historian.rb +118 -0
- data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
- data/lib/bsv/overlay/types.rb +37 -0
- data/lib/bsv/overlay.rb +1 -0
- data/lib/bsv/primitives/ecies.rb +12 -3
- data/lib/bsv/registry/client.rb +54 -7
- data/lib/bsv/script/bip276.rb +143 -0
- data/lib/bsv/script/interpreter/interpreter.rb +1 -1
- data/lib/bsv/script/push_drop_template.rb +2 -2
- data/lib/bsv/script.rb +1 -0
- data/lib/bsv/storage/downloader.rb +174 -0
- data/lib/bsv/storage/errors.rb +8 -0
- data/lib/bsv/storage/utils.rb +90 -0
- data/lib/bsv/storage.rb +16 -0
- data/lib/bsv/transaction/beef.rb +173 -19
- data/lib/bsv/transaction/beef_party.rb +119 -0
- data/lib/bsv/transaction/chain_tracker.rb +2 -2
- data/lib/bsv/transaction/fee_model.rb +1 -1
- data/lib/bsv/transaction/fee_models/live_policy.rb +1 -1
- data/lib/bsv/transaction/fee_models/satoshis_per_kilobyte.rb +1 -1
- data/lib/bsv/transaction/merkle_path.rb +2 -2
- data/lib/bsv/transaction/p2pkh.rb +1 -1
- data/lib/bsv/transaction/transaction_input.rb +1 -1
- data/lib/bsv/transaction/{transaction.rb → tx.rb} +18 -18
- data/lib/bsv/transaction/unlocking_script_template.rb +2 -2
- data/lib/bsv/transaction.rb +3 -2
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +13 -0
- data/lib/bsv/wallet/proto_wallet.rb +12 -2
- data/lib/bsv/wallet/serializer/create_signature.rb +7 -0
- data/lib/bsv/wallet/serializer/get_public_key.rb +5 -1
- data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +6 -3
- data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +6 -3
- data/lib/bsv/wallet/serializer/verify_signature.rb +7 -0
- data/lib/bsv-sdk.rb +2 -0
- metadata +14 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Storage
|
|
7
|
+
# Helpers for encoding and decoding UHRP (Unified Hash Resource Protocol) URLs.
|
|
8
|
+
#
|
|
9
|
+
# A UHRP URL is a Base58Check string with a two-byte `\xCE\x00` prefix prepended
|
|
10
|
+
# to the SHA-256 hash of the content. This makes each URL self-verifying and
|
|
11
|
+
# stable across storage providers.
|
|
12
|
+
#
|
|
13
|
+
# == URL normalisation
|
|
14
|
+
#
|
|
15
|
+
# `normalize_url` strips the `uhrp:` scheme (case-insensitive) and the optional
|
|
16
|
+
# `//` authority prefix, matching the TS SDK contract exactly. The `web+uhrp://`
|
|
17
|
+
# variant used by some browser extension manifests is *not* normalised here — that
|
|
18
|
+
# diverges from the TS reference (Python normalises it; we follow TS).
|
|
19
|
+
module Utils
|
|
20
|
+
# Two-byte UHRP version prefix: 0xCE 0x00.
|
|
21
|
+
UHRP_PREFIX = "\xCE\x00".b.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Encode a 32-byte binary SHA-256 hash as a UHRP URL.
|
|
26
|
+
#
|
|
27
|
+
# @param hash [String] 32-byte binary string (SHA-256 hash)
|
|
28
|
+
# @return [String] Base58Check-encoded UHRP URL
|
|
29
|
+
# @raise [ArgumentError] if hash is not exactly 32 bytes
|
|
30
|
+
def get_url_for_hash(hash)
|
|
31
|
+
raise ArgumentError, 'Hash length must be 32 bytes (sha256)' unless hash.bytesize == 32
|
|
32
|
+
|
|
33
|
+
BSV::Primitives::Base58.check_encode(hash.b, prefix: UHRP_PREFIX)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Compute the SHA-256 hash of +data+ and encode it as a UHRP URL.
|
|
37
|
+
#
|
|
38
|
+
# @param data [String] binary file content
|
|
39
|
+
# @return [String] Base58Check-encoded UHRP URL
|
|
40
|
+
def get_url_for_file(data)
|
|
41
|
+
hash = OpenSSL::Digest::SHA256.digest(data.b)
|
|
42
|
+
get_url_for_hash(hash)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Decode a UHRP URL and return the 32-byte binary SHA-256 hash.
|
|
46
|
+
#
|
|
47
|
+
# Accepts URLs with or without the `uhrp:` scheme and optional `//` authority.
|
|
48
|
+
#
|
|
49
|
+
# @param url [String] UHRP URL (with or without `uhrp:` prefix)
|
|
50
|
+
# @return [String] 32-byte binary SHA-256 hash
|
|
51
|
+
# @raise [ArgumentError] if the URL is not a String, empty, has a bad prefix, or has wrong length
|
|
52
|
+
# @raise [BSV::Primitives::Base58::ChecksumError] if the Base58Check checksum fails
|
|
53
|
+
def get_hash_from_url(url)
|
|
54
|
+
raise ArgumentError, 'URL must be a String' unless url.is_a?(String)
|
|
55
|
+
raise ArgumentError, 'URL must not be empty' if url.empty?
|
|
56
|
+
|
|
57
|
+
normalised = normalize_url(url)
|
|
58
|
+
result = BSV::Primitives::Base58.check_decode(normalised, prefix_length: 2)
|
|
59
|
+
raise ArgumentError, 'Bad prefix' unless result[:prefix] == UHRP_PREFIX
|
|
60
|
+
raise ArgumentError, 'Invalid length!' unless result[:data].bytesize == 32
|
|
61
|
+
|
|
62
|
+
result[:data]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Strip the `uhrp:` scheme (case-insensitive) and optional `//` from a URL.
|
|
66
|
+
#
|
|
67
|
+
# Follows the TS SDK contract: only `uhrp:` and `uhrp://` are normalised.
|
|
68
|
+
# `web+uhrp://` is deliberately not stripped (TS does not strip it; Python does).
|
|
69
|
+
#
|
|
70
|
+
# @param url [String] URL to normalise
|
|
71
|
+
# @return [String] normalised URL
|
|
72
|
+
def normalize_url(url)
|
|
73
|
+
url = url.slice(5..) if url.downcase.start_with?('uhrp:')
|
|
74
|
+
url = url.slice(2..) if url.start_with?('//')
|
|
75
|
+
url
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Return +true+ if +url+ is a syntactically valid UHRP URL.
|
|
79
|
+
#
|
|
80
|
+
# @param url [String] URL to validate
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def valid_url?(url)
|
|
83
|
+
get_hash_from_url(url)
|
|
84
|
+
true
|
|
85
|
+
rescue ArgumentError, BSV::Primitives::Base58::ChecksumError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/bsv/storage.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
# Storage module for UHRP (Unified Hash Resource Protocol) URL encoding,
|
|
5
|
+
# decoding, and validation helpers.
|
|
6
|
+
#
|
|
7
|
+
# UHRP enables content-addressed storage on BSV: a SHA-256 hash of a file
|
|
8
|
+
# is Base58Check-encoded with the `\xCE\x00` prefix to produce a stable,
|
|
9
|
+
# self-verifying URL.
|
|
10
|
+
module Storage
|
|
11
|
+
autoload :Utils, 'bsv/storage/utils'
|
|
12
|
+
autoload :DownloadResult, 'bsv/storage/downloader'
|
|
13
|
+
autoload :Downloader, 'bsv/storage/downloader'
|
|
14
|
+
autoload :DownloadError, 'bsv/storage/errors'
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/bsv/transaction/beef.rb
CHANGED
|
@@ -72,10 +72,10 @@ module BSV
|
|
|
72
72
|
|
|
73
73
|
# A BEEF entry containing a raw transaction without a merkle proof.
|
|
74
74
|
class RawTxEntry < BeefTx
|
|
75
|
-
# @return [Transaction] the transaction
|
|
75
|
+
# @return [Transaction::Tx] the transaction
|
|
76
76
|
attr_reader :transaction
|
|
77
77
|
|
|
78
|
-
# @param transaction [Transaction] the transaction
|
|
78
|
+
# @param transaction [Transaction::Tx] the transaction
|
|
79
79
|
# @raise [ArgumentError] if transaction is nil
|
|
80
80
|
def initialize(transaction:)
|
|
81
81
|
raise ArgumentError, 'RawTxEntry requires a transaction' if transaction.nil?
|
|
@@ -97,13 +97,13 @@ module BSV
|
|
|
97
97
|
|
|
98
98
|
# A BEEF entry containing a raw transaction with an associated BUMP index.
|
|
99
99
|
class ProvenTxEntry < BeefTx
|
|
100
|
-
# @return [Transaction] the transaction
|
|
100
|
+
# @return [Transaction::Tx] the transaction
|
|
101
101
|
attr_reader :transaction
|
|
102
102
|
|
|
103
103
|
# @return [Integer] index into the BEEF bumps array
|
|
104
104
|
attr_reader :bump_index
|
|
105
105
|
|
|
106
|
-
# @param transaction [Transaction] the transaction
|
|
106
|
+
# @param transaction [Transaction::Tx] the transaction
|
|
107
107
|
# @param bump_index [Integer] index into the bumps array
|
|
108
108
|
# @raise [ArgumentError] if transaction or bump_index is nil
|
|
109
109
|
def initialize(transaction:, bump_index:)
|
|
@@ -188,7 +188,7 @@ module BSV
|
|
|
188
188
|
# After parsing, input source transactions are wired automatically.
|
|
189
189
|
#
|
|
190
190
|
# @param data [String] raw BEEF binary
|
|
191
|
-
# @return [Beef] the parsed BEEF bundle
|
|
191
|
+
# @return [Transaction::Beef] the parsed BEEF bundle
|
|
192
192
|
def self.from_binary(data)
|
|
193
193
|
raise ArgumentError, "truncated BEEF: need at least 4 bytes for version, got #{data.bytesize}" if data.bytesize < 4
|
|
194
194
|
|
|
@@ -247,7 +247,7 @@ module BSV
|
|
|
247
247
|
# Deserialise a BEEF bundle from a hex string.
|
|
248
248
|
#
|
|
249
249
|
# @param hex [String] hex-encoded BEEF data
|
|
250
|
-
# @return [Beef] the parsed BEEF bundle
|
|
250
|
+
# @return [Transaction::Beef] the parsed BEEF bundle
|
|
251
251
|
def self.from_hex(hex)
|
|
252
252
|
from_binary(BSV::Primitives::Hex.decode(hex, name: 'BEEF hex'))
|
|
253
253
|
end
|
|
@@ -320,7 +320,7 @@ module BSV
|
|
|
320
320
|
# Find a transaction in the bundle by its wire-order transaction ID.
|
|
321
321
|
#
|
|
322
322
|
# @param wtxid [String] 32-byte wire-order wtxid
|
|
323
|
-
# @return [Transaction, nil] the matching transaction, or nil
|
|
323
|
+
# @return [Transaction::Tx, nil] the matching transaction, or nil
|
|
324
324
|
def find_transaction(wtxid)
|
|
325
325
|
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
326
326
|
BSV.logger&.debug { "[Beef] find_transaction: #{wtxid.reverse.unpack1('H*')} in #{@transactions.length} entries" }
|
|
@@ -353,7 +353,7 @@ module BSV
|
|
|
353
353
|
# Find a transaction with all source_transactions wired for signing.
|
|
354
354
|
#
|
|
355
355
|
# @param wtxid [String] 32-byte wire-order wtxid
|
|
356
|
-
# @return [Transaction, nil] the transaction with wired inputs, or nil
|
|
356
|
+
# @return [Transaction::Tx, nil] the transaction with wired inputs, or nil
|
|
357
357
|
def find_transaction_for_signing(wtxid)
|
|
358
358
|
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
359
359
|
tx = find_transaction(wtxid)
|
|
@@ -367,7 +367,7 @@ module BSV
|
|
|
367
367
|
# and merkle paths) for atomic proof validation.
|
|
368
368
|
#
|
|
369
369
|
# @param wtxid [String] 32-byte wire-order wtxid
|
|
370
|
-
# @return [Transaction, nil] the transaction with full proof tree, or nil
|
|
370
|
+
# @return [Transaction::Tx, nil] the transaction with full proof tree, or nil
|
|
371
371
|
def find_atomic_transaction(wtxid)
|
|
372
372
|
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
373
373
|
tx = find_transaction(wtxid)
|
|
@@ -385,6 +385,161 @@ module BSV
|
|
|
385
385
|
to_atomic_binary(subject_wtxid).unpack1('H*')
|
|
386
386
|
end
|
|
387
387
|
|
|
388
|
+
# --- Supporting methods for multi-party BEEF exchange ---
|
|
389
|
+
|
|
390
|
+
# Add a TXID-only entry for +wtxid+ if no entry exists yet.
|
|
391
|
+
#
|
|
392
|
+
# If an entry already exists (in any format), the call is a no-op —
|
|
393
|
+
# TXID-only is the weakest format, so an existing stronger entry is kept.
|
|
394
|
+
#
|
|
395
|
+
# @param wtxid [String] 32-byte wire-order binary txid
|
|
396
|
+
# @return [BeefTx] the existing or newly added entry
|
|
397
|
+
def merge_txid_only(wtxid)
|
|
398
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
399
|
+
existing = @transactions.find { |bt| bt.wtxid == wtxid }
|
|
400
|
+
return existing if existing
|
|
401
|
+
|
|
402
|
+
entry = TxidOnlyEntry.new(known_wtxid: wtxid)
|
|
403
|
+
@transactions << entry
|
|
404
|
+
entry
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Return a shallow copy of this Transaction::Beef.
|
|
408
|
+
#
|
|
409
|
+
# Both +@bumps+ and +@transactions+ arrays are duplicated (new arrays),
|
|
410
|
+
# but the +BeefTx+ and +MerklePath+ objects they contain are shared
|
|
411
|
+
# references. This mirrors the TS SDK's +clone+ contract: entries are
|
|
412
|
+
# effectively immutable once added to a bundle, so shallow semantics are
|
|
413
|
+
# correct. If a deeper copy is ever required, add a separate +deep_dup+
|
|
414
|
+
# rather than changing this method's contract.
|
|
415
|
+
#
|
|
416
|
+
# +@subject_wtxid+ (Atomic BEEF) and +@txs_not_valid+ (cyclic-graph
|
|
417
|
+
# metadata) are preserved on the copy.
|
|
418
|
+
#
|
|
419
|
+
# @return [Transaction::Beef] a new shallow copy
|
|
420
|
+
def clone
|
|
421
|
+
# Use super (Object#clone) rather than self.class.new so subclasses
|
|
422
|
+
# (e.g. Transaction::BeefParty) with different initialize signatures
|
|
423
|
+
# work correctly. Object#clone does a shallow ivar copy without
|
|
424
|
+
# invoking initialize; we then dup the two arrays so the copy's
|
|
425
|
+
# contents can mutate independently.
|
|
426
|
+
c = super
|
|
427
|
+
c.instance_variable_set(:@bumps, @bumps.dup)
|
|
428
|
+
c.instance_variable_set(:@transactions, @transactions.dup)
|
|
429
|
+
c.instance_variable_set(:@txs_not_valid, @txs_not_valid&.dup)
|
|
430
|
+
c
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Return a new Transaction::Beef with TXID-only entries removed for any
|
|
434
|
+
# wtxid in +known_wtxids+.
|
|
435
|
+
#
|
|
436
|
+
# RAW_TX and RAW_TX_AND_BUMP entries are always retained, even when their
|
|
437
|
+
# wtxid appears in +known_wtxids+. Only +TxidOnlyEntry+ records are
|
|
438
|
+
# candidates for removal (they carry no proof data the recipient needs).
|
|
439
|
+
#
|
|
440
|
+
# After dropping TXID-only entries, any BUMP that is no longer referenced
|
|
441
|
+
# by any remaining +ProvenTxEntry+ is removed, and all +bump_index+
|
|
442
|
+
# fields are renumbered to match the new bumps array (mirrors TS
|
|
443
|
+
# +trimKnownTxids+ at +Beef.ts:861-914+).
|
|
444
|
+
#
|
|
445
|
+
# Does not mutate +self+. Starts from a {#clone} so the caller's state
|
|
446
|
+
# is preserved.
|
|
447
|
+
#
|
|
448
|
+
# @param known_wtxids [Array<String>] binary wtxids the recipient already has
|
|
449
|
+
# @return [Transaction::Beef] a new bundle with the specified entries trimmed
|
|
450
|
+
def trim_known_wtxids(known_wtxids)
|
|
451
|
+
known_set = known_wtxids.to_set
|
|
452
|
+
|
|
453
|
+
trimmed = clone
|
|
454
|
+
trimmed.instance_variable_set(
|
|
455
|
+
:@transactions,
|
|
456
|
+
trimmed.transactions.reject { |bt| bt.is_a?(TxidOnlyEntry) && known_set.include?(bt.wtxid) }
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Find which bump indices are still referenced
|
|
460
|
+
referenced = Set.new
|
|
461
|
+
trimmed.transactions.each do |bt|
|
|
462
|
+
referenced.add(bt.bump_index) if bt.is_a?(ProvenTxEntry)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# If all bumps are still referenced, nothing more to do
|
|
466
|
+
return trimmed if referenced.size == trimmed.bumps.length
|
|
467
|
+
|
|
468
|
+
# Build old → new index map for surviving bumps
|
|
469
|
+
index_map = {}
|
|
470
|
+
new_idx = 0
|
|
471
|
+
trimmed.bumps.each_with_index do |_, old_idx|
|
|
472
|
+
if referenced.include?(old_idx)
|
|
473
|
+
index_map[old_idx] = new_idx
|
|
474
|
+
new_idx += 1
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Drop unreferenced bumps
|
|
479
|
+
trimmed.instance_variable_set(
|
|
480
|
+
:@bumps,
|
|
481
|
+
trimmed.bumps.each_with_index.filter_map { |bump, i| bump if referenced.include?(i) }
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Renumber bump_index on all ProvenTxEntry records
|
|
485
|
+
trimmed.instance_variable_set(
|
|
486
|
+
:@transactions,
|
|
487
|
+
trimmed.transactions.map do |bt|
|
|
488
|
+
next bt unless bt.is_a?(ProvenTxEntry)
|
|
489
|
+
|
|
490
|
+
new_bump_idx = index_map.fetch(bt.bump_index)
|
|
491
|
+
ProvenTxEntry.new(transaction: bt.transaction, bump_index: new_bump_idx).tap do |e|
|
|
492
|
+
e.transaction.merkle_path = trimmed.bumps[new_bump_idx]
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
trimmed
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Return the wtxids of transactions that are "valid" in this bundle.
|
|
501
|
+
#
|
|
502
|
+
# A transaction is valid when it either:
|
|
503
|
+
# - has a merkle proof (is a +ProvenTxEntry+), or
|
|
504
|
+
# - all of its inputs chain back to proven transactions within the bundle.
|
|
505
|
+
#
|
|
506
|
+
# TXID-only entries are excluded. Cyclic transactions (from
|
|
507
|
+
# +@txs_not_valid+) are also excluded.
|
|
508
|
+
#
|
|
509
|
+
# Used by {Transaction::BeefParty} to record which wtxids a party gains
|
|
510
|
+
# knowledge of after a {#merge_beef_from_party} call.
|
|
511
|
+
#
|
|
512
|
+
# @return [Array<String>] binary wire-order wtxids
|
|
513
|
+
def valid_wtxids
|
|
514
|
+
invalid = @txs_not_valid || Set.new
|
|
515
|
+
known = Set.new
|
|
516
|
+
|
|
517
|
+
# Seed with proven entries (excluding any marked cyclic/unsortable)
|
|
518
|
+
@transactions.each do |bt|
|
|
519
|
+
known.add(bt.wtxid) if bt.is_a?(ProvenTxEntry) && !invalid.include?(bt.wtxid)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Iteratively resolve unproven entries whose inputs are all known.
|
|
523
|
+
# Skip @txs_not_valid entries — by definition they can't be ordered
|
|
524
|
+
# for validation, so a counterparty receiving them can't validate either.
|
|
525
|
+
changed = true
|
|
526
|
+
while changed
|
|
527
|
+
changed = false
|
|
528
|
+
@transactions.each do |bt|
|
|
529
|
+
next if bt.is_a?(TxidOnlyEntry) || known.include?(bt.wtxid)
|
|
530
|
+
next if invalid.include?(bt.wtxid)
|
|
531
|
+
next unless bt.respond_to?(:transaction)
|
|
532
|
+
|
|
533
|
+
if bt.transaction.inputs.all? { |inp| known.include?(inp.prev_wtxid) }
|
|
534
|
+
known.add(bt.wtxid)
|
|
535
|
+
changed = true
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
known.to_a
|
|
541
|
+
end
|
|
542
|
+
|
|
388
543
|
# --- Merge operations ---
|
|
389
544
|
|
|
390
545
|
# Add or deduplicate a merkle path (BUMP) in this BEEF bundle.
|
|
@@ -440,7 +595,7 @@ module BSV
|
|
|
440
595
|
# (same txid) are upgraded if a stronger format is now available (F5.7):
|
|
441
596
|
# TXID_ONLY → RAW_TX or RAW_TX_AND_BUMP; RAW_TX → RAW_TX_AND_BUMP.
|
|
442
597
|
#
|
|
443
|
-
# @param tx [Transaction] the transaction to merge
|
|
598
|
+
# @param tx [Transaction::Tx] the transaction to merge
|
|
444
599
|
# @return [BeefTx] the (possibly existing or upgraded) BeefTx entry
|
|
445
600
|
def merge_transaction(tx)
|
|
446
601
|
wtxid = tx.wtxid
|
|
@@ -479,7 +634,7 @@ module BSV
|
|
|
479
634
|
# @param bump_index [Integer, nil] optional BUMP index
|
|
480
635
|
# @return [BeefTx] the new or upgraded BeefTx entry
|
|
481
636
|
def merge_raw_tx(raw_bytes, bump_index: nil)
|
|
482
|
-
tx =
|
|
637
|
+
tx = Tx.from_binary(raw_bytes)
|
|
483
638
|
|
|
484
639
|
if bump_index
|
|
485
640
|
unless bump_index.is_a?(Integer) && bump_index >= 0 && bump_index < @bumps.length
|
|
@@ -512,7 +667,7 @@ module BSV
|
|
|
512
667
|
# BUMP indices are remapped during merge. New BeefTx instances are
|
|
513
668
|
# constructed rather than sharing references with the source bundle (F5.9).
|
|
514
669
|
#
|
|
515
|
-
# @param other [Beef] the BEEF bundle to merge from
|
|
670
|
+
# @param other [Transaction::Beef] the BEEF bundle to merge from
|
|
516
671
|
# @return [self]
|
|
517
672
|
# @raise [ArgumentError] if a transaction in +other+ has a +bump_index+
|
|
518
673
|
# that does not point to any BUMP in +other.bumps+ (i.e. the source
|
|
@@ -740,7 +895,7 @@ module BSV
|
|
|
740
895
|
case format
|
|
741
896
|
when FORMAT_TXID_ONLY
|
|
742
897
|
# Wire stores txid in internal (little-endian / wire) byte order;
|
|
743
|
-
# store as-is in known_wtxid so it matches
|
|
898
|
+
# store as-is in known_wtxid so it matches Tx#wtxid.
|
|
744
899
|
raise ArgumentError, 'truncated BEEF: not enough bytes for TXID_ONLY entry' if offset + 32 > data.bytesize
|
|
745
900
|
|
|
746
901
|
known_wtxid = data.byteslice(offset, 32)
|
|
@@ -749,12 +904,12 @@ module BSV
|
|
|
749
904
|
when FORMAT_RAW_TX_AND_BUMP
|
|
750
905
|
bump_index, vi_size = VarInt.decode(data, offset)
|
|
751
906
|
offset += vi_size
|
|
752
|
-
tx, consumed =
|
|
907
|
+
tx, consumed = Tx.from_binary_with_offset(data, offset)
|
|
753
908
|
offset += consumed
|
|
754
909
|
tx.merkle_path = beef.bumps[bump_index] if bump_index < beef.bumps.length
|
|
755
910
|
beef.transactions << ProvenTxEntry.new(transaction: tx, bump_index: bump_index)
|
|
756
911
|
when FORMAT_RAW_TX
|
|
757
|
-
tx, consumed =
|
|
912
|
+
tx, consumed = Tx.from_binary_with_offset(data, offset)
|
|
758
913
|
offset += consumed
|
|
759
914
|
beef.transactions << RawTxEntry.new(transaction: tx)
|
|
760
915
|
end
|
|
@@ -768,7 +923,7 @@ module BSV
|
|
|
768
923
|
offset += vi_size
|
|
769
924
|
|
|
770
925
|
num_txs.times do
|
|
771
|
-
tx, consumed =
|
|
926
|
+
tx, consumed = Tx.from_binary_with_offset(data, offset)
|
|
772
927
|
offset += consumed
|
|
773
928
|
|
|
774
929
|
has_bump = data.getbyte(offset)
|
|
@@ -800,8 +955,7 @@ module BSV
|
|
|
800
955
|
|
|
801
956
|
input.source_transaction = source
|
|
802
957
|
BSV.logger&.debug do
|
|
803
|
-
"[Beef] wired input #{input.
|
|
804
|
-
"-> source #{source.wtxid.reverse.unpack1('H*')}"
|
|
958
|
+
"[Beef] wired input #{input.dtxid_hex}:#{input.prev_tx_out_index} -> source #{source.dtxid}"
|
|
805
959
|
end
|
|
806
960
|
end
|
|
807
961
|
|
|
@@ -821,7 +975,7 @@ module BSV
|
|
|
821
975
|
# RAW_TX → RAW_TX_AND_BUMP (if bump now available)
|
|
822
976
|
#
|
|
823
977
|
# @param existing [BeefTx] the current entry in @transactions
|
|
824
|
-
# @param tx [Transaction, nil] the raw transaction (may be nil for TXID_ONLY → TXID_ONLY)
|
|
978
|
+
# @param tx [Transaction::Tx, nil] the raw transaction (may be nil for TXID_ONLY → TXID_ONLY)
|
|
825
979
|
# @param bump_index [Integer, nil] a BUMP index already validated against @bumps
|
|
826
980
|
# @return [BeefTx, nil] the upgraded entry, or nil when no upgrade is needed
|
|
827
981
|
def upgrade_beef_tx(existing, tx, bump_index: nil)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Transaction
|
|
5
|
+
# Transaction::Beef subclass that tracks per-party knowledge of wtxids.
|
|
6
|
+
#
|
|
7
|
+
# Used in multi-party BEEF exchange to avoid re-transmitting transaction
|
|
8
|
+
# data and proofs that a counterparty already possesses. Each party is
|
|
9
|
+
# identified by a caller-supplied string and associated with the set of
|
|
10
|
+
# wtxids it is known to hold.
|
|
11
|
+
#
|
|
12
|
+
# @example Two-party exchange
|
|
13
|
+
# party = BSV::Transaction::BeefParty.new(['alice', 'bob'])
|
|
14
|
+
# party.merge_beef_from_party('alice', alice_beef)
|
|
15
|
+
# trimmed = party.trimmed_beef_for_party('bob') # plain Transaction::Beef
|
|
16
|
+
class BeefParty < Beef
|
|
17
|
+
# @param initial_parties [Array<String>] optional initial party identifiers
|
|
18
|
+
# @raise [ArgumentError] if +initial_parties+ contains duplicates
|
|
19
|
+
#
|
|
20
|
+
# Defaults to BEEF_V2 (BRC-96) because TXID-only entries — which are
|
|
21
|
+
# central to BeefParty's purpose — are only valid in V2. Matches the TS
|
|
22
|
+
# SDK's +new Beef()+ default.
|
|
23
|
+
def initialize(initial_parties = [])
|
|
24
|
+
super(version: BEEF_V2)
|
|
25
|
+
@party_knowledge = {}
|
|
26
|
+
initial_parties.each { |p| add_party(p) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param party_id [String]
|
|
30
|
+
# @return [Boolean] true if +party_id+ has been added
|
|
31
|
+
def party?(party_id)
|
|
32
|
+
@party_knowledge.key?(party_id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Add a new unique party identifier.
|
|
36
|
+
#
|
|
37
|
+
# @param party_id [String]
|
|
38
|
+
# @raise [ArgumentError] if +party_id+ is already known
|
|
39
|
+
def add_party(party_id)
|
|
40
|
+
raise ArgumentError, "duplicate party #{party_id}" if party?(party_id)
|
|
41
|
+
|
|
42
|
+
@party_knowledge[party_id] = Set.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Record additional wtxids as known to +party_id+.
|
|
46
|
+
#
|
|
47
|
+
# Auto-creates the party if it is not yet known (TS parity). Also
|
|
48
|
+
# merges a TXID-only entry into the underlying bundle for each wtxid.
|
|
49
|
+
#
|
|
50
|
+
# @param party_id [String] party identifier (auto-created if unknown)
|
|
51
|
+
# @param wtxids [Array<String>] 32-byte wire-order binary wtxids
|
|
52
|
+
# @raise [ArgumentError] if any element of +wtxids+ is not a valid wtxid
|
|
53
|
+
def add_known_wtxids_for_party(party_id, wtxids)
|
|
54
|
+
add_party(party_id) unless party?(party_id)
|
|
55
|
+
wtxids.each do |wtxid|
|
|
56
|
+
BSV::Primitives::Hex.validate_wtxid!(wtxid, name: 'wtxid')
|
|
57
|
+
@party_knowledge[party_id].add(wtxid)
|
|
58
|
+
merge_txid_only(wtxid)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param party_id [String]
|
|
63
|
+
# @return [Array<String>] 32-byte wire-order binary wtxids known to +party_id+
|
|
64
|
+
# @raise [ArgumentError] if +party_id+ is unknown
|
|
65
|
+
def known_wtxids_for_party(party_id)
|
|
66
|
+
raise ArgumentError, "unknown party #{party_id}" unless party?(party_id)
|
|
67
|
+
|
|
68
|
+
@party_knowledge[party_id].to_a
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Merge a Transaction::Beef received from +party_id+.
|
|
72
|
+
#
|
|
73
|
+
# Merges all transactions and BUMPs from +beef_or_binary+ into +self+,
|
|
74
|
+
# then records the valid wtxids of the merged bundle as known to
|
|
75
|
+
# +party_id+. Auto-creates the party if not yet known.
|
|
76
|
+
#
|
|
77
|
+
# @param party_id [String] party identifier
|
|
78
|
+
# @param beef_or_binary [Transaction::Beef, String] a Transaction::Beef
|
|
79
|
+
# instance or raw binary BEEF bytes
|
|
80
|
+
def merge_beef_from_party(party_id, beef_or_binary)
|
|
81
|
+
beef = beef_or_binary.is_a?(Beef) ? beef_or_binary : Beef.from_binary(beef_or_binary)
|
|
82
|
+
# Capture the set of wtxids the party actually sent before merging.
|
|
83
|
+
beef_wtxids = beef.transactions.map(&:wtxid)
|
|
84
|
+
merge(beef)
|
|
85
|
+
# Record knowledge of any tx the party sent that is now provably valid
|
|
86
|
+
# against the merged state. This captures the cross-bundle case where
|
|
87
|
+
# an unproven tx in +beef+ becomes valid via inputs already proven in
|
|
88
|
+
# +self+ (and conversely doesn't record knowledge the party never sent).
|
|
89
|
+
known = valid_wtxids & beef_wtxids
|
|
90
|
+
add_known_wtxids_for_party(party_id, known)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Return a new Transaction::Beef (not Transaction::BeefParty) with
|
|
94
|
+
# TXID-only entries that are known to +party_id+ removed.
|
|
95
|
+
#
|
|
96
|
+
# RAW_TX and RAW_TX_AND_BUMP entries are always retained even when the
|
|
97
|
+
# party knows the txid — those formats carry proof data. BUMP indices
|
|
98
|
+
# are renumbered if any unreferenced bumps are dropped.
|
|
99
|
+
#
|
|
100
|
+
# Does not mutate +self+.
|
|
101
|
+
#
|
|
102
|
+
# @param party_id [String]
|
|
103
|
+
# @return [Transaction::Beef] a new trimmed bundle (plain Beef, not BeefParty)
|
|
104
|
+
# @raise [ArgumentError] if +party_id+ is unknown
|
|
105
|
+
def trimmed_beef_for_party(party_id)
|
|
106
|
+
raise ArgumentError, "unknown party #{party_id}" unless party?(party_id)
|
|
107
|
+
|
|
108
|
+
known = @party_knowledge[party_id].to_a
|
|
109
|
+
|
|
110
|
+
# Build a plain Beef from our current state so trim_known_wtxids
|
|
111
|
+
# returns a Beef, not a BeefParty.
|
|
112
|
+
plain = Beef.new(version: @version, bumps: @bumps.dup, transactions: @transactions.dup)
|
|
113
|
+
plain.instance_variable_set(:@subject_wtxid, @subject_wtxid)
|
|
114
|
+
plain.instance_variable_set(:@txs_not_valid, @txs_not_valid&.dup)
|
|
115
|
+
plain.trim_known_wtxids(known)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -4,7 +4,7 @@ module BSV
|
|
|
4
4
|
module Transaction
|
|
5
5
|
# Duck type for block header lookups used by the SDK's verify methods.
|
|
6
6
|
#
|
|
7
|
-
# {Beef#verify}, {MerklePath#verify}, and {
|
|
7
|
+
# {Beef#verify}, {MerklePath#verify}, and {Tx#verify} define
|
|
8
8
|
# what a valid structure *is* — they walk trees, check proofs, and
|
|
9
9
|
# compare roots. But they have no data source of their own. The
|
|
10
10
|
# chain tracker is the data source: an object the consumer provides
|
|
@@ -56,7 +56,7 @@ module BSV
|
|
|
56
56
|
# Return a default ChainTracker backed by the GorillaPool provider.
|
|
57
57
|
#
|
|
58
58
|
# @param testnet [Boolean] when true, uses the testnet provider
|
|
59
|
-
# @return [ChainTracker]
|
|
59
|
+
# @return [Transaction::ChainTracker]
|
|
60
60
|
def self.default(testnet: false)
|
|
61
61
|
new(BSV::Network::Providers::GorillaPool.default(testnet: testnet))
|
|
62
62
|
end
|
|
@@ -17,7 +17,7 @@ module BSV
|
|
|
17
17
|
class FeeModel
|
|
18
18
|
# Compute the fee for a transaction.
|
|
19
19
|
#
|
|
20
|
-
# @param transaction [Transaction] the transaction to compute the fee for
|
|
20
|
+
# @param transaction [Transaction::Tx] the transaction to compute the fee for
|
|
21
21
|
# @return [Integer] the fee in satoshis
|
|
22
22
|
# @raise [NotImplementedError] if not overridden by a subclass
|
|
23
23
|
def compute_fee(_transaction)
|
|
@@ -66,7 +66,7 @@ module BSV
|
|
|
66
66
|
|
|
67
67
|
# Compute the fee for a transaction using the latest ARC rate.
|
|
68
68
|
#
|
|
69
|
-
# @param transaction [Transaction] the transaction to compute the fee for
|
|
69
|
+
# @param transaction [Transaction::Tx] the transaction to compute the fee for
|
|
70
70
|
# @return [Integer] the fee in satoshis
|
|
71
71
|
def compute_fee(transaction)
|
|
72
72
|
rate = current_rate
|
|
@@ -23,7 +23,7 @@ module BSV
|
|
|
23
23
|
|
|
24
24
|
# Compute the fee for a transaction based on its estimated size.
|
|
25
25
|
#
|
|
26
|
-
# @param transaction [Transaction] the transaction to compute the fee for
|
|
26
|
+
# @param transaction [Transaction::Tx] the transaction to compute the fee for
|
|
27
27
|
# @return [Integer] the fee in satoshis
|
|
28
28
|
def compute_fee(transaction)
|
|
29
29
|
size = transaction.estimated_size
|
|
@@ -322,7 +322,7 @@ module BSV
|
|
|
322
322
|
# logic is: reject when `current_height - block_height < 100` (immature).
|
|
323
323
|
#
|
|
324
324
|
# @param dtxid_hex [String] hex-encoded transaction ID (display order)
|
|
325
|
-
# @param chain_tracker [ChainTracker] chain tracker to verify the root against
|
|
325
|
+
# @param chain_tracker [Transaction::ChainTracker] chain tracker to verify the root against
|
|
326
326
|
# @return [Boolean] true if the computed root matches the block at this height
|
|
327
327
|
def verify(dtxid_hex, chain_tracker)
|
|
328
328
|
BSV::Primitives::Hex.validate_dtxid_hex!(dtxid_hex, name: 'dtxid_hex')
|
|
@@ -450,7 +450,7 @@ module BSV
|
|
|
450
450
|
# that the extracted path computes the same merkle root as the
|
|
451
451
|
# source.
|
|
452
452
|
#
|
|
453
|
-
# The primary use case is +
|
|
453
|
+
# The primary use case is +Tx#to_beef+: when a BUMP loaded
|
|
454
454
|
# from a proof store carries +txid: true+ flags for transactions
|
|
455
455
|
# that are not part of the current BEEF bundle, extracting only the
|
|
456
456
|
# bundled txids strips the phantom flags (and the now-unneeded
|
|
@@ -24,7 +24,7 @@ module BSV
|
|
|
24
24
|
|
|
25
25
|
# Generate the P2PKH unlocking script for the given input.
|
|
26
26
|
#
|
|
27
|
-
# @param tx [Transaction] the transaction being signed
|
|
27
|
+
# @param tx [Transaction::Tx] the transaction being signed
|
|
28
28
|
# @param input_index [Integer] the input index to sign
|
|
29
29
|
# @return [Script::Script] the unlocking script (signature + pubkey)
|
|
30
30
|
def sign(tx, input_index)
|
|
@@ -26,7 +26,7 @@ module BSV
|
|
|
26
26
|
# @return [Script::Script, nil] locking script of the source output (needed for sighash)
|
|
27
27
|
attr_accessor :source_locking_script
|
|
28
28
|
|
|
29
|
-
# @return [Transaction, nil] the full source transaction (for BEEF wiring)
|
|
29
|
+
# @return [Transaction::Tx, nil] the full source transaction (for BEEF wiring)
|
|
30
30
|
attr_accessor :source_transaction
|
|
31
31
|
|
|
32
32
|
# @return [UnlockingScriptTemplate, nil] template for deferred signing
|