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.
@@ -42,12 +42,21 @@ module BSV
42
42
  @else_stack.push(false)
43
43
  end
44
44
 
45
- # OP_ELSE: toggle conditional branch (only one ELSE per IF after genesis)
45
+ # OP_ELSE: toggle the current conditional branch.
46
+ #
47
+ # Multi-ELSE handling matches the TS SDK (Spend.ts OP_ELSE case):
48
+ # - In relaxed mode (no explicit flags), each ELSE toggles the branch —
49
+ # any number of ELSEs per IF is valid. This is the script-021 case.
50
+ # - With explicit post-genesis flags, only one ELSE per IF is permitted;
51
+ # a second ELSE raises UNBALANCED_CONDITIONAL. Vectors such as
52
+ # node.script.bitcoin-sv.0731 and teranode.0057 enforce this.
46
53
  def op_else
47
54
  raise ScriptError.new(ScriptErrorCode::UNBALANCED_CONDITIONAL, 'OP_ELSE without matching OP_IF') if @cond_stack.empty?
48
55
 
49
- # After genesis: only one ELSE per IF
50
- raise ScriptError.new(ScriptErrorCode::UNBALANCED_CONDITIONAL, 'duplicate OP_ELSE') if @else_stack.pop
56
+ already_seen_else = @else_stack.pop
57
+ if already_seen_else && explicit_flags? && after_genesis?
58
+ raise ScriptError.new(ScriptErrorCode::UNBALANCED_CONDITIONAL, 'duplicate OP_ELSE')
59
+ end
51
60
 
52
61
  case @cond_stack.last
53
62
  when :true then @cond_stack[-1] = :false
@@ -97,20 +106,23 @@ module BSV
97
106
 
98
107
  # OP_VER: push 4-byte little-endian transaction version onto the stack.
99
108
  #
100
- # Requires a transaction context (@tx must not be nil). Raises
101
- # MISSING_TX_CONTEXT when called from Interpreter.evaluate (no-tx path).
109
+ # Resolves the version from +@tx.version+ (when verifying a
110
+ # transaction) or from the +tx_version:+ parameter passed to
111
+ # +Interpreter.evaluate+. Raises MISSING_TX_CONTEXT only if neither
112
+ # is available.
102
113
  #
103
114
  # The version is always encoded as exactly 4 bytes (LE uint32), never as
104
115
  # a ScriptNumber. Version 1 → 01000000, version 2 → 02000000.
105
116
  def op_ver
106
- if @tx.nil?
117
+ bytes = tx_version_bytes
118
+ if bytes.nil?
107
119
  raise ScriptError.new(
108
120
  ScriptErrorCode::MISSING_TX_CONTEXT,
109
121
  'OP_VER requires transaction context'
110
122
  )
111
123
  end
112
124
 
113
- @dstack.push_bytes(tx_version_bytes)
125
+ @dstack.push_bytes(bytes)
114
126
  end
115
127
 
116
128
  # OP_VERIF: version-conditional branch — executes like OP_IF but compares
@@ -166,20 +178,25 @@ module BSV
166
178
  end
167
179
 
168
180
  # Returns the transaction version as a 4-byte little-endian binary
169
- # string. Used by OP_VER, OP_VERIF, and OP_VERNOTIF.
181
+ # string, or +nil+ if no version is available. Used by OP_VER,
182
+ # OP_VERIF, and OP_VERNOTIF. Prefers +@tx.version+ when verifying a
183
+ # transaction; falls back to the +tx_version:+ argument used by
184
+ # evaluate-without-tx (e.g. conformance corpus runs).
170
185
  def tx_version_bytes
171
- [@tx.version].pack('V')
186
+ v = @tx&.version || @tx_version
187
+ v.nil? ? nil : [v].pack('V')
172
188
  end
173
189
 
174
190
  # Compares raw bytes against the current transaction version.
175
191
  # Returns false if:
176
192
  # - +data+ is not exactly 4 bytes
177
- # - @tx is nil (no transaction context)
178
- # - the bytes do not match the 4-byte LE encoding of @tx.version
193
+ # - no version is available (neither @tx nor @tx_version set)
194
+ # - the bytes do not match the 4-byte LE encoding of the version
179
195
  def tx_version_matches?(data)
180
- return false if @tx.nil? || data.bytesize != 4
196
+ return false if data.bytesize != 4
181
197
 
