bsv-sdk 0.24.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 +62 -0
- data/README.md +4 -2
- 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/helpers.rb +3 -3
- 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.rb +1 -0
- data/lib/bsv/primitives/digest.rb +107 -13
- data/lib/bsv/primitives/ecies.rb +12 -3
- data/lib/bsv/registry/client.rb +43 -1
- data/lib/bsv/script/bip276.rb +143 -0
- 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/push_drop_template.rb +2 -2
- data/lib/bsv/script/script.rb +21 -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 +168 -14
- data/lib/bsv/transaction/beef_party.rb +119 -0
- data/lib/bsv/transaction/chain_tracker.rb +1 -1
- 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 +1 -1
- data/lib/bsv/transaction/p2pkh.rb +1 -1
- data/lib/bsv/transaction/transaction_input.rb +78 -14
- data/lib/bsv/transaction/transaction_output.rb +46 -6
- data/lib/bsv/transaction/tx.rb +370 -39
- data/lib/bsv/transaction/unlocking_script_template.rb +2 -2
- data/lib/bsv/transaction.rb +1 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv-sdk.rb +2 -0
- metadata +13 -2
- data/lib/bsv/secp256k1_native.bundle +0 -0
|
@@ -53,18 +53,74 @@ module BSV
|
|
|
53
53
|
# stack overflow from deeply nested conditionals.
|
|
54
54
|
MAX_CONDITIONAL_DEPTH = 256
|
|
55
55
|
|
|
56
|
+
# Opcodes that require Chronicle to execute. With explicit flags but
|
|
57
|
+
# without UTXO_AFTER_CHRONICLE, executing any of these raises
|
|
58
|
+
# DISABLED_OPCODE. OP_VER / OP_VERIF / OP_VERNOTIF are included because
|
|
59
|
+
# pre-Chronicle they're either reserved (OP_VER) or behave as a
|
|
60
|
+
# conditional-only NOP in non-executing branches — execution itself is
|
|
61
|
+
# disabled. Mirrors TS Spend.ts (lines ~694-709) and Go IsDisabled.
|
|
62
|
+
CHRONICLE_ONLY_OPCODES = [
|
|
63
|
+
Opcodes::OP_2MUL, Opcodes::OP_2DIV,
|
|
64
|
+
Opcodes::OP_VER, Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF
|
|
65
|
+
].freeze
|
|
66
|
+
|
|
67
|
+
# The two version-conditional opcodes — extracted to module-level
|
|
68
|
+
# constants so the interpreter hot path (every conditional dispatch /
|
|
69
|
+
# every executed chunk) doesn't allocate a fresh Array per call.
|
|
70
|
+
VER_CONDITIONAL_OPCODES = [Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF].freeze
|
|
71
|
+
|
|
72
|
+
# Recognised verification flag names. Catches typos (a misspelled
|
|
73
|
+
# +SIGPUSHONLLY+ would otherwise silently disable enforcement) and forces
|
|
74
|
+
# any new flag to be declared here before use. The set is the union of
|
|
75
|
+
# flags appearing in the canonical conformance corpus and the Bitcoin
|
|
76
|
+
# Core +script_tests.json+ fixture, plus the witness/taproot family that
|
|
77
|
+
# downstream callers filter out before reaching the interpreter.
|
|
78
|
+
KNOWN_FLAGS = Set[
|
|
79
|
+
'CHECKLOCKTIMEVERIFY', 'CHECKSEQUENCEVERIFY',
|
|
80
|
+
'CLEANSTACK', 'DERSIG', 'DISCOURAGE_UPGRADABLE_NOPS',
|
|
81
|
+
'DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM',
|
|
82
|
+
'GENESIS', 'LOW_S',
|
|
83
|
+
'MINIMALDATA', 'MINIMALIF',
|
|
84
|
+
'NULLDUMMY', 'NULLFAIL',
|
|
85
|
+
'P2SH', 'SIGHASH_FORKID', 'SIGPUSHONLY', 'STRICTENC',
|
|
86
|
+
'TAPROOT',
|
|
87
|
+
'UTXO_AFTER_CHRONICLE', 'UTXO_AFTER_GENESIS',
|
|
88
|
+
'WITNESS'
|
|
89
|
+
].freeze
|
|
90
|
+
|
|
56
91
|
# Evaluate unlock + lock scripts without transaction context.
|
|
57
92
|
#
|
|
58
93
|
# Signature operations will always fail (no sighash available).
|
|
59
94
|
#
|
|
95
|
+
# @example Relaxed mode (post-Chronicle defaults, no malleability checks)
|
|
96
|
+
# Interpreter.evaluate(unlock, lock) # flags defaults to nil
|
|
97
|
+
#
|
|
98
|
+
# @example Explicit flag set
|
|
99
|
+
# Interpreter.evaluate(unlock, lock, flags: %w[UTXO_AFTER_GENESIS CLEANSTACK])
|
|
100
|
+
#
|
|
101
|
+
# @example Explicit-but-empty (pre-Genesis, pre-Chronicle, strict)
|
|
102
|
+
# Interpreter.evaluate(unlock, lock, flags: []) # NOT the same as nil
|
|
103
|
+
#
|
|
60
104
|
# @param unlock_script [Script] the unlocking script
|
|
61
105
|
# @param lock_script [Script] the locking script
|
|
106
|
+
# @param flags [Array<String>, Set<String>, nil] explicit verification flags
|
|
107
|
+
# (e.g. +SIGPUSHONLY+, +CLEANSTACK+, +UTXO_AFTER_CHRONICLE+). +nil+
|
|
108
|
+
# selects relaxed (post-Chronicle) mode where malleability checks are
|
|
109
|
+
# off; an empty +[]+ array is explicit-but-empty (pre-Genesis,
|
|
110
|
+
# pre-Chronicle strict mode). Each flag string must appear in
|
|
111
|
+
# {KNOWN_FLAGS} — unknown names raise +ArgumentError+.
|
|
112
|
+
# @param tx_version [Integer, nil] transaction version made available to
|
|
113
|
+
# +OP_VER+/+OP_VERIF+/+OP_VERNOTIF+ when no transaction is supplied
|
|
62
114
|
# @return [Boolean] +true+ if execution succeeds
|
|
63
115
|
# @raise [ScriptError] if script execution fails
|
|
64
|
-
|
|
116
|
+
# @raise [ArgumentError] if +flags+ contains an unknown name or
|
|
117
|
+
# +tx_version+ is not a valid uint32
|
|
118
|
+
def self.evaluate(unlock_script, lock_script, flags: nil, tx_version: nil)
|
|
65
119
|
new(
|
|
66
120
|
unlock_script: unlock_script,
|
|
67
|
-
lock_script: lock_script
|
|
121
|
+
lock_script: lock_script,
|
|
122
|
+
flags: flags,
|
|
123
|
+
tx_version: tx_version
|
|
68
124
|
).execute
|
|
69
125
|
end
|
|
70
126
|
|
|
@@ -75,19 +131,28 @@ module BSV
|
|
|
75
131
|
# @param unlock_script [Script] the input's unlocking script
|
|
76
132
|
# @param lock_script [Script] the previous output's locking script
|
|
77
133
|
# @param satoshis [Integer] the value of the previous output in satoshis
|
|
134
|
+
# @param flags [Array<String>, Set<String>, nil] explicit verification flags
|
|
135
|
+
# (see {.evaluate}). Production callers (e.g. +Tx#verify_input+) do not
|
|
136
|
+
# pass +flags+, so mainnet transaction validation always runs in
|
|
137
|
+
# relaxed mode; the parameter exists for conformance-corpus and
|
|
138
|
+
# regression-vector runners that need to drive specific flag combinations.
|
|
78
139
|
# @return [Boolean] +true+ if verification succeeds
|
|
79
140
|
# @raise [ScriptError] if script execution fails
|
|
80
|
-
|
|
141
|
+
# @raise [ArgumentError] if +flags+ contains an unknown name
|
|
142
|
+
def self.verify(tx:, input_index:, unlock_script:, lock_script:, satoshis:, flags: nil)
|
|
81
143
|
new(
|
|
82
144
|
unlock_script: unlock_script,
|
|
83
145
|
lock_script: lock_script,
|
|
84
146
|
tx: tx,
|
|
85
147
|
input_index: input_index,
|
|
86
|
-
satoshis: satoshis
|
|
148
|
+
satoshis: satoshis,
|
|
149
|
+
flags: flags
|
|
87
150
|
).execute
|
|
88
151
|
end
|
|
89
152
|
|
|
90
153
|
def execute
|
|
154
|
+
enforce_sig_pushonly
|
|
155
|
+
|
|
91
156
|
scripts = [@unlock_script, @lock_script]
|
|
92
157
|
script_names = %w[unlock_script lock_script]
|
|
93
158
|
|
|
@@ -123,12 +188,15 @@ module BSV
|
|
|
123
188
|
|
|
124
189
|
private
|
|
125
190
|
|
|
126
|
-
def initialize(unlock_script:, lock_script:, tx: nil, input_index: nil, satoshis: nil
|
|
191
|
+
def initialize(unlock_script:, lock_script:, tx: nil, input_index: nil, satoshis: nil,
|
|
192
|
+
flags: nil, tx_version: nil)
|
|
127
193
|
@unlock_script = unlock_script
|
|
128
194
|
@lock_script = lock_script
|
|
129
195
|
@tx = tx
|
|
130
196
|
@input_index = input_index
|
|
131
197
|
@satoshis = satoshis
|
|
198
|
+
@flags = normalise_flags(flags)
|
|
199
|
+
@tx_version = validate_tx_version(tx_version)
|
|
132
200
|
|
|
133
201
|
@dstack = Stack.new
|
|
134
202
|
@astack = Stack.new
|
|
@@ -141,24 +209,137 @@ module BSV
|
|
|
141
209
|
@current_chunk_idx = 0
|
|
142
210
|
end
|
|
143
211
|
|
|
212
|
+
# Normalises the +flags:+ kwarg into a frozen Set of recognised names.
|
|
213
|
+
# +nil+ stays +nil+ (relaxed mode); any iterable is coerced to a Set of
|
|
214
|
+
# strings, dropping +nil+ / empty entries silently (so callers can pass
|
|
215
|
+
# +flags_csv.split(',')+ without trimming). Unknown flag names raise
|
|
216
|
+
# +ArgumentError+ — typos in consensus-affecting flag strings would
|
|
217
|
+
# otherwise silently disable the corresponding rule.
|
|
218
|
+
def normalise_flags(flags)
|
|
219
|
+
return nil if flags.nil?
|
|
220
|
+
|
|
221
|
+
normalised = Set.new
|
|
222
|
+
flags.each do |raw|
|
|
223
|
+
next if raw.nil?
|
|
224
|
+
|
|
225
|
+
name = raw.to_s.strip
|
|
226
|
+
next if name.empty?
|
|
227
|
+
|
|
228
|
+
unless KNOWN_FLAGS.include?(name)
|
|
229
|
+
raise ArgumentError,
|
|
230
|
+
"unknown verification flag: #{raw.inspect} (add to KNOWN_FLAGS if intentional)"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
normalised << name
|
|
234
|
+
end
|
|
235
|
+
normalised.freeze
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Ensures the +tx_version:+ kwarg is a valid uint32. Returns +nil+ for nil
|
|
239
|
+
# input. Raises +ArgumentError+ for negative values, values > 2^32-1, or
|
|
240
|
+
# non-Integer inputs — these would otherwise silently coerce inside
|
|
241
|
+
# +Array#pack('V')+ (negative wraps to 0xFFFFFFFF, oversized wraps mod
|
|
242
|
+
# 2^32), masking caller bugs.
|
|
243
|
+
def validate_tx_version(version)
|
|
244
|
+
return nil if version.nil?
|
|
245
|
+
return version if version.is_a?(Integer) && version >= 0 && version <= 0xFFFFFFFF
|
|
246
|
+
|
|
247
|
+
raise ArgumentError, "tx_version must be a uint32 (0..0xFFFFFFFF), got #{version.inspect}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Whether explicit verification flags were supplied.
|
|
251
|
+
# In their absence the interpreter behaves as if Chronicle is active and
|
|
252
|
+
# the unlock-script malleability flags are off (matches the TS SDK's
|
|
253
|
+
# +isRelaxed+ default — see Spend.ts).
|
|
254
|
+
def explicit_flags?
|
|
255
|
+
!@flags.nil?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def flag?(name)
|
|
259
|
+
explicit_flags? && @flags.include?(name)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Whether post-Chronicle semantics apply (OP_2MUL/2DIV enabled,
|
|
263
|
+
# OP_VER/OP_VERIF/OP_VERNOTIF interpret +tx_version+).
|
|
264
|
+
def chronicle?
|
|
265
|
+
return flag?('UTXO_AFTER_CHRONICLE') if explicit_flags?
|
|
266
|
+
|
|
267
|
+
true
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Whether post-Genesis rules apply. With explicit flags this requires one
|
|
271
|
+
# of the genesis-era flags; without, the interpreter is always post-Genesis.
|
|
272
|
+
def after_genesis?
|
|
273
|
+
return flag?('GENESIS') || flag?('UTXO_AFTER_GENESIS') || flag?('UTXO_AFTER_CHRONICLE') if explicit_flags?
|
|
274
|
+
|
|
275
|
+
true
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def enforce_sig_pushonly?
|
|
279
|
+
return flag?('SIGPUSHONLY') if explicit_flags?
|
|
280
|
+
|
|
281
|
+
false
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def enforce_sig_pushonly
|
|
285
|
+
return unless enforce_sig_pushonly?
|
|
286
|
+
return if @unlock_script.push_only?
|
|
287
|
+
|
|
288
|
+
raise ScriptError.new(
|
|
289
|
+
ScriptErrorCode::SIG_PUSHONLY,
|
|
290
|
+
'unlock script must contain only push-data operations'
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def enforce_clean_stack?
|
|
295
|
+
return flag?('CLEANSTACK') if explicit_flags?
|
|
296
|
+
|
|
297
|
+
false
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def enforce_clean_stack
|
|
301
|
+
return unless enforce_clean_stack?
|
|
302
|
+
return if @dstack.length == 1
|
|
303
|
+
|
|
304
|
+
raise ScriptError.new(
|
|
305
|
+
ScriptErrorCode::CLEAN_STACK,
|
|
306
|
+
"CLEANSTACK requires exactly one stack item at end (found #{@dstack.length})"
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
144
310
|
def execute_opcode(chunk)
|
|
145
311
|
opcode = chunk.opcode
|
|
146
312
|
|
|
313
|
+
# Pre-Chronicle, pre-Genesis mode (explicit non-genesis flags) treats
|
|
314
|
+
# OP_VER / OP_VERIF / OP_VERNOTIF as universally illegal — they raise
|
|
315
|
+
# even in a non-executing branch. Mirrors TS Spend.ts line ~700.
|
|
316
|
+
enforce_pre_genesis_ver_gate(opcode)
|
|
317
|
+
|
|
147
318
|
# After OP_RETURN inside a conditional: only process flow control opcodes
|
|
148
319
|
# and OP_RETURN itself (which may terminate at top level once conditionals
|
|
149
320
|
# are balanced), matching Go SDK's branchExecuting semantics.
|
|
150
321
|
if @after_op_return
|
|
151
|
-
|
|
322
|
+
if conditional_opcode?(opcode) || opcode == Opcodes::OP_RETURN
|
|
323
|
+
return if skipped_ver_conditional?(opcode)
|
|
324
|
+
|
|
325
|
+
dispatch_opcode(opcode, chunk)
|
|
326
|
+
end
|
|
152
327
|
return
|
|
153
328
|
end
|
|
154
329
|
|
|
155
330
|
# In non-executing branch: only dispatch conditional opcodes (for nesting tracking).
|
|
156
331
|
# All other opcodes are skipped.
|
|
157
332
|
unless branch_executing?
|
|
158
|
-
|
|
333
|
+
if conditional_opcode?(opcode)
|
|
334
|
+
return if skipped_ver_conditional?(opcode)
|
|
335
|
+
|
|
336
|
+
dispatch_opcode(opcode, chunk)
|
|
337
|
+
end
|
|
159
338
|
return
|
|
160
339
|
end
|
|
161
340
|
|
|
341
|
+
enforce_chronicle_gate(opcode)
|
|
342
|
+
|
|
162
343
|
BSV.logger&.debug do
|
|
163
344
|
name = Opcodes.name_for(opcode) || format('0x%02x', opcode)
|
|
164
345
|
"[Interpreter] #{name} (stack: #{@dstack.length})"
|
|
@@ -166,6 +347,41 @@ module BSV
|
|
|
166
347
|
dispatch_opcode(opcode, chunk)
|
|
167
348
|
end
|
|
168
349
|
|
|
350
|
+
def enforce_chronicle_gate(opcode)
|
|
351
|
+
return unless CHRONICLE_ONLY_OPCODES.include?(opcode)
|
|
352
|
+
return if chronicle?
|
|
353
|
+
|
|
354
|
+
raise ScriptError.new(
|
|
355
|
+
ScriptErrorCode::DISABLED_OPCODE,
|
|
356
|
+
"#{Opcodes.name_for(opcode) || format('0x%02x', opcode)} is disabled outside Chronicle"
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# OP_VERIF / OP_VERNOTIF in a non-executing branch are conditional opcodes
|
|
361
|
+
# post-Chronicle (push to cond_stack, like OP_IF in a non-executing branch),
|
|
362
|
+
# but a complete NOP pre-Chronicle post-genesis — they neither push nor
|
|
363
|
+
# consume. This matches TS Spend.ts (line ~710) and Go opcodeVerConditional.
|
|
364
|
+
def skipped_ver_conditional?(opcode)
|
|
365
|
+
return false unless VER_CONDITIONAL_OPCODES.include?(opcode)
|
|
366
|
+
|
|
367
|
+
!chronicle? && after_genesis?
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Pre-Genesis VERIF / VERNOTIF are unconditionally illegal (even in
|
|
371
|
+
# non-executing branches — they enter the dispatcher as conditional opcodes).
|
|
372
|
+
# OP_VER pre-Genesis is illegal only when executing; in a non-executing
|
|
373
|
+
# branch it's skipped silently. Mirrors the Bitcoin Core script test
|
|
374
|
+
# rule "VER non-functional (ok if not executed); VERIF illegal everywhere".
|
|
375
|
+
def enforce_pre_genesis_ver_gate(opcode)
|
|
376
|
+
return unless VER_CONDITIONAL_OPCODES.include?(opcode)
|
|
377
|
+
return unless explicit_flags? && !after_genesis?
|
|
378
|
+
|
|
379
|
+
raise ScriptError.new(
|
|
380
|
+
ScriptErrorCode::DISABLED_OPCODE,
|
|
381
|
+
"#{Opcodes.name_for(opcode) || format('0x%02x', opcode)} is illegal pre-Genesis"
|
|
382
|
+
)
|
|
383
|
+
end
|
|
384
|
+
|
|
169
385
|
def dispatch_opcode(opcode, chunk)
|
|
170
386
|
case opcode
|
|
171
387
|
# --- Data push ---
|
|
@@ -295,9 +511,12 @@ module BSV
|
|
|
295
511
|
end
|
|
296
512
|
|
|
297
513
|
# Verify final stack state: must have at least one truthy element on top.
|
|
514
|
+
# When the CLEANSTACK flag is set, additionally requires exactly one item.
|
|
298
515
|
def check_final_stack
|
|
299
516
|
raise ScriptError.new(ScriptErrorCode::EMPTY_STACK, 'stack empty at end of script execution') if @dstack.empty?
|
|
300
517
|
|
|
518
|
+
enforce_clean_stack
|
|
519
|
+
|
|
301
520
|
return if @dstack.pop_bool
|
|
302
521
|
|
|
303
522
|
raise ScriptError.new(ScriptErrorCode::EVAL_FALSE, 'false stack entry at end of script execution')
|
|
@@ -42,12 +42,21 @@ module BSV
|
|
|
42
42
|
@else_stack.push(false)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# OP_ELSE: toggle conditional branch
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
#
|
|
101
|
-
#
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
# -
|
|
178
|
-
# - the bytes do not match the 4-byte LE encoding of
|
|
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
|
|
196
|
+
return false if data.bytesize != 4
|
|
181
197
|
|
|
182
|
-
|
|
198
|
+
bytes = tx_version_bytes
|
|
199
|
+
!bytes.nil? && data == bytes
|
|
183
200
|
end
|
|
184
201
|
end
|
|
185
202
|
end
|
|
@@ -93,7 +93,7 @@ module BSV
|
|
|
93
93
|
# the wallet's derived key, then returns a P2PKH unlock wrapped in a
|
|
94
94
|
# PushDrop unlock (which is a pass-through).
|
|
95
95
|
#
|
|
96
|
-
# @param tx [
|
|
96
|
+
# @param tx [Transaction::Tx] the spending transaction
|
|
97
97
|
# @param input_index [Integer] which input to sign
|
|
98
98
|
# @return [BSV::Script::Script] the unlocking script
|
|
99
99
|
def sign(tx, input_index)
|
|
@@ -125,7 +125,7 @@ module BSV
|
|
|
125
125
|
|
|
126
126
|
# Estimated byte length of the unlocking script.
|
|
127
127
|
#
|
|
128
|
-
# @param _tx [
|
|
128
|
+
# @param _tx [Transaction::Tx] unused
|
|
129
129
|
# @param _input_index [Integer] unused
|
|
130
130
|
# @return [Integer]
|
|
131
131
|
def estimated_length(_tx, _input_index)
|
data/lib/bsv/script/script.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
data/lib/bsv/script.rb
CHANGED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module BSV
|
|
8
|
+
module Storage
|
|
9
|
+
# Result returned by {Downloader#download}.
|
|
10
|
+
DownloadResult = Data.define(:data, :mime_type)
|
|
11
|
+
|
|
12
|
+
# Downloads UHRP-addressed content from distributed storage hosts.
|
|
13
|
+
#
|
|
14
|
+
# Resolution: queries the +ls_uhrp+ lookup service via a {BSV::Overlay::LookupResolver},
|
|
15
|
+
# decodes each PushDrop output to extract the host URL (field[2]) and expiry timestamp
|
|
16
|
+
# (field[3], varint). Expired entries are silently dropped.
|
|
17
|
+
#
|
|
18
|
+
# Download: fetches each resolved URL in turn. Any HTTP 4xx/5xx, empty body, or
|
|
19
|
+
# network exception is treated as a failed host and the next URL is tried. This
|
|
20
|
+
# matches the TS SDK contract — no special treatment of 401/402/403.
|
|
21
|
+
#
|
|
22
|
+
# HTTP redirects are not followed (Net::HTTP default). This is intentional in v1.
|
|
23
|
+
#
|
|
24
|
+
# Download URLs are taken directly from the overlay; no private-IP filter is applied
|
|
25
|
+
# here. (The LookupResolver applies SSRF filtering to SLAP-discovered *infrastructure*
|
|
26
|
+
# hosts — content URLs are end-user data and are not subject to the same filter.)
|
|
27
|
+
class Downloader
|
|
28
|
+
# @param network_preset [Symbol] :mainnet, :testnet, or :local
|
|
29
|
+
# @param lookup_resolver [BSV::Overlay::LookupResolver] injectable resolver (nil = default)
|
|
30
|
+
# @param http_client [#call, nil] injectable HTTP client for testing.
|
|
31
|
+
# Must respond to +.call(url_string)+ and return an object exposing +#code+ (Integer),
|
|
32
|
+
# +#body+ (binary String), and +#[](header_name)+ for header access.
|
|
33
|
+
# Default: a thin {Net::HTTP.get_response} wrapper.
|
|
34
|
+
# @param timeout [Integer] per-request timeout in seconds
|
|
35
|
+
def initialize(network_preset: :mainnet, lookup_resolver: nil, http_client: nil, timeout: 30)
|
|
36
|
+
@lookup_resolver = lookup_resolver || BSV::Overlay::LookupResolver.new(network_preset: network_preset)
|
|
37
|
+
@http_client = http_client
|
|
38
|
+
@timeout = timeout
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Resolve a UHRP URL to a list of HTTP(S) download URLs.
|
|
42
|
+
#
|
|
43
|
+
# Queries the +ls_uhrp+ lookup service, decodes each PushDrop output,
|
|
44
|
+
# drops expired entries, and returns the remaining URLs.
|
|
45
|
+
#
|
|
46
|
+
# @param uhrp_url [String] UHRP URL (with or without +uhrp://+ prefix)
|
|
47
|
+
# @return [Array<String>] resolved HTTP(S) download URLs
|
|
48
|
+
# @raise [BSV::Storage::DownloadError] if the lookup answer is not an output-list
|
|
49
|
+
def resolve(uhrp_url)
|
|
50
|
+
question = BSV::Overlay::LookupQuestion.new(
|
|
51
|
+
service: 'ls_uhrp',
|
|
52
|
+
query: { 'uhrpUrl' => BSV::Storage::Utils.normalize_url(uhrp_url) }
|
|
53
|
+
)
|
|
54
|
+
answer = @lookup_resolver.query(question)
|
|
55
|
+
raise DownloadError, 'Lookup answer must be an output list' unless answer.type == 'output-list'
|
|
56
|
+
|
|
57
|
+
current_time = Time.now.to_i
|
|
58
|
+
urls = []
|
|
59
|
+
|
|
60
|
+
answer.outputs.each do |output|
|
|
61
|
+
url = decode_output_url(output, current_time)
|
|
62
|
+
urls << url if url
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
urls
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Download the content addressed by +uhrp_url+.
|
|
69
|
+
#
|
|
70
|
+
# Validates the URL, resolves it to a list of hosts, then attempts each
|
|
71
|
+
# host in order. Verifies the SHA-256 hash of the downloaded body against
|
|
72
|
+
# the hash embedded in the UHRP URL. Returns on the first successful match.
|
|
73
|
+
#
|
|
74
|
+
# @param uhrp_url [String] UHRP URL
|
|
75
|
+
# @return [BSV::Storage::DownloadResult] downloaded data and MIME type
|
|
76
|
+
# @raise [ArgumentError] if the URL is not a valid UHRP URL
|
|
77
|
+
# @raise [BSV::Storage::DownloadError] if no host yields content matching the hash
|
|
78
|
+
def download(uhrp_url)
|
|
79
|
+
raise ArgumentError, 'Invalid parameter UHRP url' unless BSV::Storage::Utils.valid_url?(uhrp_url)
|
|
80
|
+
|
|
81
|
+
urls = resolve(uhrp_url)
|
|
82
|
+
raise DownloadError, 'No one currently hosts this file!' if urls.empty?
|
|
83
|
+
|
|
84
|
+
expected_hash = BSV::Storage::Utils.get_hash_from_url(uhrp_url)
|
|
85
|
+
|
|
86
|
+
urls.each do |url|
|
|
87
|
+
result = attempt_download(url, expected_hash)
|
|
88
|
+
return result if result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
raise DownloadError, "Unable to download content from #{uhrp_url}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def decode_output_url(output, current_time)
|
|
97
|
+
beef_data = output['beef'] || output[:beef]
|
|
98
|
+
output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
|
|
99
|
+
return nil if beef_data.nil?
|
|
100
|
+
return nil if output_index.negative?
|
|
101
|
+
|
|
102
|
+
beef = parse_beef(beef_data)
|
|
103
|
+
return nil unless beef
|
|
104
|
+
|
|
105
|
+
beef_tx = beef.transactions.last
|
|
106
|
+
return nil if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
|
|
107
|
+
|
|
108
|
+
txout = beef_tx.transaction.outputs[output_index]
|
|
109
|
+
return nil unless txout
|
|
110
|
+
|
|
111
|
+
fields = txout.locking_script.pushdrop_fields
|
|
112
|
+
return nil if fields.nil? || fields.length < 4
|
|
113
|
+
|
|
114
|
+
expiry, = BSV::Transaction::VarInt.decode(fields[3])
|
|
115
|
+
return nil if expiry < current_time
|
|
116
|
+
|
|
117
|
+
url = fields[2].force_encoding('UTF-8')
|
|
118
|
+
return nil unless url.valid_encoding?
|
|
119
|
+
|
|
120
|
+
url
|
|
121
|
+
rescue StandardError
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_beef(beef_data)
|
|
126
|
+
case beef_data
|
|
127
|
+
when String
|
|
128
|
+
BSV::Transaction::Beef.from_binary(beef_data)
|
|
129
|
+
when Array
|
|
130
|
+
BSV::Transaction::Beef.from_binary(beef_data.pack('C*'))
|
|
131
|
+
end
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def attempt_download(url, expected_hash)
|
|
137
|
+
response = fetch(url)
|
|
138
|
+
return nil if response.nil?
|
|
139
|
+
|
|
140
|
+
code = response.code.to_i
|
|
141
|
+
# 3xx is treated as a failed host: redirects are intentionally not followed in v1,
|
|
142
|
+
# so a 302 body is never the content we want.
|
|
143
|
+
return nil if code >= 300
|
|
144
|
+
|
|
145
|
+
body = response.body.to_s
|
|
146
|
+
return nil if body.empty?
|
|
147
|
+
|
|
148
|
+
actual_hash = BSV::Primitives::Digest.sha256(body)
|
|
149
|
+
return nil unless actual_hash == expected_hash
|
|
150
|
+
|
|
151
|
+
DownloadResult.new(data: body, mime_type: response['Content-Type'])
|
|
152
|
+
rescue StandardError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def fetch(url)
|
|
157
|
+
if @http_client
|
|
158
|
+
@http_client.call(url)
|
|
159
|
+
else
|
|
160
|
+
uri = URI(url)
|
|
161
|
+
Net::HTTP.start(
|
|
162
|
+
uri.hostname,
|
|
163
|
+
uri.port,
|
|
164
|
+
use_ssl: uri.scheme == 'https',
|
|
165
|
+
open_timeout: @timeout,
|
|
166
|
+
read_timeout: @timeout
|
|
167
|
+
) { |http| http.request(Net::HTTP::Get.new(uri)) }
|
|
168
|
+
end
|
|
169
|
+
rescue StandardError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|