bsv-sdk 0.8.2 → 0.9.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-sdk.md +851 -0
- data/README.md +3 -1
- data/lib/bsv/primitives/curve.rb +31 -34
- data/lib/bsv/primitives/digest.rb +1 -1
- data/lib/bsv/primitives/ecdsa.rb +15 -3
- data/lib/bsv/primitives/extended_key.rb +2 -2
- data/lib/bsv/primitives/hex.rb +77 -0
- data/lib/bsv/primitives/openssl_ec_shim.rb +22 -34
- data/lib/bsv/primitives/private_key.rb +18 -8
- data/lib/bsv/primitives/public_key.rb +6 -3
- data/lib/bsv/primitives/ripemd160.rb +212 -0
- data/lib/bsv/primitives/secp256k1.rb +93 -5
- data/lib/bsv/primitives/signature.rb +6 -1
- data/lib/bsv/primitives.rb +2 -0
- data/lib/bsv/script/builder.rb +1 -1
- data/lib/bsv/script/chunk.rb +21 -2
- data/lib/bsv/script/interpreter/error.rb +2 -0
- data/lib/bsv/script/interpreter/interpreter.rb +14 -8
- data/lib/bsv/script/interpreter/operations/arithmetic.rb +6 -1
- data/lib/bsv/script/interpreter/operations/crypto.rb +24 -10
- data/lib/bsv/script/interpreter/operations/flow_control.rb +23 -7
- data/lib/bsv/script/interpreter/stack.rb +70 -13
- data/lib/bsv/script/opcodes.rb +88 -6
- data/lib/bsv/script/push_drop_template.rb +10 -2
- data/lib/bsv/script/script.rb +262 -77
- data/lib/bsv/transaction/beef.rb +209 -33
- data/lib/bsv/transaction/merkle_path.rb +34 -3
- data/lib/bsv/transaction/transaction.rb +42 -10
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/wallet.rb +1 -0
- metadata +6 -4
- data/CHANGELOG.md +0 -717
|
@@ -253,19 +253,29 @@ module BSV
|
|
|
253
253
|
end
|
|
254
254
|
|
|
255
255
|
# -------------------------------------------------------------------
|
|
256
|
-
# Windowed-NAF scalar multiplication
|
|
256
|
+
# Windowed-NAF scalar multiplication (variable-time, public scalars)
|
|
257
257
|
# -------------------------------------------------------------------
|
|
258
258
|
|
|
259
|
+
# @!visibility private
|
|
260
|
+
# Maximum number of entries kept in the wNAF precomputation cache.
|
|
261
|
+
# Bounds memory usage for long-running processes (e.g. servers).
|
|
262
|
+
WNAF_CACHE_MAX = 512
|
|
263
|
+
|
|
259
264
|
# @!visibility private
|
|
260
265
|
# Cache for precomputed wNAF tables, keyed by "window:x:y".
|
|
266
|
+
# Evicts oldest entry when the LRU limit is reached.
|
|
261
267
|
WNAF_TABLE_CACHE = {} # rubocop:disable Style/MutableConstant
|
|
262
268
|
|
|
263
269
|
# @!visibility private
|
|
264
270
|
# Multiply a point by a scalar using windowed-NAF.
|
|
265
271
|
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
272
|
+
# Variable-time algorithm — suitable only for public scalars (e.g.
|
|
273
|
+
# signature verification). Secret-scalar paths MUST use
|
|
274
|
+
# {scalar_multiply_ct} instead.
|
|
275
|
+
#
|
|
276
|
+
# Internal method — use {Point#mul} or {Point#mul_ct} instead.
|
|
277
|
+
# Exposed as a module function only so the nested Point class can
|
|
278
|
+
# call it; not part of the public API.
|
|
269
279
|
#
|
|
270
280
|
# @param k [Integer] the scalar (must be in [1, N))
|
|
271
281
|
# @param px [Integer] affine x-coordinate of the base point
|
|
@@ -279,6 +289,9 @@ module BSV
|
|
|
279
289
|
tbl = WNAF_TABLE_CACHE[cache_key]
|
|
280
290
|
|
|
281
291
|
if tbl.nil?
|
|
292
|
+
# Evict the oldest entry when the cache is full (simple LRU).
|
|
293
|
+
WNAF_TABLE_CACHE.delete(WNAF_TABLE_CACHE.keys.first) if WNAF_TABLE_CACHE.size >= WNAF_CACHE_MAX
|
|
294
|
+
|
|
282
295
|
tbl_size = 1 << (window - 1) # e.g. w=5 -> 16 entries
|
|
283
296
|
tbl = Array.new(tbl_size)
|
|
284
297
|
tbl[0] = [px, py, 1]
|
|
@@ -320,6 +333,57 @@ module BSV
|
|
|
320
333
|
q
|
|
321
334
|
end
|
|
322
335
|
|
|
336
|
+
# -------------------------------------------------------------------
|
|
337
|
+
# Montgomery ladder scalar multiplication (constant-time, secret scalars)
|
|
338
|
+
# -------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
# @!visibility private
|
|
341
|
+
# Multiply a point by a scalar using the Montgomery ladder.
|
|
342
|
+
#
|
|
343
|
+
# Executes a fixed number of iterations (256) with one +jp_double+
|
|
344
|
+
# and one +jp_add+ per iteration regardless of the scalar value.
|
|
345
|
+
# Use this for ALL secret-scalar paths (key generation, signing,
|
|
346
|
+
# ECDH, BIP-32 derivation).
|
|
347
|
+
#
|
|
348
|
+
# *Best-effort constant-time in interpreted Ruby.* The branch on
|
|
349
|
+
# +bit+ selects which register receives each operation, and both
|
|
350
|
+
# operations always execute. However, Ruby's interpreter, GC, and
|
|
351
|
+
# the early-return branches in +jp_add+/+jp_double+ (for infinity
|
|
352
|
+
# edge cases) mean true constant-time execution is not achievable
|
|
353
|
+
# without native code. This matches the ts-sdk's TypeScript
|
|
354
|
+
# implementation, which has the same structural properties. For
|
|
355
|
+
# production deployments requiring side-channel resistance beyond
|
|
356
|
+
# what an interpreted language can offer, use a native secp256k1
|
|
357
|
+
# library (e.g. libsecp256k1 via FFI).
|
|
358
|
+
#
|
|
359
|
+
# Internal method — use {Point#mul_ct} instead. Not part of the
|
|
360
|
+
# public API.
|
|
361
|
+
#
|
|
362
|
+
# @param k [Integer] secret scalar (must be in [1, N))
|
|
363
|
+
# @param px [Integer] affine x-coordinate of the base point
|
|
364
|
+
# @param py [Integer] affine y-coordinate of the base point
|
|
365
|
+
# @return [Array(Integer, Integer, Integer)] result as Jacobian point
|
|
366
|
+
def scalar_multiply_ct(k, px, py)
|
|
367
|
+
return JP_INFINITY if k.zero?
|
|
368
|
+
|
|
369
|
+
# r0 accumulates the result; r1 = r0 + base_point at all times.
|
|
370
|
+
r0 = JP_INFINITY
|
|
371
|
+
r1 = [px, py, 1]
|
|
372
|
+
|
|
373
|
+
256.times do |i|
|
|
374
|
+
bit = (k >> (255 - i)) & 1
|
|
375
|
+
if bit.zero?
|
|
376
|
+
r1 = jp_add(r0, r1)
|
|
377
|
+
r0 = jp_double(r0)
|
|
378
|
+
else
|
|
379
|
+
r0 = jp_add(r0, r1)
|
|
380
|
+
r1 = jp_double(r1)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
r0
|
|
385
|
+
end
|
|
386
|
+
|
|
323
387
|
# Negate a Jacobian point.
|
|
324
388
|
def jp_neg(p)
|
|
325
389
|
return p if p[2].zero?
|
|
@@ -444,7 +508,10 @@ module BSV
|
|
|
444
508
|
end
|
|
445
509
|
end
|
|
446
510
|
|
|
447
|
-
# Scalar multiplication: self * scalar.
|
|
511
|
+
# Scalar multiplication: self * scalar (variable-time, wNAF).
|
|
512
|
+
#
|
|
513
|
+
# Suitable for public scalars only (e.g. signature verification).
|
|
514
|
+
# For secret-scalar paths use {#mul_ct}.
|
|
448
515
|
#
|
|
449
516
|
# @param scalar [Integer] the scalar multiplier
|
|
450
517
|
# @return [Point] the resulting point
|
|
@@ -461,6 +528,27 @@ module BSV
|
|
|
461
528
|
self.class.new(affine[0], affine[1])
|
|
462
529
|
end
|
|
463
530
|
|
|
531
|
+
# Constant-time scalar multiplication: self * scalar (Montgomery ladder).
|
|
532
|
+
#
|
|
533
|
+
# Processes all 256 bits unconditionally so execution time does not
|
|
534
|
+
# depend on the scalar value. Use this for secret-scalar paths:
|
|
535
|
+
# key generation, signing, and ECDH shared-secret derivation.
|
|
536
|
+
#
|
|
537
|
+
# @param scalar [Integer] the secret scalar multiplier
|
|
538
|
+
# @return [Point] the resulting point
|
|
539
|
+
def mul_ct(scalar)
|
|
540
|
+
return self.class.infinity if scalar.zero? || infinity?
|
|
541
|
+
|
|
542
|
+
scalar %= N
|
|
543
|
+
return self.class.infinity if scalar.zero?
|
|
544
|
+
|
|
545
|
+
jp = Secp256k1.scalar_multiply_ct(scalar, @x, @y)
|
|
546
|
+
affine = Secp256k1.jp_to_affine(jp)
|
|
547
|
+
return self.class.infinity if affine.nil?
|
|
548
|
+
|
|
549
|
+
self.class.new(affine[0], affine[1])
|
|
550
|
+
end
|
|
551
|
+
|
|
464
552
|
# Point addition: self + other.
|
|
465
553
|
#
|
|
466
554
|
# @param other [Point]
|
|
@@ -38,6 +38,11 @@ module BSV
|
|
|
38
38
|
raise ArgumentError, 'signature too short' if bytes.length < 8
|
|
39
39
|
raise ArgumentError, 'invalid sequence tag' unless bytes[0] == 0x30
|
|
40
40
|
|
|
41
|
+
# BIP-66 strict DER: length must be a single byte (0x00–0x7F).
|
|
42
|
+
# Multi-byte length encoding (where the high bit of bytes[1] is set,
|
|
43
|
+
# e.g. 0x81, 0x82 …) is not permitted in ECDSA signatures.
|
|
44
|
+
raise ArgumentError, 'non-canonical DER length (multi-byte encoding not allowed)' if bytes[1] & 0x80 != 0
|
|
45
|
+
|
|
41
46
|
total_len = bytes[1]
|
|
42
47
|
raise ArgumentError, 'length mismatch' unless total_len == bytes.length - 2
|
|
43
48
|
|
|
@@ -89,7 +94,7 @@ module BSV
|
|
|
89
94
|
# @param hex [String] hex-encoded DER signature
|
|
90
95
|
# @return [Signature]
|
|
91
96
|
def self.from_hex(hex)
|
|
92
|
-
from_der(
|
|
97
|
+
from_der(Hex.decode(hex, name: 'signature hex'))
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
# Serialise the signature as a hex-encoded DER string.
|
data/lib/bsv/primitives.rb
CHANGED
|
@@ -7,6 +7,8 @@ module BSV
|
|
|
7
7
|
# HD key derivation (BIP-32), and mnemonic phrase generation (BIP-39).
|
|
8
8
|
# All cryptography uses Ruby's stdlib +openssl+ — no external gems.
|
|
9
9
|
module Primitives
|
|
10
|
+
autoload :Hex, 'bsv/primitives/hex'
|
|
11
|
+
autoload :Ripemd160, 'bsv/primitives/ripemd160'
|
|
10
12
|
autoload :Secp256k1, 'bsv/primitives/secp256k1'
|
|
11
13
|
autoload :Curve, 'bsv/primitives/curve'
|
|
12
14
|
autoload :Digest, 'bsv/primitives/digest'
|
data/lib/bsv/script/builder.rb
CHANGED
|
@@ -45,7 +45,7 @@ module BSV
|
|
|
45
45
|
# @param hex [String] hex-encoded data to push
|
|
46
46
|
# @return [self] for chaining
|
|
47
47
|
def push_hex(hex)
|
|
48
|
-
push_data(
|
|
48
|
+
push_data(BSV::Primitives::Hex.decode(hex, name: 'hex data'))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Finalise and return the constructed {Script}.
|
data/lib/bsv/script/chunk.rb
CHANGED
|
@@ -53,13 +53,32 @@ module BSV
|
|
|
53
53
|
# Render this chunk as human-readable ASM.
|
|
54
54
|
#
|
|
55
55
|
# Data pushes are shown as hex strings; opcodes are shown by name.
|
|
56
|
+
# Opcodes with no defined name are rendered as +OP_UNKNOWN<n>+ (e.g.
|
|
57
|
+
# +OP_UNKNOWN186+) so that the output is unambiguous and round-trippable
|
|
58
|
+
# via +from_asm+.
|
|
59
|
+
#
|
|
60
|
+
# The OP_RETURN opcode is special: it carries the raw tail bytes as its
|
|
61
|
+
# data payload (absorbed during parsing). To preserve round-trip fidelity
|
|
62
|
+
# with +from_asm+, the tail is re-parsed into individual push items and
|
|
63
|
+
# each is rendered as a hex token after +OP_RETURN+.
|
|
56
64
|
#
|
|
57
65
|
# @return [String] ASM representation
|
|
58
66
|
def to_asm
|
|
59
|
-
if @data
|
|
67
|
+
if @opcode == Opcodes::OP_RETURN && @data
|
|
68
|
+
return 'OP_RETURN' if @data.empty?
|
|
69
|
+
|
|
70
|
+
# Re-parse the tail bytes into individual chunks (without OP_RETURN
|
|
71
|
+
# termination) so each push renders as a separate hex token.
|
|
72
|
+
tail_script = BSV::Script::Script.new(@data)
|
|
73
|
+
tail_chunks = tail_script.send(:parse_chunks, terminate_on_op_return: false)
|
|
74
|
+
parts = ['OP_RETURN'] + tail_chunks.map do |ch|
|
|
75
|
+
ch.data? ? ch.data.unpack1('H*') : (Opcodes.name_for(ch.opcode) || "OP_UNKNOWN#{ch.opcode}")
|
|
76
|
+
end
|
|
77
|
+
parts.join(' ')
|
|
78
|
+
elsif @data
|
|
60
79
|
@data.unpack1('H*')
|
|
61
80
|
else
|
|
62
|
-
Opcodes.name_for(@opcode) || @opcode
|
|
81
|
+
Opcodes.name_for(@opcode) || "OP_UNKNOWN#{@opcode}"
|
|
63
82
|
end
|
|
64
83
|
end
|
|
65
84
|
|
|
@@ -42,10 +42,13 @@ module BSV
|
|
|
42
42
|
# Conditional opcodes must be processed even in non-executing branches
|
|
43
43
|
# to maintain correct nesting depth.
|
|
44
44
|
CONDITIONAL_OPCODES = [
|
|
45
|
-
Opcodes::OP_IF, Opcodes::OP_NOTIF, Opcodes::OP_ELSE, Opcodes::OP_ENDIF
|
|
46
|
-
Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF
|
|
45
|
+
Opcodes::OP_IF, Opcodes::OP_NOTIF, Opcodes::OP_ELSE, Opcodes::OP_ENDIF
|
|
47
46
|
].freeze
|
|
48
47
|
|
|
48
|
+
# Maximum nesting depth for OP_IF / OP_NOTIF blocks. Prevents interpreter
|
|
49
|
+
# stack overflow from deeply nested conditionals.
|
|
50
|
+
MAX_CONDITIONAL_DEPTH = 256
|
|
51
|
+
|
|
49
52
|
# Evaluate unlock + lock scripts without transaction context.
|
|
50
53
|
#
|
|
51
54
|
# Signature operations will always fail (no sighash available).
|
|
@@ -166,9 +169,7 @@ module BSV
|
|
|
166
169
|
|
|
167
170
|
# --- Flow control ---
|
|
168
171
|
when Opcodes::OP_NOP, Opcodes::OP_NOP1, Opcodes::OP_CHECKLOCKTIMEVERIFY,
|
|
169
|
-
Opcodes::OP_CHECKSEQUENCEVERIFY, Opcodes::
|
|
170
|
-
Opcodes::OP_NOP6, Opcodes::OP_NOP7, Opcodes::OP_NOP8, Opcodes::OP_NOP9,
|
|
171
|
-
Opcodes::OP_NOP10
|
|
172
|
+
Opcodes::OP_CHECKSEQUENCEVERIFY, Opcodes::OP_NOP9, Opcodes::OP_NOP10
|
|
172
173
|
op_nop
|
|
173
174
|
when Opcodes::OP_IF then op_if
|
|
174
175
|
when Opcodes::OP_NOTIF then op_notif
|
|
@@ -176,10 +177,15 @@ module BSV
|
|
|
176
177
|
when Opcodes::OP_ENDIF then op_endif
|
|
177
178
|
when Opcodes::OP_VERIFY then op_verify
|
|
178
179
|
when Opcodes::OP_RETURN then op_return
|
|
179
|
-
when Opcodes::
|
|
180
|
+
when Opcodes::OP_RESERVED, Opcodes::OP_RESERVED1, Opcodes::OP_RESERVED2
|
|
180
181
|
op_reserved(opcode)
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
# Chronicle fail-safe: OP_VER, OP_VERIF, OP_VERNOTIF, and the Chronicle
|
|
183
|
+
# string/shift slots raise UnimplementedOpcode. Full semantics are
|
|
184
|
+
# deferred to SDK v0.10.
|
|
185
|
+
when Opcodes::OP_VER, Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF,
|
|
186
|
+
Opcodes::OP_SUBSTR, Opcodes::OP_LEFT, Opcodes::OP_RIGHT,
|
|
187
|
+
Opcodes::OP_LSHIFTNUM, Opcodes::OP_RSHIFTNUM
|
|
188
|
+
op_unimplemented(opcode)
|
|
183
189
|
|
|
184
190
|
# --- Stack manipulation ---
|
|
185
191
|
when Opcodes::OP_TOALTSTACK then op_toaltstack
|
|
@@ -59,7 +59,12 @@ module BSV
|
|
|
59
59
|
@dstack.push_int(a - b)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# OP_MUL: a * b (re-enabled after genesis)
|
|
62
|
+
# OP_MUL: a * b (re-enabled after genesis).
|
|
63
|
+
#
|
|
64
|
+
# The O(n²) memory growth of large-operand multiplication is bounded
|
|
65
|
+
# by the 32 MB stack memory cap enforced by Stack#push_int, which calls
|
|
66
|
+
# Stack#push_bytes and raises ScriptErrorCode::STACK_MEMORY_EXCEEDED
|
|
67
|
+
# before the result can be pushed.
|
|
63
68
|
def op_mul
|
|
64
69
|
b = @dstack.pop_int
|
|
65
70
|
a = @dstack.pop_int
|
|
@@ -51,10 +51,13 @@ module BSV
|
|
|
51
51
|
|
|
52
52
|
result = verify_checksig(full_sig, pubkey_bytes)
|
|
53
53
|
|
|
54
|
-
# NULLFAIL: non-empty signature that failed verification
|
|
55
|
-
|
|
54
|
+
# NULLFAIL: a non-empty signature that failed actual verification must be
|
|
55
|
+
# rejected. When there is no transaction context the signature cannot be
|
|
56
|
+
# verified at all — in that case we push false rather than raising, because
|
|
57
|
+
# no real verification failure occurred.
|
|
58
|
+
raise ScriptError.new(ScriptErrorCode::SIG_NULLFAIL, 'non-empty signature failed verification') if !result && @tx
|
|
56
59
|
|
|
57
|
-
@dstack.push_bool(
|
|
60
|
+
@dstack.push_bool(result)
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
# OP_CHECKSIGVERIFY: OP_CHECKSIG then OP_VERIFY
|
|
@@ -65,12 +68,13 @@ module BSV
|
|
|
65
68
|
raise ScriptError.new(ScriptErrorCode::CHECKSIGVERIFY_FAILED, 'OP_CHECKSIGVERIFY failed')
|
|
66
69
|
end
|
|
67
70
|
|
|
68
|
-
# OP_CHECKMULTISIG: verify M-of-N signatures
|
|
71
|
+
# OP_CHECKMULTISIG: verify M-of-N signatures.
|
|
72
|
+
#
|
|
73
|
+
# Post-Genesis BSV removes the 20-key limit. The stack memory cap
|
|
74
|
+
# (Stack::MAX_MEMORY_BYTES) is the practical upper bound on key count.
|
|
69
75
|
def op_checkmultisig
|
|
70
76
|
num_pubkeys = @dstack.pop_int.to_i32
|
|
71
|
-
|
|
72
|
-
raise ScriptError.new(ScriptErrorCode::INVALID_PUBKEY_COUNT, "invalid pubkey count: #{num_pubkeys}")
|
|
73
|
-
end
|
|
77
|
+
raise ScriptError.new(ScriptErrorCode::INVALID_PUBKEY_COUNT, "invalid pubkey count: #{num_pubkeys}") if num_pubkeys.negative?
|
|
74
78
|
|
|
75
79
|
pubkeys = Array.new(num_pubkeys) { @dstack.pop_bytes }
|
|
76
80
|
|
|
@@ -87,8 +91,10 @@ module BSV
|
|
|
87
91
|
|
|
88
92
|
success = multisig_match?(signatures, pubkeys)
|
|
89
93
|
|
|
90
|
-
# NULLFAIL: if failed
|
|
91
|
-
|
|
94
|
+
# NULLFAIL: if failed and a transaction context is present, all non-empty
|
|
95
|
+
# signatures represent real verification failures and must be rejected.
|
|
96
|
+
# Without a tx context no verification occurred, so we push false instead.
|
|
97
|
+
if !success && @tx
|
|
92
98
|
signatures.each do |sig|
|
|
93
99
|
raise ScriptError.new(ScriptErrorCode::SIG_NULLFAIL, 'non-empty signature failed verification') unless sig.empty?
|
|
94
100
|
end
|
|
@@ -183,7 +189,15 @@ module BSV
|
|
|
183
189
|
raise ScriptError.new(ScriptErrorCode::SIG_DER, "invalid DER signature: #{e.message}")
|
|
184
190
|
end
|
|
185
191
|
|
|
186
|
-
# Validate public key encoding
|
|
192
|
+
# Validate public key encoding.
|
|
193
|
+
#
|
|
194
|
+
# Accepted:
|
|
195
|
+
# 0x02, 0x03 — compressed (33 bytes)
|
|
196
|
+
# 0x04 — uncompressed (65 bytes)
|
|
197
|
+
#
|
|
198
|
+
# Rejected:
|
|
199
|
+
# 0x06, 0x07 — hybrid encoding (not valid in BSV scripts)
|
|
200
|
+
# anything else
|
|
187
201
|
def validate_pubkey_encoding!(bytes)
|
|
188
202
|
return if bytes.bytesize == 33 && [0x02, 0x03].include?(bytes.getbyte(0))
|
|
189
203
|
return if bytes.bytesize == 65 && bytes.getbyte(0) == 0x04
|
|
@@ -10,6 +10,13 @@ module BSV
|
|
|
10
10
|
|
|
11
11
|
# OP_IF: conditional execution
|
|
12
12
|
def op_if
|
|
13
|
+
if @cond_stack.length >= MAX_CONDITIONAL_DEPTH
|
|
14
|
+
raise ScriptError.new(
|
|
15
|
+
ScriptErrorCode::UNBALANCED_CONDITIONAL,
|
|
16
|
+
"conditional depth exceeded #{MAX_CONDITIONAL_DEPTH}"
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
if branch_executing?
|
|
14
21
|
@cond_stack.push(@dstack.pop_bool ? :true : :false)
|
|
15
22
|
else
|
|
@@ -20,6 +27,13 @@ module BSV
|
|
|
20
27
|
|
|
21
28
|
# OP_NOTIF: inverse conditional execution
|
|
22
29
|
def op_notif
|
|
30
|
+
if @cond_stack.length >= MAX_CONDITIONAL_DEPTH
|
|
31
|
+
raise ScriptError.new(
|
|
32
|
+
ScriptErrorCode::UNBALANCED_CONDITIONAL,
|
|
33
|
+
"conditional depth exceeded #{MAX_CONDITIONAL_DEPTH}"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
23
37
|
if branch_executing?
|
|
24
38
|
@cond_stack.push(@dstack.pop_bool ? :false : :true)
|
|
25
39
|
else
|
|
@@ -73,7 +87,7 @@ module BSV
|
|
|
73
87
|
# OP_NOP and OP_NOP1..OP_NOP10 (including CLTV/CSV treated as NOP)
|
|
74
88
|
def op_nop; end
|
|
75
89
|
|
|
76
|
-
# OP_RESERVED, OP_RESERVED1, OP_RESERVED2
|
|
90
|
+
# OP_RESERVED, OP_RESERVED1, OP_RESERVED2: fail when executing
|
|
77
91
|
def op_reserved(opcode)
|
|
78
92
|
raise ScriptError.new(
|
|
79
93
|
ScriptErrorCode::RESERVED_OPCODE,
|
|
@@ -81,13 +95,15 @@ module BSV
|
|
|
81
95
|
)
|
|
82
96
|
end
|
|
83
97
|
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
# Chronicle fail-safe: OP_VER, OP_VERIF, OP_VERNOTIF and the Chronicle
|
|
99
|
+
# string/shift slots (OP_SUBSTR, OP_LEFT, OP_RIGHT, OP_LSHIFTNUM,
|
|
100
|
+
# OP_RSHIFTNUM). Full semantics are deferred to SDK v0.10. Any script
|
|
101
|
+
# that reaches one of these opcodes will fail loudly rather than
|
|
102
|
+
# silently succeeding.
|
|
103
|
+
def op_unimplemented(opcode)
|
|
88
104
|
raise ScriptError.new(
|
|
89
|
-
ScriptErrorCode::
|
|
90
|
-
"
|
|
105
|
+
ScriptErrorCode::UNIMPLEMENTED_OPCODE,
|
|
106
|
+
"unimplemented opcode: 0x#{opcode.to_s(16).rjust(2, '0')}"
|
|
91
107
|
)
|
|
92
108
|
end
|
|
93
109
|
end
|
|
@@ -11,23 +11,34 @@ module BSV
|
|
|
11
11
|
# Implements Forth-like stack manipulation operations (dup, drop, swap,
|
|
12
12
|
# rot, over, pick, roll, tuck) parameterised by count for the multi-element
|
|
13
13
|
# opcodes (OP_2DUP, OP_2SWAP, etc.).
|
|
14
|
+
#
|
|
15
|
+
# A 32 MB aggregate memory cap is enforced on every push, matching the
|
|
16
|
+
# ts-sdk behaviour. This provides a natural bound for O(n²) opcodes such
|
|
17
|
+
# as OP_MUL operating on large script numbers.
|
|
14
18
|
class Stack
|
|
19
|
+
# Maximum total bytes held across all stack items (32 MB).
|
|
20
|
+
MAX_MEMORY_BYTES = 32 * 1024 * 1024
|
|
21
|
+
|
|
15
22
|
def initialize
|
|
16
23
|
@items = []
|
|
24
|
+
@memory_usage = 0
|
|
17
25
|
end
|
|
18
26
|
|
|
19
27
|
# --- Push ---
|
|
20
28
|
|
|
21
29
|
def push_bytes(data)
|
|
22
|
-
|
|
30
|
+
check_memory!(data.bytesize)
|
|
31
|
+
bytes = data.b
|
|
32
|
+
@memory_usage += bytes.bytesize
|
|
33
|
+
@items.push(bytes)
|
|
23
34
|
end
|
|
24
35
|
|
|
25
36
|
def push_int(script_number)
|
|
26
|
-
|
|
37
|
+
push_bytes(script_number.to_bytes)
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
def push_bool(val)
|
|
30
|
-
|
|
41
|
+
push_bytes(val ? "\x01".b : ''.b)
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
# --- Pop ---
|
|
@@ -35,10 +46,21 @@ module BSV
|
|
|
35
46
|
def pop_bytes
|
|
36
47
|
stack_error!('stack empty') if @items.empty?
|
|
37
48
|
|
|
38
|
-
@items.pop
|
|
49
|
+
item = @items.pop
|
|
50
|
+
@memory_usage -= item.bytesize
|
|
51
|
+
item
|
|
39
52
|
end
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
# Decode the top stack item as a {ScriptNumber}.
|
|
55
|
+
#
|
|
56
|
+
# @param max_length [Integer] maximum allowed byte length. Defaults to
|
|
57
|
+
# {ScriptNumber::MAX_BYTE_LENGTH} (750,000 bytes), matching Bitcoin's
|
|
58
|
+
# +nMaxNumSize+ constant. Post-Genesis BSV makes this configurable at
|
|
59
|
+
# the node level; the SDK uses the standard default.
|
|
60
|
+
# @param require_minimal [Boolean] whether to enforce minimal encoding.
|
|
61
|
+
# Defaults to +true+ — post-Genesis BSV requires minimal encoding for
|
|
62
|
+
# script numbers.
|
|
63
|
+
def pop_int(max_length: ScriptNumber::MAX_BYTE_LENGTH, require_minimal: true)
|
|
42
64
|
ScriptNumber.from_bytes(pop_bytes, max_length: max_length, require_minimal: require_minimal)
|
|
43
65
|
end
|
|
44
66
|
|
|
@@ -54,7 +76,15 @@ module BSV
|
|
|
54
76
|
@items[@items.length - 1 - idx]
|
|
55
77
|
end
|
|
56
78
|
|
|
57
|
-
|
|
79
|
+
# Decode a stack item as a {ScriptNumber} without removing it.
|
|
80
|
+
#
|
|
81
|
+
# @param idx [Integer] depth from the top (0 = top).
|
|
82
|
+
# @param max_length [Integer] maximum allowed byte length. Defaults to
|
|
83
|
+
# {ScriptNumber::MAX_BYTE_LENGTH} (750,000 bytes), matching Bitcoin's
|
|
84
|
+
# +nMaxNumSize+ constant.
|
|
85
|
+
# @param require_minimal [Boolean] whether to enforce minimal encoding.
|
|
86
|
+
# Defaults to +true+ — post-Genesis BSV requires minimal encoding.
|
|
87
|
+
def peek_int(idx = 0, max_length: ScriptNumber::MAX_BYTE_LENGTH, require_minimal: true)
|
|
58
88
|
ScriptNumber.from_bytes(peek_bytes(idx), max_length: max_length, require_minimal: require_minimal)
|
|
59
89
|
end
|
|
60
90
|
|
|
@@ -74,6 +104,7 @@ module BSV
|
|
|
74
104
|
|
|
75
105
|
def clear
|
|
76
106
|
@items.clear
|
|
107
|
+
@memory_usage = 0
|
|
77
108
|
end
|
|
78
109
|
|
|
79
110
|
def to_a
|
|
@@ -88,7 +119,11 @@ module BSV
|
|
|
88
119
|
stack_error!("stack too small for dup_n(#{count})") if @items.length < count
|
|
89
120
|
|
|
90
121
|
start = @items.length - count
|
|
91
|
-
|
|
122
|
+
added = @items[start..].sum(&:bytesize)
|
|
123
|
+
check_memory!(added)
|
|
124
|
+
copies = Array.new(count) { |i| @items[start + i].dup }
|
|
125
|
+
@memory_usage += added
|
|
126
|
+
@items.concat(copies)
|
|
92
127
|
end
|
|
93
128
|
|
|
94
129
|
# Remove the top N items.
|
|
@@ -96,7 +131,8 @@ module BSV
|
|
|
96
131
|
check_count!(count, 'drop_n')
|
|
97
132
|
stack_error!("stack too small for drop_n(#{count})") if @items.length < count
|
|
98
133
|
|
|
99
|
-
@items.pop(count)
|
|
134
|
+
removed = @items.pop(count)
|
|
135
|
+
@memory_usage -= removed.sum(&:bytesize)
|
|
100
136
|
nil
|
|
101
137
|
end
|
|
102
138
|
|
|
@@ -104,7 +140,9 @@ module BSV
|
|
|
104
140
|
def nip_n(idx)
|
|
105
141
|
check_index!(idx)
|
|
106
142
|
|
|
107
|
-
@items.delete_at(@items.length - 1 - idx)
|
|
143
|
+
removed = @items.delete_at(@items.length - 1 - idx)
|
|
144
|
+
@memory_usage -= removed.bytesize
|
|
145
|
+
removed
|
|
108
146
|
end
|
|
109
147
|
|
|
110
148
|
# Rotate: move the bottom N of the top 3N items to the top.
|
|
@@ -140,17 +178,24 @@ module BSV
|
|
|
140
178
|
stack_error!("stack too small for over_n(#{count})") if @items.length < (2 * count)
|
|
141
179
|
|
|
142
180
|
start = @items.length - (2 * count)
|
|
143
|
-
|
|
181
|
+
added = @items[start, count].sum(&:bytesize)
|
|
182
|
+
check_memory!(added)
|
|
183
|
+
copies = Array.new(count) { |i| @items[start + i].dup }
|
|
184
|
+
@memory_usage += added
|
|
185
|
+
@items.concat(copies)
|
|
144
186
|
end
|
|
145
187
|
|
|
146
188
|
# Copy item at index n to top (0 = top).
|
|
147
189
|
def pick_n(idx)
|
|
148
190
|
check_index!(idx)
|
|
149
191
|
|
|
150
|
-
@items
|
|
192
|
+
copy = @items[@items.length - 1 - idx].dup
|
|
193
|
+
check_memory!(copy.bytesize)
|
|
194
|
+
@memory_usage += copy.bytesize
|
|
195
|
+
@items.push(copy)
|
|
151
196
|
end
|
|
152
197
|
|
|
153
|
-
# Move item at index n to top (0 = top).
|
|
198
|
+
# Move item at index n to top (0 = top). Memory usage is unchanged.
|
|
154
199
|
def roll_n(idx)
|
|
155
200
|
check_index!(idx)
|
|
156
201
|
|
|
@@ -162,7 +207,10 @@ module BSV
|
|
|
162
207
|
def tuck
|
|
163
208
|
stack_error!('stack too small for tuck') if @items.length < 2
|
|
164
209
|
|
|
165
|
-
|
|
210
|
+
copy = @items.last.dup
|
|
211
|
+
check_memory!(copy.bytesize)
|
|
212
|
+
@memory_usage += copy.bytesize
|
|
213
|
+
@items.insert(@items.length - 2, copy)
|
|
166
214
|
end
|
|
167
215
|
|
|
168
216
|
# --- Boolean conversion ---
|
|
@@ -187,6 +235,15 @@ module BSV
|
|
|
187
235
|
raise ScriptError.new(ScriptErrorCode::INVALID_STACK_OPERATION, message)
|
|
188
236
|
end
|
|
189
237
|
|
|
238
|
+
def check_memory!(additional_bytes)
|
|
239
|
+
return unless @memory_usage + additional_bytes > MAX_MEMORY_BYTES
|
|
240
|
+
|
|
241
|
+
raise ScriptError.new(
|
|
242
|
+
ScriptErrorCode::STACK_MEMORY_EXCEEDED,
|
|
243
|
+
"stack memory limit of #{MAX_MEMORY_BYTES} bytes exceeded"
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
190
247
|
def check_index!(idx)
|
|
191
248
|
return if idx >= 0 && idx < @items.length
|
|
192
249
|
|