182
- data == tx_version_bytes
198
+ bytes = tx_version_bytes
199
+ !bytes.nil? && data == bytes
183
200
  end
184
201
  end
185
202
  end
@@ -22,12 +22,20 @@ module BSV
22
22
  # script.p2pkh? #=> true
23
23
  # script.to_asm #=> "OP_DUP OP_HASH160 ... OP_EQUALVERIFY OP_CHECKSIG"
24
24
  class Script
25
- # @return [String] the raw script bytes
25
+ # @return [String] the raw script bytes (frozen, binary encoding)
26
+ # @note Scripts are protocol-level immutable; the underlying bytes are
27
+ # defensively copied + frozen on construction. Downstream callers
28
+ # that need to build a script incrementally must compose bytes
29
+ # into a new String and pass it to a fresh +Script.new+.
26
30
  attr_reader :bytes
27
31
 
28
32
  # @param bytes [String] raw script bytes (default: empty)
29
33
  def initialize(bytes = ''.b)
30
- @bytes = bytes.b
34
+ # Defensive copy + freeze: scripts are immutable in the protocol; freezing
35
+ # @bytes prevents external in-place mutation from staling caches that
36
+ # depend on the script's serialisation (see
37
+ # docs/reference/sighash-cache.md).
38
+ @bytes = bytes.b.dup.freeze
31
39
  @chunks = nil
32
40
  end
33
41
 
@@ -460,6 +468,17 @@ module BSV
460
468
  b.getbyte(22) == Opcodes::OP_EQUAL
461
469
  end
462
470
 
471
+ # Whether the script consists entirely of push-data operations.
472
+ #
473
+ # A script is push-only iff every chunk's opcode is at most +OP_16+
474
+ # (0x60). Used by the script interpreter to enforce the +SIGPUSHONLY+
475
+ # verification flag on unlock scripts.
476
+ #
477
+ # @return [Boolean]
478
+ def push_only?
479
+ chunks.all? { |chunk| chunk.opcode <= Opcodes::OP_16 }
480
+ end
481
+
463
482
  # Whether this is an OP_RETURN data carrier script.
464
483
  #
465
484
  # Matches both +OP_RETURN ...+ and +OP_FALSE OP_RETURN ...+ forms.
@@ -145,7 +145,7 @@ module BSV
145
145
  body = response.body.to_s
146
146
  return nil if body.empty?
147
147
 
148
- actual_hash = OpenSSL::Digest::SHA256.digest(body)
148
+ actual_hash = BSV::Primitives::Digest.sha256(body)
149
149
  return nil unless actual_hash == expected_hash
150
150
 
151
151
  DownloadResult.new(data: body, mime_type: response['Content-Type'])
@@ -38,7 +38,7 @@ module BSV
38
38
  # @param data [String] binary file content
39
39
  # @return [String] Base58Check-encoded UHRP URL
40
40
  def get_url_for_file(data)
41
- hash = OpenSSL::Digest::SHA256.digest(data.b)
41
+ hash = BSV::Primitives::Digest.sha256(data.b)
42
42
  get_url_for_hash(hash)
43
43
  end
44
44
 
@@ -14,19 +14,42 @@ module BSV
14
14
  # @return [Integer] index of the output within the previous transaction
15
15
  attr_reader :prev_tx_out_index
16
16
 
17
- # @return [Integer] sequence number (default: 0xFFFFFFFF)
18
- attr_accessor :sequence
19
-
20
- # @return [Script::Script, nil] the unlocking script (set after signing)
21
- attr_accessor :unlocking_script
17
+ # @!attribute [rw] sequence
18
+ # @return [Integer] sequence number (default: 0xFFFFFFFF)
19
+ # @note Setting this invalidates the owning Tx's wire cache and the
20
+ # hash_sequence component of the sighash cache. See
21
+ # {file:docs/reference/sighash-cache.md}.
22
+ attr_reader :sequence
23
+
24
+ # @!attribute [rw] unlocking_script
25
+ # @return [Script::Script, nil] the unlocking script (set after signing)
26
+ # @note Setting this invalidates the owning Tx's wire cache only.
27
+ # The unlocking script does not enter the BIP-143 preimage, so the
28
+ # sighash component caches are not touched. See
29
+ # {file:docs/reference/sighash-cache.md}.
30
+ attr_reader :unlocking_script
22
31
 
23
32
  # @return [Integer, nil] satoshi value of the source output (needed for sighash)
