bsv-sdk 0.1.0 → 0.2.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 +49 -0
- data/lib/bsv/primitives/curve.rb +3 -3
- data/lib/bsv/primitives/ecies.rb +5 -5
- data/lib/bsv/primitives/private_key.rb +35 -0
- data/lib/bsv/primitives/public_key.rb +36 -0
- data/lib/bsv/primitives/signature.rb +2 -2
- data/lib/bsv/script/chunk.rb +15 -19
- data/lib/bsv/script/interpreter/interpreter.rb +13 -3
- data/lib/bsv/script/interpreter/operations/arithmetic.rb +1 -1
- data/lib/bsv/script/interpreter/operations/crypto.rb +1 -1
- data/lib/bsv/script/interpreter/operations/flow_control.rb +15 -8
- data/lib/bsv/script/interpreter/script_number.rb +1 -1
- data/lib/bsv/script/interpreter/stack.rb +2 -2
- data/lib/bsv/transaction/beef.rb +303 -1
- data/lib/bsv/transaction/chain_tracker.rb +43 -0
- data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +95 -0
- data/lib/bsv/transaction/chain_trackers.rb +10 -0
- data/lib/bsv/transaction/fee_model.rb +28 -0
- data/lib/bsv/transaction/fee_models/satoshis_per_kilobyte.rb +35 -0
- data/lib/bsv/transaction/fee_models.rb +10 -0
- data/lib/bsv/transaction/merkle_path.rb +17 -2
- data/lib/bsv/transaction/transaction.rb +397 -17
- data/lib/bsv/transaction/transaction_input.rb +16 -0
- data/lib/bsv/transaction/transaction_output.rb +18 -2
- data/lib/bsv/transaction/var_int.rb +20 -0
- data/lib/bsv/transaction/verification_error.rb +26 -0
- data/lib/bsv/transaction.rb +5 -0
- data/lib/bsv/version.rb +1 -1
- metadata +9 -2
data/lib/bsv/transaction/beef.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
module BSV
|
|
4
6
|
module Transaction
|
|
5
7
|
# Background Evaluation Extended Format (BEEF) for SPV-ready transaction
|
|
@@ -104,6 +106,11 @@ module BSV
|
|
|
104
106
|
# @param data [String] raw BEEF binary
|
|
105
107
|
# @return [Beef] the parsed BEEF bundle
|
|
106
108
|
def self.from_binary(data)
|
|
109
|
+
if data.bytesize < 4
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"truncated BEEF: need at least 4 bytes for version, got #{data.bytesize}"
|
|
112
|
+
end
|
|
113
|
+
|
|
107
114
|
offset = 0
|
|
108
115
|
|
|
109
116
|
version = data.byteslice(offset, 4).unpack1('V')
|
|
@@ -112,9 +119,13 @@ module BSV
|
|
|
112
119
|
beef = new(version: version)
|
|
113
120
|
|
|
114
121
|
if version == ATOMIC_BEEF
|
|
122
|
+
if data.bytesize < offset + 36
|
|
123
|
+
remaining = data.bytesize - offset
|
|
124
|
+
raise ArgumentError, "truncated Atomic BEEF: need 36 bytes at offset #{offset}, got #{remaining}"
|
|
125
|
+
end
|
|
126
|
+
|
|
115
127
|
beef.instance_variable_set(:@subject_txid, data.byteslice(offset, 32))
|
|
116
128
|
offset += 32
|
|
117
|
-
# Read inner V2 version
|
|
118
129
|
inner_version = data.byteslice(offset, 4).unpack1('V')
|
|
119
130
|
offset += 4
|
|
120
131
|
beef.instance_variable_set(:@version, inner_version)
|
|
@@ -208,6 +219,261 @@ module BSV
|
|
|
208
219
|
nil
|
|
209
220
|
end
|
|
210
221
|
|
|
222
|
+
# Find the merkle path (BUMP) for a transaction by its txid.
|
|
223
|
+
#
|
|
224
|
+
# @param txid [String] 32-byte txid in internal byte order
|
|
225
|
+
# @return [MerklePath, nil] the merkle path, or nil if not found
|
|
226
|
+
def find_bump(txid)
|
|
227
|
+
bt = @transactions.find { |entry| entry.txid == txid && entry.format == FORMAT_RAW_TX_AND_BUMP }
|
|
228
|
+
return unless bt
|
|
229
|
+
|
|
230
|
+
bt.transaction&.merkle_path || (bt.bump_index && @bumps[bt.bump_index])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Find a transaction with all source_transactions wired for signing.
|
|
234
|
+
#
|
|
235
|
+
# @param txid [String] 32-byte txid in internal byte order
|
|
236
|
+
# @return [Transaction, nil] the transaction with wired inputs, or nil
|
|
237
|
+
def find_transaction_for_signing(txid)
|
|
238
|
+
tx = find_transaction(txid)
|
|
239
|
+
return unless tx
|
|
240
|
+
|
|
241
|
+
wire_inputs(tx)
|
|
242
|
+
tx
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Find a transaction and recursively wire its ancestry (source transactions
|
|
246
|
+
# and merkle paths) for atomic proof validation.
|
|
247
|
+
#
|
|
248
|
+
# @param txid [String] 32-byte txid in internal byte order
|
|
249
|
+
# @return [Transaction, nil] the transaction with full proof tree, or nil
|
|
250
|
+
def find_atomic_transaction(txid)
|
|
251
|
+
tx = find_transaction(txid)
|
|
252
|
+
return unless tx
|
|
253
|
+
|
|
254
|
+
wire_ancestry(tx)
|
|
255
|
+
tx
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Serialise as Atomic BEEF (BRC-95) hex string.
|
|
259
|
+
#
|
|
260
|
+
# @param subject_txid [String] 32-byte subject transaction ID
|
|
261
|
+
# @return [String] hex-encoded Atomic BEEF
|
|
262
|
+
def to_atomic_hex(subject_txid)
|
|
263
|
+
to_atomic_binary(subject_txid).unpack1('H*')
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# --- Merge operations ---
|
|
267
|
+
|
|
268
|
+
# Add or deduplicate a merkle path (BUMP) in this BEEF bundle.
|
|
269
|
+
#
|
|
270
|
+
# If an existing BUMP shares the same block_height and merkle root,
|
|
271
|
+
# it is combined (via MerklePath#combine) and the existing index is
|
|
272
|
+
# returned. Otherwise the BUMP is appended.
|
|
273
|
+
#
|
|
274
|
+
# @param merkle_path [MerklePath] the BUMP to merge
|
|
275
|
+
# @return [Integer] the index of the (possibly merged) BUMP
|
|
276
|
+
def merge_bump(merkle_path)
|
|
277
|
+
root = merkle_path.compute_root
|
|
278
|
+
@bumps.each_with_index do |existing, idx|
|
|
279
|
+
next unless existing.block_height == merkle_path.block_height
|
|
280
|
+
|
|
281
|
+
if existing.compute_root == root
|
|
282
|
+
existing.combine(merkle_path)
|
|
283
|
+
return idx
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
@bumps << merkle_path
|
|
288
|
+
@bumps.length - 1
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Add a transaction to this BEEF bundle.
|
|
292
|
+
#
|
|
293
|
+
# Recursively merges the transaction's ancestors (via source_transaction
|
|
294
|
+
# references on inputs) and their merkle paths. Duplicate transactions
|
|
295
|
+
# (same txid) are not re-added.
|
|
296
|
+
#
|
|
297
|
+
# @param tx [Transaction] the transaction to merge
|
|
298
|
+
# @return [BeefTx] the (possibly existing) BeefTx entry
|
|
299
|
+
def merge_transaction(tx)
|
|
300
|
+
txid = tx.txid
|
|
301
|
+
|
|
302
|
+
# Check for existing entry
|
|
303
|
+
existing = @transactions.find { |bt| bt.txid == txid }
|
|
304
|
+
return existing if existing
|
|
305
|
+
|
|
306
|
+
# Recursively merge ancestors first (dependency order)
|
|
307
|
+
tx.inputs.each do |input|
|
|
308
|
+
merge_transaction(input.source_transaction) if input.source_transaction
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Merge this transaction's BUMP if it has one
|
|
312
|
+
entry = if tx.merkle_path
|
|
313
|
+
bump_idx = merge_bump(tx.merkle_path)
|
|
314
|
+
BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_idx)
|
|
315
|
+
else
|
|
316
|
+
BeefTx.new(format: FORMAT_RAW_TX, transaction: tx)
|
|
317
|
+
end
|
|
318
|
+
@transactions << entry
|
|
319
|
+
entry
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Add a transaction from raw binary data.
|
|
323
|
+
#
|
|
324
|
+
# @param raw_bytes [String] raw transaction binary
|
|
325
|
+
# @param bump_index [Integer, nil] optional BUMP index
|
|
326
|
+
# @return [BeefTx] the new BeefTx entry
|
|
327
|
+
def merge_raw_tx(raw_bytes, bump_index: nil)
|
|
328
|
+
tx = Transaction.from_binary(raw_bytes)
|
|
329
|
+
existing = @transactions.find { |bt| bt.txid == tx.txid }
|
|
330
|
+
return existing if existing
|
|
331
|
+
|
|
332
|
+
entry = if bump_index
|
|
333
|
+
tx.merkle_path = @bumps[bump_index] if bump_index < @bumps.length
|
|
334
|
+
BeefTx.new(format: FORMAT_RAW_TX_AND_BUMP, transaction: tx, bump_index: bump_index)
|
|
335
|
+
else
|
|
336
|
+
BeefTx.new(format: FORMAT_RAW_TX, transaction: tx)
|
|
337
|
+
end
|
|
338
|
+
@transactions << entry
|
|
339
|
+
entry
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Merge all BUMPs and transactions from another BEEF bundle.
|
|
343
|
+
#
|
|
344
|
+
# BUMP indices are remapped during merge.
|
|
345
|
+
#
|
|
346
|
+
# @param other [Beef] the BEEF bundle to merge from
|
|
347
|
+
# @return [self]
|
|
348
|
+
def merge(other)
|
|
349
|
+
# Build index remap for BUMPs
|
|
350
|
+
bump_remap = {}
|
|
351
|
+
other.bumps.each_with_index do |bump, old_idx|
|
|
352
|
+
bump_remap[old_idx] = merge_bump(bump)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Merge transactions with remapped BUMP indices
|
|
356
|
+
other.transactions.each do |beef_tx|
|
|
357
|
+
case beef_tx.format
|
|
358
|
+
when FORMAT_TXID_ONLY
|
|
359
|
+
next if @transactions.any? { |bt| bt.txid == beef_tx.known_txid }
|
|
360
|
+
|
|
361
|
+
@transactions << BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: beef_tx.known_txid)
|
|
362
|
+
else
|
|
363
|
+
next if @transactions.any? { |bt| bt.txid == beef_tx.txid }
|
|
364
|
+
|
|
365
|
+
if beef_tx.format == FORMAT_RAW_TX_AND_BUMP && beef_tx.bump_index
|
|
366
|
+
new_idx = bump_remap[beef_tx.bump_index] || beef_tx.bump_index
|
|
367
|
+
beef_tx.transaction.merkle_path = @bumps[new_idx]
|
|
368
|
+
@transactions << BeefTx.new(
|
|
369
|
+
format: FORMAT_RAW_TX_AND_BUMP,
|
|
370
|
+
transaction: beef_tx.transaction,
|
|
371
|
+
bump_index: new_idx
|
|
372
|
+
)
|
|
373
|
+
else
|
|
374
|
+
@transactions << BeefTx.new(format: FORMAT_RAW_TX, transaction: beef_tx.transaction)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
self
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Convert a transaction entry to TXID-only format.
|
|
383
|
+
#
|
|
384
|
+
# @param txid [String] 32-byte txid in internal byte order
|
|
385
|
+
# @return [BeefTx, nil] the converted entry, or nil if not found
|
|
386
|
+
def make_txid_only(txid)
|
|
387
|
+
idx = @transactions.index { |bt| bt.txid == txid }
|
|
388
|
+
return unless idx
|
|
389
|
+
|
|
390
|
+
@transactions[idx] = BeefTx.new(format: FORMAT_TXID_ONLY, known_txid: txid)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# --- Validation ---
|
|
394
|
+
|
|
395
|
+
# Check structural validity of the BEEF bundle.
|
|
396
|
+
#
|
|
397
|
+
# A valid BEEF has every transaction either:
|
|
398
|
+
# - proven (has a BUMP / merkle_path), or
|
|
399
|
+
# - all its inputs reference transactions that are themselves valid
|
|
400
|
+
# within this bundle.
|
|
401
|
+
#
|
|
402
|
+
# @param allow_txid_only [Boolean] whether TXID-only entries count as valid (default: false)
|
|
403
|
+
# @return [Boolean] true if structurally valid
|
|
404
|
+
def valid?(allow_txid_only: false)
|
|
405
|
+
known_txids = build_known_txids(allow_txid_only)
|
|
406
|
+
|
|
407
|
+
# TXID-only entries are invalid unless explicitly allowed
|
|
408
|
+
has_txid_only = @transactions.any? { |bt| bt.format == FORMAT_TXID_ONLY }
|
|
409
|
+
return false if has_txid_only && !allow_txid_only
|
|
410
|
+
|
|
411
|
+
pending = @transactions.select { |bt| bt.transaction && !known_txids.include?(bt.txid) }
|
|
412
|
+
|
|
413
|
+
# Iteratively resolve: if all inputs of a tx are known, it becomes known
|
|
414
|
+
changed = true
|
|
415
|
+
while changed
|
|
416
|
+
changed = false
|
|
417
|
+
pending.reject! do |bt|
|
|
418
|
+
all_inputs_known = bt.transaction.inputs.all? do |input|
|
|
419
|
+
known_txids.include?(input.prev_tx_id.reverse)
|
|
420
|
+
end
|
|
421
|
+
if all_inputs_known
|
|
422
|
+
known_txids.add(bt.txid)
|
|
423
|
+
changed = true
|
|
424
|
+
end
|
|
425
|
+
all_inputs_known
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
pending.empty?
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Sort transactions in topological (dependency) order in place.
|
|
433
|
+
#
|
|
434
|
+
# After sorting, every transaction's input ancestors appear before it
|
|
435
|
+
# in the array. This is required for correct BEEF serialisation.
|
|
436
|
+
#
|
|
437
|
+
# @return [self]
|
|
438
|
+
def sort_transactions!
|
|
439
|
+
return self if @transactions.length <= 1
|
|
440
|
+
|
|
441
|
+
txid_index = {}
|
|
442
|
+
@transactions.each_with_index { |bt, i| txid_index[bt.txid] = i }
|
|
443
|
+
|
|
444
|
+
# Build adjacency: for each tx, which other txs must come before it?
|
|
445
|
+
in_degree = Array.new(@transactions.length, 0)
|
|
446
|
+
dependents = Array.new(@transactions.length) { [] }
|
|
447
|
+
|
|
448
|
+
@transactions.each_with_index do |bt, i|
|
|
449
|
+
next unless bt.transaction
|
|
450
|
+
|
|
451
|
+
bt.transaction.inputs.each do |input|
|
|
452
|
+
dep_idx = txid_index[input.prev_tx_id.reverse]
|
|
453
|
+
next unless dep_idx
|
|
454
|
+
|
|
455
|
+
dependents[dep_idx] << i
|
|
456
|
+
in_degree[i] += 1
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Kahn's algorithm
|
|
461
|
+
queue = (0...@transactions.length).select { |i| in_degree[i].zero? }
|
|
462
|
+
sorted = []
|
|
463
|
+
|
|
464
|
+
until queue.empty?
|
|
465
|
+
idx = queue.shift
|
|
466
|
+
sorted << @transactions[idx]
|
|
467
|
+
dependents[idx].each do |dep|
|
|
468
|
+
in_degree[dep] -= 1
|
|
469
|
+
queue << dep if in_degree[dep].zero?
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
@transactions = sorted
|
|
474
|
+
self
|
|
475
|
+
end
|
|
476
|
+
|
|
211
477
|
# --- Private class methods for deserialisation ---
|
|
212
478
|
|
|
213
479
|
class << self
|
|
@@ -303,6 +569,42 @@ module BSV
|
|
|
303
569
|
|
|
304
570
|
private
|
|
305
571
|
|
|
572
|
+
# Build a set of txids that are "known" (proven or txid-only).
|
|
573
|
+
def build_known_txids(allow_txid_only)
|
|
574
|
+
known = Set.new
|
|
575
|
+
@transactions.each do |bt|
|
|
576
|
+
case bt.format
|
|
577
|
+
when FORMAT_RAW_TX_AND_BUMP
|
|
578
|
+
known.add(bt.txid)
|
|
579
|
+
when FORMAT_TXID_ONLY
|
|
580
|
+
known.add(bt.txid) if allow_txid_only
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
known
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Wire source_transaction references on a transaction's inputs.
|
|
587
|
+
def wire_inputs(tx)
|
|
588
|
+
tx.inputs.each do |input|
|
|
589
|
+
next if input.source_transaction
|
|
590
|
+
|
|
591
|
+
source = find_transaction(input.prev_tx_id.reverse)
|
|
592
|
+
input.source_transaction = source if source
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Recursively wire source_transactions and merkle_paths.
|
|
597
|
+
def wire_ancestry(tx)
|
|
598
|
+
wire_inputs(tx)
|
|
599
|
+
tx.inputs.each do |input|
|
|
600
|
+
next unless input.source_transaction
|
|
601
|
+
|
|
602
|
+
source = input.source_transaction
|
|
603
|
+
source.merkle_path ||= find_bump(source.txid)
|
|
604
|
+
wire_ancestry(source)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
306
608
|
def build_bump_map
|
|
307
609
|
map = {}.compare_by_identity
|
|
308
610
|
idx = 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Transaction
|
|
5
|
+
# Base class for chain trackers that verify merkle roots against the blockchain.
|
|
6
|
+
#
|
|
7
|
+
# Chain trackers confirm that a given merkle root corresponds to a valid block
|
|
8
|
+
# at a specific height. This is essential for SPV verification — without it,
|
|
9
|
+
# merkle proofs cannot be validated against the actual blockchain.
|
|
10
|
+
#
|
|
11
|
+
# Subclasses must implement {#valid_root_for_height?} and {#current_height}.
|
|
12
|
+
#
|
|
13
|
+
# @example Implementing a custom chain tracker
|
|
14
|
+
# class MyTracker < BSV::Transaction::ChainTracker
|
|
15
|
+
# def valid_root_for_height?(root, height)
|
|
16
|
+
# # query your block header source
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def current_height
|
|
20
|
+
# # return current chain tip height
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
class ChainTracker
|
|
24
|
+
# Verify that a merkle root is valid for the given block height.
|
|
25
|
+
#
|
|
26
|
+
# @param root [String] merkle root as a hex string
|
|
27
|
+
# @param height [Integer] block height
|
|
28
|
+
# @return [Boolean] true if the root matches the block at the given height
|
|
29
|
+
# @raise [NotImplementedError] if not overridden by a subclass
|
|
30
|
+
def valid_root_for_height?(_root, _height)
|
|
31
|
+
raise NotImplementedError, "#{self.class}#valid_root_for_height? must be implemented"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Return the current blockchain height.
|
|
35
|
+
#
|
|
36
|
+
# @return [Integer] the height of the chain tip
|
|
37
|
+
# @raise [NotImplementedError] if not overridden by a subclass
|
|
38
|
+
def current_height
|
|
39
|
+
raise NotImplementedError, "#{self.class}#current_height must be implemented"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module BSV
|
|
8
|
+
module Transaction
|
|
9
|
+
module ChainTrackers
|
|
10
|
+
# Chain tracker that verifies merkle roots using the WhatsOnChain API.
|
|
11
|
+
#
|
|
12
|
+
# Queries the WoC block header endpoint to retrieve the merkle root for a
|
|
13
|
+
# given block height and compares it with the provided root.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# tracker = BSV::Transaction::ChainTrackers::WhatsOnChain.new
|
|
17
|
+
# tracker.valid_root_for_height?('abcd...', 800_000)
|
|
18
|
+
class WhatsOnChain < ChainTracker
|
|
19
|
+
BASE_URL = 'https://api.whatsonchain.com'
|
|
20
|
+
|
|
21
|
+
NETWORKS = {
|
|
22
|
+
main: 'main',
|
|
23
|
+
mainnet: 'main',
|
|
24
|
+
test: 'test',
|
|
25
|
+
testnet: 'test',
|
|
26
|
+
stn: 'stn'
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# @param network [Symbol] :main, :mainnet, :test, :testnet, or :stn
|
|
30
|
+
# @param api_key [String, nil] optional WoC API key
|
|
31
|
+
# @param http_client [#request, nil] injectable HTTP client for testing
|
|
32
|
+
def initialize(network: :main, api_key: nil, http_client: nil)
|
|
33
|
+
super()
|
|
34
|
+
@network = NETWORKS.fetch(network) { raise ArgumentError, "unknown network: #{network}" }
|
|
35
|
+
@api_key = api_key
|
|
36
|
+
@http_client = http_client
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Verify that a merkle root is valid for the given block height.
|
|
40
|
+
#
|
|
41
|
+
# @param root [String] merkle root as a hex string
|
|
42
|
+
# @param height [Integer] block height
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def valid_root_for_height?(root, height)
|
|
45
|
+
response = get("/v1/bsv/#{@network}/block/#{height}/header")
|
|
46
|
+
return false if response.nil?
|
|
47
|
+
|
|
48
|
+
data = JSON.parse(response.body)
|
|
49
|
+
data['merkleroot'].downcase == root.downcase
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return the current blockchain height.
|
|
53
|
+
#
|
|
54
|
+
# @return [Integer]
|
|
55
|
+
def current_height
|
|
56
|
+
response = get("/v1/bsv/#{@network}/chain/info", not_found_returns_nil: false)
|
|
57
|
+
data = JSON.parse(response.body)
|
|
58
|
+
data['blocks']
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# @param path [String] API path
|
|
64
|
+
# @param not_found_returns_nil [Boolean] if true, return nil on 404 instead of raising
|
|
65
|
+
# @return [Net::HTTPResponse, nil]
|
|
66
|
+
def get(path, not_found_returns_nil: true)
|
|
67
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
68
|
+
request = Net::HTTP::Get.new(uri)
|
|
69
|
+
request['Authorization'] = @api_key if @api_key
|
|
70
|
+
|
|
71
|
+
response = execute(uri, request)
|
|
72
|
+
code = response.code.to_i
|
|
73
|
+
|
|
74
|
+
return nil if not_found_returns_nil && code == 404
|
|
75
|
+
return response if (200..299).cover?(code)
|
|
76
|
+
|
|
77
|
+
raise BSV::Network::ChainProviderError.new(
|
|
78
|
+
response.body || "HTTP #{code}",
|
|
79
|
+
status_code: code
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def execute(uri, request)
|
|
84
|
+
if @http_client
|
|
85
|
+
@http_client.request(uri, request)
|
|
86
|
+
else
|
|
87
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
88
|
+
http.request(request)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Transaction
|
|
5
|
+
# Base class for fee models that compute transaction fees.
|
|
6
|
+
#
|
|
7
|
+
# Fee models determine how many satoshis a transaction should pay in fees
|
|
8
|
+
# based on its size or other properties. Subclasses must implement
|
|
9
|
+
# {#compute_fee}.
|
|
10
|
+
#
|
|
11
|
+
# @example Implementing a custom fee model
|
|
12
|
+
# class FixedFee < BSV::Transaction::FeeModel
|
|
13
|
+
# def compute_fee(_transaction)
|
|
14
|
+
# 1000
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
class FeeModel
|
|
18
|
+
# Compute the fee for a transaction.
|
|
19
|
+
#
|
|
20
|
+
# @param transaction [Transaction] the transaction to compute the fee for
|
|
21
|
+
# @return [Integer] the fee in satoshis
|
|
22
|
+
# @raise [NotImplementedError] if not overridden by a subclass
|
|
23
|
+
def compute_fee(_transaction)
|
|
24
|
+
raise NotImplementedError, "#{self.class}#compute_fee must be implemented"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Transaction
|
|
5
|
+
module FeeModels
|
|
6
|
+
# Fee model that charges a configurable number of satoshis per kilobyte.
|
|
7
|
+
#
|
|
8
|
+
# Uses the transaction's estimated size (from templates for unsigned inputs,
|
|
9
|
+
# actual size for signed inputs) to compute the fee.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# model = BSV::Transaction::FeeModels::SatoshisPerKilobyte.new(value: 100)
|
|
13
|
+
# fee = model.compute_fee(transaction) # => 25 (for a ~250 byte tx)
|
|
14
|
+
class SatoshisPerKilobyte < FeeModel
|
|
15
|
+
# @return [Integer] satoshis per kilobyte rate
|
|
16
|
+
attr_reader :value
|
|
17
|
+
|
|
18
|
+
# @param value [Integer] satoshis per kilobyte (default: 50)
|
|
19
|
+
def initialize(value: 50)
|
|
20
|
+
super()
|
|
21
|
+
@value = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Compute the fee for a transaction based on its estimated size.
|
|
25
|
+
#
|
|
26
|
+
# @param transaction [Transaction] the transaction to compute the fee for
|
|
27
|
+
# @return [Integer] the fee in satoshis
|
|
28
|
+
def compute_fee(transaction)
|
|
29
|
+
size = transaction.estimated_size
|
|
30
|
+
(size / 1000.0 * @value).ceil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -186,6 +186,21 @@ module BSV
|
|
|
186
186
|
compute_root(txid).reverse.unpack1('H*')
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
+
# --- Verification ---
|
|
190
|
+
|
|
191
|
+
# Verify that this merkle path is valid for a given transaction.
|
|
192
|
+
#
|
|
193
|
+
# Computes the merkle root from the path and txid, then checks it
|
|
194
|
+
# against the blockchain via the provided chain tracker.
|
|
195
|
+
#
|
|
196
|
+
# @param txid_hex [String] hex-encoded transaction ID (display order)
|
|
197
|
+
# @param chain_tracker [ChainTracker] chain tracker to verify the root against
|
|
198
|
+
# @return [Boolean] true if the computed root matches the block at this height
|
|
199
|
+
def verify(txid_hex, chain_tracker)
|
|
200
|
+
root_hex = compute_root_hex(txid_hex)
|
|
201
|
+
chain_tracker.valid_root_for_height?(root_hex, @block_height)
|
|
202
|
+
end
|
|
203
|
+
|
|
189
204
|
# --- Combine ---
|
|
190
205
|
|
|
191
206
|
# Merge another merkle path into this one.
|
|
@@ -208,7 +223,7 @@ module BSV
|
|
|
208
223
|
@path << [] if h >= @path.length
|
|
209
224
|
next if h >= other.path.length
|
|
210
225
|
|
|
211
|
-
existing = @path[h].
|
|
226
|
+
existing = @path[h].to_h { |e| [e.offset, e] }
|
|
212
227
|
other.path[h].each do |elem|
|
|
213
228
|
existing[elem.offset] ||= elem
|
|
214
229
|
end
|
|
@@ -222,7 +237,7 @@ module BSV
|
|
|
222
237
|
|
|
223
238
|
def build_indexed_path
|
|
224
239
|
@path.map do |level|
|
|
225
|
-
level.
|
|
240
|
+
level.to_h { |elem| [elem.offset, elem] }
|
|
226
241
|
end
|
|
227
242
|
end
|
|
228
243
|
|