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.
@@ -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
- # Internal methoduse {Point#mul} instead. Exposed as a module
267
- # function only so the nested Point class can call it; not part of
268
- # the public API.
272
+ # Variable-time algorithmsuitable 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([hex].pack('H*'))
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.
@@ -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'
@@ -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([hex].pack('H*'))
48
+ push_data(BSV::Primitives::Hex.decode(hex, name: 'hex data'))
49
49
  end
50
50
 
51
51
  # Finalise and return the constructed {Script}.
@@ -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.to_s
81
+ Opcodes.name_for(@opcode) || "OP_UNKNOWN#{@opcode}"
63
82
  end
64
83
  end
65
84
 
@@ -49,6 +49,8 @@ module BSV
49
49
  IMPOSSIBLE_ENCODING = :impossible_encoding
50
50
  INVALID_OPCODE = :invalid_opcode
51
51
  MINIMAL_DATA = :minimal_data
52
+ STACK_MEMORY_EXCEEDED = :stack_memory_exceeded
53
+ UNIMPLEMENTED_OPCODE = :unimplemented_opcode
52
54
  end
53
55
  end
54
56
  end
@@ -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::OP_NOP4, Opcodes::OP_NOP5,
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::OP_VER, Opcodes::OP_RESERVED, Opcodes::OP_RESERVED1, Opcodes::OP_RESERVED2
180
+ when Opcodes::OP_RESERVED, Opcodes::OP_RESERVED1, Opcodes::OP_RESERVED2
180
181
  op_reserved(opcode)
181
- when Opcodes::OP_VERIF, Opcodes::OP_VERNOTIF
182
- op_ver_conditional(opcode)
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
- raise ScriptError.new(ScriptErrorCode::SIG_NULLFAIL, 'non-empty signature failed verification') unless result
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(true)
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
- if num_pubkeys.negative? || num_pubkeys > 20
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, all signatures must be empty
91
- unless success
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 (compressed or uncompressed).
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, OP_VER: fail when executing
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
- # OP_VERIF, OP_VERNOTIF: always-illegal after genesis fail only when executing
85
- def op_ver_conditional(opcode)
86
- return unless branch_executing?
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::RESERVED_OPCODE,
90
- "attempt to execute reserved opcode: 0x#{opcode.to_s(16).rjust(2, '0')}"
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
- @items.push(data.b)
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
- @items.push(script_number.to_bytes)
37
+ push_bytes(script_number.to_bytes)
27
38
  end
28
39
 
29
40
  def push_bool(val)
30
- @items.push(val ? "\x01".b : ''.b)
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
- def pop_int(max_length: ScriptNumber::MAX_BYTE_LENGTH, require_minimal: false)
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
- def peek_int(idx = 0, max_length: ScriptNumber::MAX_BYTE_LENGTH, require_minimal: false)
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
- count.times { |i| @items.push(@items[start + i].dup) }
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
- count.times { |i| @items.push(@items[start + i].dup) }
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.push(@items[@items.length - 1 - idx].dup)
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
- @items.insert(@items.length - 2, @items.last.dup)
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