33
+ # @note Enters the BIP-143 preimage at step 6 but no current cache layer
34
+ # memoises the per-input preimage, so no invalidator is required. If a
35
+ # future change adds preimage or per-input digest memoisation, add a
36
+ # setter override here that invalidates the relevant cache slice. See
37
+ # {file:docs/reference/sighash-cache.md}.
24
38
  attr_accessor :source_satoshis
25
39
 
26
40
  # @return [Script::Script, nil] locking script of the source output (needed for sighash)
41
+ # @note Enters the BIP-143 preimage as scriptCode (step 5) when no
42
+ # subscript override is supplied. Same caveat as {#source_satoshis}:
43
+ # no current cache layer depends on this field, but a future preimage
44
+ # cache would need a setter override here. See
45
+ # {file:docs/reference/sighash-cache.md}.
27
46
  attr_accessor :source_locking_script
28
47
 
29
48
  # @return [Transaction::Tx, nil] the full source transaction (for BEEF wiring)
49
+ # @note Source data is lazily resolved from this Tx during {Tx#verify}
50
+ # and {Tx#sighash_preimage}. Mutation of this field after resolution
51
+ # has occurred has no effect on caches because no current cache layer
52
+ # depends on resolved source data.
30
53
  attr_accessor :source_transaction
31
54
 
32
55
  # @return [UnlockingScriptTemplate, nil] template for deferred signing
@@ -38,23 +61,60 @@ module BSV
38
61
  # @param sequence [Integer] sequence number
39
62
  def initialize(prev_wtxid:, prev_tx_out_index:, unlocking_script: nil, sequence: 0xFFFFFFFF)
40
63
  BSV::Primitives::Hex.validate_wtxid!(prev_wtxid, name: 'prev_wtxid')
41
- @prev_wtxid = prev_wtxid.b
64
+ # Defensively copy + freeze so external mutation of the caller's String
65
+ # cannot stale the cached outpoint_binary / to_binary.
66
+ @prev_wtxid = prev_wtxid.b.dup.freeze
42
67
  @prev_tx_out_index = prev_tx_out_index
43
68
  @unlocking_script = unlocking_script
44
69
  @sequence = sequence
70
+ @owning_tx = nil
45
71
  BSV.logger&.debug { "[TransactionInput] prev_wtxid set: #{dtxid_hex}:#{@prev_tx_out_index}" }
46
72
  end
47
73
 
74
+ # Called by +#dup+ and +#clone+. Clears the owning-Tx backref so that the
75
+ # cloned input does not belong to any transaction until it is explicitly
76
+ # added via +Tx#add_input+.
77
+ def initialize_copy(other)
78
+ super
79
+ @owning_tx = nil
80
+ end
81
+
82
+ # Sets the sequence number and invalidates the L1 binary memo and any
83
+ # owning-Tx slice caches that incorporate sequence (BIP-143 preimage
84
+ # and wire format).
85
+ #
86
+ # @param value [Integer] new sequence number
87
+ def sequence=(value)
88
+ @sequence = value
89
+ @to_binary = nil
90
+ @owning_tx&.send(:invalidate_sequence_components_cache)
91
+ @owning_tx&.send(:invalidate_wire_cache)
92
+ end
93
+
94
+ # Sets the unlocking script and invalidates the L1 binary memo and the
95
+ # owning-Tx wire cache. Unlocking script does not enter the BIP-143
96
+ # preimage, so the sequence/outputs components caches are not touched.
97
+ #
98
+ # @param value [Script::Script, nil] new unlocking script
99
+ def unlocking_script=(value)
100
+ @unlocking_script = value
101
+ @to_binary = nil
102
+ @owning_tx&.send(:invalidate_wire_cache)
103
+ end
104
+
48
105
  # Serialise the input to its binary wire format.
49
106
  #
107
+ # @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
50
108
  # @return [String] binary input (outpoint + varint + script + sequence)
51
109
  def to_binary
52
- script_bytes = @unlocking_script ? @unlocking_script.to_binary : ''.b
53
- @prev_wtxid +
54
- [@prev_tx_out_index].pack('V') +
55
- VarInt.encode(script_bytes.bytesize) +
56
- script_bytes +
57
- [@sequence].pack('V')
110
+ @to_binary ||= begin
111
+ script_bytes = @unlocking_script ? @unlocking_script.to_binary : ''.b
112
+ (@prev_wtxid +
113
+ [@prev_tx_out_index].pack('V') +
114
+ VarInt.encode(script_bytes.bytesize) +
115
+ script_bytes +
116
+ [@sequence].pack('V')).freeze
117
+ end
58
118
  end
