bsv-sdk 0.25.0 → 0.26.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 +34 -0
- data/README.md +4 -2
- data/lib/bsv/primitives/digest.rb +107 -13
- data/lib/bsv/script/interpreter/error.rb +2 -0
- data/lib/bsv/script/interpreter/interpreter.rb +226 -7
- data/lib/bsv/script/interpreter/operations/flow_control.rb +30 -13
- data/lib/bsv/script/script.rb +21 -2
- data/lib/bsv/storage/downloader.rb +1 -1
- data/lib/bsv/storage/utils.rb +1 -1
- data/lib/bsv/transaction/transaction_input.rb +77 -13
- data/lib/bsv/transaction/transaction_output.rb +46 -6
- data/lib/bsv/transaction/tx.rb +359 -28
- data/lib/bsv/version.rb +1 -1
- metadata +1 -2
- data/lib/bsv/secp256k1_native.bundle +0 -0
data/lib/bsv/transaction/tx.rb
CHANGED
|
@@ -9,6 +9,13 @@ module BSV
|
|
|
9
9
|
# computation (with FORKID), signing, script verification, and fee
|
|
10
10
|
# estimation.
|
|
11
11
|
#
|
|
12
|
+
# @note Not thread-safe. Direct mutation of {#inputs} / {#outputs} arrays
|
|
13
|
+
# (e.g. `tx.inputs << input`) bypasses the cache-invalidation contract
|
|
14
|
+
# and may produce silently invalid sighashes. Use {#add_input} /
|
|
15
|
+
# {#add_output} and the documented setter surface, or call
|
|
16
|
+
# {#invalidate_caches} after direct mutation. See
|
|
17
|
+
# {file:docs/reference/sighash-cache.md}.
|
|
18
|
+
#
|
|
12
19
|
# @example Build, sign, and serialise a transaction
|
|
13
20
|
# tx = BSV::Transaction::Tx.new
|
|
14
21
|
# tx.add_input(input)
|
|
@@ -59,35 +66,151 @@ module BSV
|
|
|
59
66
|
|
|
60
67
|
# Append a transaction input.
|
|
61
68
|
#
|
|
69
|
+
# Idempotent on re-add: calling +add_input(x)+ twice with the same input
|
|
70
|
+
# on the same +Transaction::Tx+ returns +self+ on the second call without
|
|
71
|
+
# appending a duplicate or invalidating caches. Raises +ArgumentError+
|
|
72
|
+
# if the input is already attached to a different +Transaction::Tx+.
|
|
73
|
+
#
|
|
62
74
|
# @param input [Transaction::TransactionInput] the input to add
|
|
63
75
|
# @return [self] for chaining
|
|
76
|
+
# @raise [ArgumentError] if the input is already attached to a different
|
|
77
|
+
# Tx instance. Sharing across Tx instances is anti-idiomatic;
|
|
78
|
+
# construct a fresh instance per Tx. See
|
|
79
|
+
# {file:docs/reference/sighash-cache.md#one-owner}.
|
|
64
80
|
def add_input(input)
|
|
81
|
+
existing_owner = input.instance_variable_get(:@owning_tx)
|
|
82
|
+
if existing_owner
|
|
83
|
+
return self if existing_owner.equal?(self)
|
|
84
|
+
|
|
85
|
+
raise ArgumentError,
|
|
86
|
+
"TransactionInput #{input.dtxid_hex}:#{input.prev_tx_out_index} is already attached to a Tx"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
input.instance_variable_set(:@owning_tx, self)
|
|
65
90
|
@inputs << input
|
|
91
|
+
# Avoid the O(N+M) rebind walk inside invalidate_caches — the new
|
|
92
|
+
# input's backref was just set above and every existing input/output
|
|
93
|
+
# was rebound when it was originally added. clear_caches is O(1).
|
|
94
|
+
clear_caches
|
|
66
95
|
self
|
|
67
96
|
end
|
|
68
97
|
|
|
69
98
|
# Append a transaction output.
|
|
70
99
|
#
|
|
100
|
+
# Idempotent on re-add: calling +add_output(x)+ twice with the same output
|
|
101
|
+
# on the same +Transaction::Tx+ returns +self+ on the second call without
|
|
102
|
+
# appending a duplicate or invalidating caches. Raises +ArgumentError+
|
|
103
|
+
# if the output is already attached to a different +Transaction::Tx+.
|
|
104
|
+
#
|
|
71
105
|
# @param output [Transaction::TransactionOutput] the output to add
|
|
72
106
|
# @return [self] for chaining
|
|
107
|
+
# @raise [ArgumentError] if the output is already attached to a different
|
|
108
|
+
# Tx instance. Sharing across Tx instances is anti-idiomatic;
|
|
109
|
+
# construct a fresh instance per Tx. See
|
|
110
|
+
# {file:docs/reference/sighash-cache.md#one-owner}.
|
|
73
111
|
def add_output(output)
|
|
112
|
+
existing_owner = output.instance_variable_get(:@owning_tx)
|
|
113
|
+
if existing_owner
|
|
114
|
+
return self if existing_owner.equal?(self)
|
|
115
|
+
|
|
116
|
+
asm_fragment = output.locking_script&.to_asm || '<nil locking_script>'
|
|
117
|
+
asm_fragment = "#{asm_fragment[0, 40]}..." if asm_fragment.length > 40
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"TransactionOutput (#{asm_fragment}) is already attached to a Tx"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
output.instance_variable_set(:@owning_tx, self)
|
|
74
123
|
@outputs << output
|
|
124
|
+
# Avoid the O(N+M) rebind walk inside invalidate_caches — the new
|
|
125
|
+
# output's backref was just set above and every existing input/output
|
|
126
|
+
# was rebound when it was originally added. clear_caches is O(1).
|
|
127
|
+
clear_caches
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Invalidate all cached state on this transaction (sighash components,
|
|
132
|
+
# wire serialisation, etc.) AND restore the +@owning_tx+ backref on every
|
|
133
|
+
# current input and output. This is what makes the escape hatch complete:
|
|
134
|
+
# if a caller appended an input/output via direct array mutation
|
|
135
|
+
# (bypassing +add_input+ / +add_output+), the new struct's backref is nil
|
|
136
|
+
# and future +sequence=+ / +locking_script=+ / etc. setters would not
|
|
137
|
+
# bubble invalidation up. After +invalidate_caches+, the backref is
|
|
138
|
+
# rebound so subsequent setter mutations flow through correctly.
|
|
139
|
+
#
|
|
140
|
+
# Raises +ArgumentError+ if any input or output is already attached to a
|
|
141
|
+
# different +Transaction::Tx+ — matches the cross-Tx rebind contract of
|
|
142
|
+
# +add_input+ / +add_output+.
|
|
143
|
+
#
|
|
144
|
+
# @example After mutating the inputs array directly
|
|
145
|
+
# tx.inputs << input # bypasses the documented setter surface
|
|
146
|
+
# tx.invalidate_caches # clears all cache layers + rebinds @owning_tx
|
|
147
|
+
# input.sequence = 42 # now invalidates correctly
|
|
148
|
+
# tx.verify(...) # safe
|
|
149
|
+
#
|
|
150
|
+
# @note Rarely needed. Normal mutation through the documented setter
|
|
151
|
+
# surface (input.sequence=, output.satoshis=, etc.) invalidates the
|
|
152
|
+
# right cache slices automatically. This method is an escape hatch
|
|
153
|
+
# for code that mutates @inputs / @outputs through unsupported paths.
|
|
154
|
+
# See {file:docs/reference/sighash-cache.md#escape-hatch}.
|
|
155
|
+
#
|
|
156
|
+
# @return [self] for chaining
|
|
157
|
+
# @raise [ArgumentError] if any input or output is attached to a different
|
|
158
|
+
# Tx instance
|
|
159
|
+
def invalidate_caches
|
|
160
|
+
rebind_owning_tx!
|
|
161
|
+
clear_caches
|
|
75
162
|
self
|
|
76
163
|
end
|
|
77
164
|
|
|
165
|
+
# Called by +#dup+ and +#clone+. Deep-dups +@inputs+ and +@outputs+ so that
|
|
166
|
+
# the copy and the original do not share mutable input/output state. Rebinds
|
|
167
|
+
# +@owning_tx+ on each copied struct to +self+ (the new transaction). Resets
|
|
168
|
+
# all cache ivars directly so the dup does not share mutable cache state
|
|
169
|
+
# (especially +@hash_outputs_single+, a Hash) with the original.
|
|
170
|
+
#
|
|
171
|
+
# This closes the hazard at +beef.rb:703,707+ where a shallow dup would leave
|
|
172
|
+
# the copied inputs/outputs pointing at the original transaction's cache.
|
|
173
|
+
#
|
|
174
|
+
# Note: we cannot delegate to +invalidate_caches+ here because that method
|
|
175
|
+
# calls +Hash#clear+ on +@hash_outputs_single+, which would mutate the
|
|
176
|
+
# shared Hash and evict the *original*'s per-index cache too. Direct
|
|
177
|
+
# +ivar = nil+ assignments on +self+ leave the original untouched.
|
|
178
|
+
def initialize_copy(other)
|
|
179
|
+
super
|
|
180
|
+
@inputs = @inputs.map do |i|
|
|
181
|
+
dup_input = i.dup
|
|
182
|
+
dup_input.instance_variable_set(:@owning_tx, self)
|
|
183
|
+
dup_input
|
|
184
|
+
end
|
|
185
|
+
@outputs = @outputs.map do |o|
|
|
186
|
+
dup_output = o.dup
|
|
187
|
+
dup_output.instance_variable_set(:@owning_tx, self)
|
|
188
|
+
dup_output
|
|
189
|
+
end
|
|
190
|
+
@to_binary = nil
|
|
191
|
+
@wtxid = nil
|
|
192
|
+
@hash_prevouts = nil
|
|
193
|
+
@hash_sequence = nil
|
|
194
|
+
@hash_outputs_all = nil
|
|
195
|
+
@hash_outputs_single = nil
|
|
196
|
+
end
|
|
197
|
+
|
|
78
198
|
# --- Serialisation ---
|
|
79
199
|
|
|
80
200
|
# Serialise the transaction to its binary wire format.
|
|
81
201
|
#
|
|
82
|
-
# @
|
|
202
|
+
# @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
|
|
203
|
+
# @return [String] raw transaction bytes (binary encoding, frozen)
|
|
83
204
|
def to_binary
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
205
|
+
@to_binary ||= begin
|
|
206
|
+
buf = [@version].pack('V')
|
|
207
|
+
buf << VarInt.encode(@inputs.length)
|
|
208
|
+
@inputs.each { |i| buf << i.to_binary }
|
|
209
|
+
buf << VarInt.encode(@outputs.length)
|
|
210
|
+
@outputs.each { |o| buf << o.to_binary }
|
|
211
|
+
buf << [@lock_time].pack('V')
|
|
212
|
+
buf.b.freeze
|
|
213
|
+
end
|
|
91
214
|
end
|
|
92
215
|
|
|
93
216
|
# Serialise the transaction to a hex string.
|
|
@@ -397,11 +520,14 @@ module BSV
|
|
|
397
520
|
# Used by BEEF, BUMPs, and merkle paths, which all work in wire byte order
|
|
398
521
|
# to match {TransactionInput#prev_wtxid}.
|
|
399
522
|
#
|
|
523
|
+
# @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
|
|
400
524
|
# @return [String] 32-byte transaction ID in wire byte order
|
|
401
525
|
def wtxid
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
526
|
+
@wtxid ||= begin
|
|
527
|
+
id = BSV::Primitives::Digest.sha256d(to_binary)
|
|
528
|
+
BSV.logger&.debug { "[Tx] wtxid computed (dtxid=#{id.reverse.unpack1('H*')})" }
|
|
529
|
+
id.freeze
|
|
530
|
+
end
|
|
405
531
|
end
|
|
406
532
|
|
|
407
533
|
# The transaction ID as a hex string (display byte order).
|
|
@@ -621,25 +747,103 @@ module BSV
|
|
|
621
747
|
# - +:output_overflow+ — Ruby raises; TS/Python return +false+; Go omits the check
|
|
622
748
|
# - +:script_failure+ — Ruby raises; TS/Python return +false+; Go also propagates errors
|
|
623
749
|
# - +:missing_source+ — Ruby raises; consistent with TS/Go/Python (all raise/error)
|
|
750
|
+
# - +verified:+ kwarg — Ruby-only; no equivalent seam exists in the TS, Go, or Python SDKs.
|
|
751
|
+
# This is a novel Ruby-side extension: a bidirectional wtxid-dedup Hash that the caller
|
|
752
|
+
# can seed (short-circuit ancestors already verified) and read after +verify+ returns
|
|
753
|
+
# (populate a persistent verification cache). See {file:docs/reference/verify-kwarg.md}.
|
|
754
|
+
# The reference SDKs repeat the full walk every time +verify+ is called.
|
|
624
755
|
#
|
|
625
756
|
# @param chain_tracker [Transaction::ChainTracker] chain tracker for merkle root validation
|
|
626
757
|
# @param fee_model [FeeModel, nil] optional fee model to validate the root transaction's fee
|
|
758
|
+
# @param verified [Hash{String => Boolean}, nil] bidirectional wtxid dedup Hash.
|
|
759
|
+
# Keys are 32-byte wire-order binary wtxids (not hex). Values are presence-only —
|
|
760
|
+
# the SDK writes +true+ for wtxids it walks and treats any truthy value as
|
|
761
|
+
# "already verified". A +false+ value is treated as absent (does not short-circuit)
|
|
762
|
+
# and will be overwritten with +true+ if the wtxid is subsequently walked.
|
|
763
|
+
# Callers use this three ways:
|
|
764
|
+
#
|
|
765
|
+
# - **Pre-seed** (short-circuit): pass a Hash pre-populated with wtxids you have
|
|
766
|
+
# verified in an earlier session. Their subtrees are skipped.
|
|
767
|
+
# - **Post-read** (accumulate): pass an empty Hash; after +verify+ returns, read the
|
|
768
|
+
# keys to see which wtxids the SDK walked. Populates a persistent verification cache.
|
|
769
|
+
# - **Bidirectional** (both): pass a Hash that seeds the walk AND collects the walked
|
|
770
|
+
# wtxids in one call.
|
|
771
|
+
#
|
|
772
|
+
# The Hash is mutated in place — the caller's object identity is preserved.
|
|
773
|
+
# +nil+ (default) means "no seed, no post-read" — the SDK uses an internal Hash the
|
|
774
|
+
# caller cannot access. Rejected at entry with +ArgumentError+: anything that is not
|
|
775
|
+
# +nil+ or a mutable +Hash+; a frozen +Hash+; or a +Hash+ with a truthy +default+
|
|
776
|
+
# value or any +default_proc+ (funds risk — missing keys would return truthy and
|
|
777
|
+
# silently short-circuit verification).
|
|
627
778
|
# @return [true] on successful verification
|
|
779
|
+
# @raise [ArgumentError] if +verified:+ is (a) not +nil+ or a +Hash+, (b) a frozen
|
|
780
|
+
# +Hash+ (the SDK writes walked wtxids into it and cannot mutate a frozen one), or
|
|
781
|
+
# (c) a +Hash+ with a truthy +default+ value or a +default_proc+ (missing keys
|
|
782
|
+
# would return truthy and silently short-circuit verification — funds risk)
|
|
628
783
|
# @raise [VerificationError] with code +:invalid_merkle_proof+ if a merkle proof is invalid
|
|
629
784
|
# @raise [VerificationError] with code +:insufficient_fee+ if the fee is below the model's threshold
|
|
630
785
|
# @raise [VerificationError] with code +:output_overflow+ if outputs exceed inputs
|
|
631
786
|
# @raise [VerificationError] with code +:script_failure+ if script execution fails
|
|
632
787
|
# @raise [VerificationError] with code +:missing_source+ if an input is missing required source data
|
|
633
|
-
|
|
634
|
-
|
|
788
|
+
# @note Security (trust contract): the caller warrants that every wtxid present in
|
|
789
|
+
# +verified:+ at method entry is fully verified — script AND chain-anchor (merkle proof)
|
|
790
|
+
# AND any consensus-flag context. A seeded wtxid is treated as fully verified regardless
|
|
791
|
+
# of what proof or script data it carries. The SDK does **not** re-run the merkle proof
|
|
792
|
+
# for a seeded ancestor. If the caller's cache is stale or compromised, +verify+ will
|
|
793
|
+
# return +true+ for ancestry that has not actually been verified. Cached wtxids are
|
|
794
|
+
# pinned to the consensus flags under which they were originally verified — invalidate
|
|
795
|
+
# the caller's cache when consensus flags change, on chain reorganisation, or on any
|
|
796
|
+
# event that could invalidate a prior verification. Fee validation is not affected by
|
|
797
|
+
# this set — the fee gate is a caller-passed policy, not a cached claim, and runs once
|
|
798
|
+
# at the start of every +verify+ call when +fee_model+ is given.
|
|
799
|
+
# @note Concurrency: the SDK writes to +verified:+ in place without locking. Passing the
|
|
800
|
+
# same Hash to concurrent +verify+ calls is a race — guard it externally, or give each
|
|
801
|
+
# thread its own Hash and merge the results afterwards. The class-level
|
|
802
|
+
# "Not thread-safe" note on {Tx} covers direct-mutation concerns; this is the
|
|
803
|
+
# specific footgun that comes with the in-place-mutation contract of this kwarg.
|
|
804
|
+
# @example Pre-seed short-circuit
|
|
805
|
+
# # wallet has already verified source_tx in a prior session
|
|
806
|
+
# cache = { source_tx.wtxid => true }
|
|
807
|
+
# tx.verify(chain_tracker: tracker, verified: cache)
|
|
808
|
+
# @example Post-read accumulate
|
|
809
|
+
# walked = {}
|
|
810
|
+
# tx.verify(chain_tracker: tracker, verified: walked)
|
|
811
|
+
# walked.keys # => [subject_wtxid, ancestor1_wtxid, ...]
|
|
812
|
+
# @example Bidirectional round-trip
|
|
813
|
+
# cache = load_persistent_verified_cache
|
|
814
|
+
# tx.verify(chain_tracker: tracker, verified: cache)
|
|
815
|
+
# save_persistent_verified_cache(cache) # includes newly-walked wtxids
|
|
816
|
+
# @since 0.11.0
|
|
817
|
+
def verify(chain_tracker:, fee_model: nil, verified: nil)
|
|
818
|
+
verified = normalise_verified(verified)
|
|
819
|
+
# Fee gate runs once at the start of every verify call. Fee is a caller-passed policy,
|
|
820
|
+
# not a cached script-validity claim — seeding the subject wtxid cannot silently disable it.
|
|
821
|
+
# verify_fee → total_input_satoshis raises ArgumentError when source data is missing;
|
|
822
|
+
# translate that to :missing_source VerificationError so callers see a consistent error
|
|
823
|
+
# taxonomy from #verify (all failure modes raise VerificationError, not ArgumentError).
|
|
824
|
+
if fee_model
|
|
825
|
+
begin
|
|
826
|
+
verify_fee(fee_model)
|
|
827
|
+
rescue ArgumentError => e
|
|
828
|
+
raise VerificationError.new(:missing_source,
|
|
829
|
+
"cannot compute fee: #{e.message}")
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
635
833
|
queue = [self]
|
|
636
834
|
|
|
637
835
|
until queue.empty?
|
|
638
836
|
tx = queue.shift
|
|
639
837
|
wtxid = tx.wtxid
|
|
640
|
-
|
|
838
|
+
# Top-of-loop dedup covers both walked-during-this-call and caller-seeded wtxids.
|
|
839
|
+
# A seeded wtxid short-circuits everything — including the merkle proof — because
|
|
840
|
+
# the caller warrants full trust. See @note Security.
|
|
841
|
+
# fetch(k, false) bypasses any custom Hash default / default_proc — critical, because
|
|
842
|
+
# Hash.new(true)[missing_key] would return true and short-circuit verification silently
|
|
843
|
+
# (funds risk). normalise_verified also rejects such Hashes at entry, but this is
|
|
844
|
+
# defence in depth.
|
|
845
|
+
next if verified.fetch(wtxid, false)
|
|
641
846
|
|
|
642
|
-
# Merkle path short-circuit: proven transaction needs no input verification
|
|
643
847
|
if tx.merkle_path
|
|
644
848
|
unless tx.merkle_path.verify(tx.txid_hex, chain_tracker)
|
|
645
849
|
raise VerificationError.new(:invalid_merkle_proof,
|
|
@@ -650,9 +854,6 @@ module BSV
|
|
|
650
854
|
next
|
|
651
855
|
end
|
|
652
856
|
|
|
653
|
-
# Fee validation (root transaction only)
|
|
654
|
-
verify_fee(fee_model) if tx.equal?(self) && fee_model
|
|
655
|
-
|
|
656
857
|
# Verify each input
|
|
657
858
|
tx.inputs.each_with_index do |input, index|
|
|
658
859
|
# Populate source data from source_transaction when not already set.
|
|
@@ -676,9 +877,10 @@ module BSV
|
|
|
676
877
|
)
|
|
677
878
|
end
|
|
678
879
|
|
|
679
|
-
# Enqueue source transaction
|
|
880
|
+
# Enqueue source transaction unless it's already dealt with (seeded or walked).
|
|
881
|
+
# fetch(k, false) bypasses any custom Hash default — see note above at top-of-loop dedup.
|
|
680
882
|
source_tx = input.source_transaction
|
|
681
|
-
queue << source_tx if source_tx && !verified
|
|
883
|
+
queue << source_tx if source_tx && !verified.fetch(source_tx.wtxid, false)
|
|
682
884
|
end
|
|
683
885
|
|
|
684
886
|
# Output ≤ input check
|
|
@@ -832,6 +1034,44 @@ module BSV
|
|
|
832
1034
|
"input #{idx} source_transaction has no output at index #{input.prev_tx_out_index}"
|
|
833
1035
|
end
|
|
834
1036
|
|
|
1037
|
+
# Validate the +verified:+ kwarg for {#verify}.
|
|
1038
|
+
#
|
|
1039
|
+
# Returns the caller's Hash directly (no dup, no freeze) so the SDK can mutate it in place —
|
|
1040
|
+
# the bidirectional contract from {#verify}. When +arg+ is +nil+, returns a fresh empty
|
|
1041
|
+
# Hash that the caller has no reference to; that Hash is discarded when +verify+ returns.
|
|
1042
|
+
#
|
|
1043
|
+
# @param arg [Hash, nil]
|
|
1044
|
+
# @return [Hash] the caller's Hash unchanged, or a fresh empty Hash if arg was nil
|
|
1045
|
+
# @raise [ArgumentError] if +arg+ is neither +nil+ nor a Hash, a frozen Hash (the SDK writes
|
|
1046
|
+
# walked wtxids into it and cannot mutate a frozen Hash), or a Hash with a truthy default
|
|
1047
|
+
# value or a default_proc (missing keys would return truthy and silently short-circuit
|
|
1048
|
+
# verification for un-seeded wtxids — funds risk)
|
|
1049
|
+
def normalise_verified(arg)
|
|
1050
|
+
return {} if arg.nil?
|
|
1051
|
+
|
|
1052
|
+
unless arg.is_a?(Hash)
|
|
1053
|
+
raise ArgumentError,
|
|
1054
|
+
"verified: must be nil or Hash of wtxid => truthy entries (got #{arg.class})"
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
if arg.frozen?
|
|
1058
|
+
raise ArgumentError,
|
|
1059
|
+
'verified: Hash must be mutable — the SDK writes walked wtxids into it'
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
# A Hash with a truthy default or a default_proc could return a truthy value for missing
|
|
1063
|
+
# keys, silently short-circuiting verification for un-seeded wtxids. The walk sites use
|
|
1064
|
+
# `fetch(k, false)` as defence in depth, but reject at entry so the caller sees a clear
|
|
1065
|
+
# error rather than an opaquely-behaving verify.
|
|
1066
|
+
if arg.default_proc || arg.default
|
|
1067
|
+
raise ArgumentError,
|
|
1068
|
+
'verified: Hash must not have a truthy default or a default_proc — ' \
|
|
1069
|
+
'missing keys must return falsy so verification cannot be silently skipped'
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
arg
|
|
1073
|
+
end
|
|
1074
|
+
|
|
835
1075
|
def verify_input_requirements(tx, input, index)
|
|
836
1076
|
dtxid_hex = tx.txid_hex
|
|
837
1077
|
if input.unlocking_script.nil?
|
|
@@ -866,37 +1106,59 @@ module BSV
|
|
|
866
1106
|
"outputs (#{output_total}) exceed inputs (#{input_total}) for transaction #{tx.txid_hex}")
|
|
867
1107
|
end
|
|
868
1108
|
|
|
869
|
-
ZERO_HASH = "\x00".b * 32
|
|
1109
|
+
ZERO_HASH = ("\x00".b * 32).freeze # rubocop:disable Style/RedundantFreeze
|
|
870
1110
|
private_constant :ZERO_HASH
|
|
871
1111
|
|
|
1112
|
+
# @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
|
|
872
1113
|
def hash_prevouts(anyone_can_pay)
|
|
873
1114
|
return ZERO_HASH if anyone_can_pay
|
|
874
1115
|
|
|
875
|
-
|
|
876
|
-
|
|
1116
|
+
@hash_prevouts ||=
|
|
1117
|
+
BSV::Primitives::Digest.sha256d(@inputs.map(&:outpoint_binary).join).freeze
|
|
877
1118
|
end
|
|
878
1119
|
|
|
1120
|
+
# @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
|
|
879
1121
|
def hash_sequence(anyone_can_pay, base_type)
|
|
880
1122
|
return ZERO_HASH if anyone_can_pay || base_type == Sighash::SINGLE || base_type == Sighash::NONE
|
|
881
1123
|
|
|
882
|
-
|
|
883
|
-
|
|
1124
|
+
@hash_sequence ||=
|
|
1125
|
+
BSV::Primitives::Digest.sha256d(@inputs.map { |i| [i.sequence].pack('V') }.join).freeze
|
|
884
1126
|
end
|
|
885
1127
|
|
|
1128
|
+
# @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
|
|
886
1129
|
def hash_outputs(base_type, input_index)
|
|
887
1130
|
case base_type
|
|
888
1131
|
when Sighash::NONE
|
|
889
1132
|
ZERO_HASH
|
|
890
1133
|
when Sighash::SINGLE
|
|
1134
|
+
# NEVER cache the ZERO_HASH branch — after add_output, an idx that was
|
|
1135
|
+
# out-of-range may move in-range; a cached ZERO_HASH would be stale.
|
|
891
1136
|
return ZERO_HASH if input_index >= @outputs.length
|
|
892
1137
|
|
|
893
|
-
|
|
1138
|
+
hash_outputs_single(input_index)
|
|
894
1139
|
else # ALL (and any non-standard base type per BIP-143)
|
|
895
|
-
|
|
896
|
-
BSV::Primitives::Digest.sha256d(buf)
|
|
1140
|
+
hash_outputs_all
|
|
897
1141
|
end
|
|
898
1142
|
end
|
|
899
1143
|
|
|
1144
|
+
# Memoises the +hash_outputs+ ALL branch (sha256d of all outputs joined).
|
|
1145
|
+
#
|
|
1146
|
+
# @!visibility private
|
|
1147
|
+
def hash_outputs_all
|
|
1148
|
+
@hash_outputs_all ||= BSV::Primitives::Digest.sha256d(@outputs.map(&:to_binary).join).freeze
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
# Memoises +hash_outputs+ for the SIGHASH_SINGLE branch, keyed by input
|
|
1152
|
+
# index. The Hash itself is the ivar; per-key lookup uses +Hash#[]= ||=+
|
|
1153
|
+
# which does not trigger +Naming/MemoizedInstanceVariableName+.
|
|
1154
|
+
#
|
|
1155
|
+
# @!visibility private
|
|
1156
|
+
def hash_outputs_single(input_index)
|
|
1157
|
+
@hash_outputs_single ||= {}
|
|
1158
|
+
@hash_outputs_single[input_index] ||=
|
|
1159
|
+
BSV::Primitives::Digest.sha256d(@outputs[input_index].to_binary).freeze
|
|
1160
|
+
end
|
|
1161
|
+
|
|
900
1162
|
# Collect this transaction and all its ancestors in dependency order
|
|
901
1163
|
# (ancestors first, self last). Stops recursion at transactions with
|
|
902
1164
|
# a merkle_path (proven leaves). Deduplicates by wtxid (wire-order bytes).
|
|
@@ -1066,6 +1328,75 @@ module BSV
|
|
|
1066
1328
|
scale_factor = LOG10_RECIPROCAL_D_VALUES_1TO9[Random.rand(9)] # Array indexing starts at 0
|
|
1067
1329
|
(min + (scale_factor * (max - min))).floor
|
|
1068
1330
|
end
|
|
1331
|
+
|
|
1332
|
+
# Clears the +hash_sequence+ component-hash memo. Called by
|
|
1333
|
+
# +TransactionInput#sequence=+ via the owning-Tx backref.
|
|
1334
|
+
#
|
|
1335
|
+
# @!visibility private
|
|
1336
|
+
def invalidate_sequence_components_cache
|
|
1337
|
+
@hash_sequence = nil
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
# Clears the +hash_outputs+ component-hash memos (both the ALL branch and
|
|
1341
|
+
# the SIGHASH_SINGLE per-index cache). Called by
|
|
1342
|
+
# +TransactionOutput#satoshis=+ and +TransactionOutput#locking_script=+
|
|
1343
|
+
# via the owning-Tx backref.
|
|
1344
|
+
#
|
|
1345
|
+
# @!visibility private
|
|
1346
|
+
def invalidate_outputs_components_cache
|
|
1347
|
+
@hash_outputs_all = nil
|
|
1348
|
+
@hash_outputs_single&.clear
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
# Clears the L2 wire-serialisation caches (+to_binary+ and +wtxid+).
|
|
1352
|
+
# Called by per-struct setter bubbles and +invalidate_caches+.
|
|
1353
|
+
#
|
|
1354
|
+
# @!visibility private
|
|
1355
|
+
def invalidate_wire_cache
|
|
1356
|
+
@to_binary = nil
|
|
1357
|
+
@wtxid = nil
|
|
1358
|
+
end
|
|
1359
|
+
|
|
1360
|
+
# Clears every cache slice on this Tx without the +rebind_owning_tx!+
|
|
1361
|
+
# walk. Called from +add_input+ / +add_output+ where the new struct's
|
|
1362
|
+
# backref was just set inline and existing structs' backrefs were
|
|
1363
|
+
# established when they were added — making the rebind walk redundant
|
|
1364
|
+
# and reducing N-input construction from O(N²) to O(N).
|
|
1365
|
+
#
|
|
1366
|
+
# The public +invalidate_caches+ keeps the rebind walk because it is
|
|
1367
|
+
# the documented escape hatch for direct array mutation, where the
|
|
1368
|
+
# walk is required.
|
|
1369
|
+
#
|
|
1370
|
+
# @!visibility private
|
|
1371
|
+
def clear_caches
|
|
1372
|
+
@hash_prevouts = nil
|
|
1373
|
+
invalidate_sequence_components_cache
|
|
1374
|
+
invalidate_outputs_components_cache
|
|
1375
|
+
invalidate_wire_cache
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
# Restore +@owning_tx+ on every current input and output. Called by
|
|
1379
|
+
# +invalidate_caches+ so the escape hatch covers direct array mutation
|
|
1380
|
+
# (e.g. +tx.inputs << input+ bypasses +add_input+ and leaves the new
|
|
1381
|
+
# struct's backref nil — subsequent setter mutations would not bubble
|
|
1382
|
+
# invalidation).
|
|
1383
|
+
#
|
|
1384
|
+
# Raises if any struct is currently attached to a *different* Tx —
|
|
1385
|
+
# matches the +add_input+ / +add_output+ cross-Tx rebind contract.
|
|
1386
|
+
#
|
|
1387
|
+
# @!visibility private
|
|
1388
|
+
def rebind_owning_tx!
|
|
1389
|
+
(@inputs + @outputs).each do |struct|
|
|
1390
|
+
existing_owner = struct.instance_variable_get(:@owning_tx)
|
|
1391
|
+
if existing_owner && !existing_owner.equal?(self)
|
|
1392
|
+
raise ArgumentError,
|
|
1393
|
+
"#{struct.class.name.split('::').last} is already attached to a different Tx; " \
|
|
1394
|
+
'invalidate_caches cannot rebind across transactions'
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
struct.instance_variable_set(:@owning_tx, self)
|
|
1398
|
+
end
|
|
1399
|
+
end
|
|
1069
1400
|
end
|
|
1070
1401
|
end
|
|
1071
1402
|
end
|
data/lib/bsv/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsv-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
@@ -175,7 +175,6 @@ files:
|
|
|
175
175
|
- lib/bsv/script/opcodes.rb
|
|
176
176
|
- lib/bsv/script/push_drop_template.rb
|
|
177
177
|
- lib/bsv/script/script.rb
|
|
178
|
-
- lib/bsv/secp256k1_native.bundle
|
|
179
178
|
- lib/bsv/storage.rb
|
|
180
179
|
- lib/bsv/storage/downloader.rb
|
|
181
180
|
- lib/bsv/storage/errors.rb
|
|
Binary file
|