59
119
 
60
120
  # Deserialise a transaction input from binary data.
@@ -113,9 +173,13 @@ module BSV
113
173
 
114
174
  # Serialise the outpoint (prev_wtxid + output index) as binary.
115
175
  #
176
+ # Memoised: outpoint components are +attr_reader+ only so the value is
177
+ # immutable after construction. Returns a frozen binary string.
178
+ #
179
+ # @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
116
180
  # @return [String] 36-byte outpoint
117
181
  def outpoint_binary
118
- @prev_wtxid + [@prev_tx_out_index].pack('V')
182
+ @outpoint_binary ||= (@prev_wtxid + [@prev_tx_out_index].pack('V')).freeze
119
183
  end
120
184
 
121
185
  # The previous transaction ID in display-order hex.
@@ -8,11 +8,17 @@ module BSV
8
8
  # Outputs are consumed by transaction inputs that provide matching
9
9
  # unlocking scripts.
10
10
  class TransactionOutput
11
- # @return [Integer] the output value in satoshis
12
- attr_accessor :satoshis
11
+ # @!attribute [rw] satoshis
12
+ # @return [Integer] the output value in satoshis
13
+ # @note Setting this invalidates the owning Tx's outputs-components
14
+ # and wire caches. See {file:docs/reference/sighash-cache.md}.
15
+ attr_reader :satoshis
13
16
 
14
- # @return [Script::Script] the locking script (spending conditions)
15
- attr_accessor :locking_script
17
+ # @!attribute [rw] locking_script
18
+ # @return [Script::Script] the locking script (spending conditions)
19
+ # @note Setting this invalidates the owning Tx's outputs-components
20
+ # and wire caches. See {file:docs/reference/sighash-cache.md}.
21
+ attr_reader :locking_script
16
22
 
17
23
  # @return [Boolean] whether this output receives change
18
24
  attr_accessor :change
@@ -24,14 +30,48 @@ module BSV
24
30
  @satoshis = satoshis
25
31
  @locking_script = locking_script
26
32
  @change = change
33
+ @owning_tx = nil
34
+ end
35
+
36
+ # Called by +#dup+ and +#clone+. Clears the owning-Tx backref so that the
37
+ # cloned output does not belong to any transaction until it is explicitly
38
+ # added via +Tx#add_output+.
39
+ def initialize_copy(other)
40
+ super
41
+ @owning_tx = nil
42
+ end
43
+
44
+ # Sets the satoshi value and invalidates the L1 binary memo and the
45
+ # owning-Tx outputs-components and wire caches.
46
+ #
47
+ # @param value [Integer] new satoshi value
48
+ def satoshis=(value)
49
+ @satoshis = value
50
+ @to_binary = nil
51
+ @owning_tx&.send(:invalidate_outputs_components_cache)
52
+ @owning_tx&.send(:invalidate_wire_cache)
53
+ end
54
+
55
+ # Sets the locking script and invalidates the L1 binary memo and the
56
+ # owning-Tx outputs-components and wire caches.
57
+ #
58
+ # @param value [Script::Script] new locking script
59
+ def locking_script=(value)
60
+ @locking_script = value
61
+ @to_binary = nil
62
+ @owning_tx&.send(:invalidate_outputs_components_cache)
63
+ @owning_tx&.send(:invalidate_wire_cache)
27
64
  end
28
65
 
29
66
  # Serialise the output to its binary wire format.
30
67
  #
68
+ # @note Memoised; see {file:docs/reference/sighash-cache.md} for the invalidation contract.
31
69
  # @return [String] binary output (8-byte LE satoshis + varint + script)
32
70
  def to_binary
33
- script_bytes = @locking_script.to_binary
34
- [satoshis].pack('Q<') + VarInt.encode(script_bytes.bytesize) + script_bytes
71
+ @to_binary ||= begin
72
+ script_bytes = @locking_script.to_binary
73
+ ([@satoshis].pack('Q<') + VarInt.encode(script_bytes.bytesize) + script_bytes).freeze
74
+ end
35
75
  end
36
76
 
37
77
  # Deserialise a transaction output from binary data.