bsv-sdk 0.1.0 → 0.2.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 +49 -0
- data/lib/bsv/primitives/curve.rb +3 -3
- data/lib/bsv/primitives/ecies.rb +5 -5
- data/lib/bsv/primitives/private_key.rb +35 -0
- data/lib/bsv/primitives/public_key.rb +36 -0
- data/lib/bsv/primitives/signature.rb +2 -2
- data/lib/bsv/script/chunk.rb +15 -19
- data/lib/bsv/script/interpreter/interpreter.rb +13 -3
- data/lib/bsv/script/interpreter/operations/arithmetic.rb +1 -1
- data/lib/bsv/script/interpreter/operations/crypto.rb +1 -1
- data/lib/bsv/script/interpreter/operations/flow_control.rb +15 -8
- data/lib/bsv/script/interpreter/script_number.rb +1 -1
- data/lib/bsv/script/interpreter/stack.rb +2 -2
- data/lib/bsv/transaction/beef.rb +303 -1
- data/lib/bsv/transaction/chain_tracker.rb +43 -0
- data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +95 -0
- data/lib/bsv/transaction/chain_trackers.rb +10 -0
- data/lib/bsv/transaction/fee_model.rb +28 -0
- data/lib/bsv/transaction/fee_models/satoshis_per_kilobyte.rb +35 -0
- data/lib/bsv/transaction/fee_models.rb +10 -0
- data/lib/bsv/transaction/merkle_path.rb +17 -2
- data/lib/bsv/transaction/transaction.rb +397 -17
- data/lib/bsv/transaction/transaction_input.rb +16 -0
- data/lib/bsv/transaction/transaction_output.rb +18 -2
- data/lib/bsv/transaction/var_int.rb +20 -0
- data/lib/bsv/transaction/verification_error.rb +26 -0
- data/lib/bsv/transaction.rb +5 -0
- data/lib/bsv/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83e581aac74bb9865acd4dac19b8e7422f8a018f7fea7cd9085eb6ad58add6ca
|
|
4
|
+
data.tar.gz: 7f9bd7d2b7902dc080943a4b4ac30526e82ba38a0d278be14b07d575a8f52ade
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2ad1b26a36e2c052f4109b4ec3649bfdc1f3efd08d0e0e8b08b054a7f61b0a0ae2f777f0da3d4445259681e06b0dd31dc74b252ae0ed8c10f5ff11f67963917
|
|
7
|
+
data.tar.gz: '08955c85b4bee8ffcc1c7e84e775c3306ccf8e573103f2347072f86bcd67633abd96193fe516484e94f4cd047edaad5cda52b6612dabbceade6b05dd272acd09'
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,55 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-03-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
#### Primitives
|
|
13
|
+
|
|
14
|
+
- ECDH shared secret derivation (`PrivateKey#derive_shared_secret`, `PublicKey#derive_shared_secret`)
|
|
15
|
+
- BRC-42 key derivation (`PrivateKey#derive_child`, `PublicKey#derive_child`) with official spec test vectors
|
|
16
|
+
|
|
17
|
+
#### Transaction
|
|
18
|
+
|
|
19
|
+
- Chain tracker interface (`ChainTracker` base class) with WhatsOnChain implementation
|
|
20
|
+
- Fee model interface (`FeeModel` base class) with `SatoshisPerKilobyte` implementation
|
|
21
|
+
- `Transaction#fee` with change output distribution across multiple change outputs
|
|
22
|
+
- `Transaction#verify` for full SPV verification (merkle path, script execution, recursive ancestry)
|
|
23
|
+
- `TransactionOutput#change` flag for identifying change outputs
|
|
24
|
+
- `MerklePath#verify` for SPV chain tracker integration
|
|
25
|
+
- BEEF completion: `Beef#merge`, `Beef#valid?`, lookup methods (`find_bump`, `find_transaction_for_signing`)
|
|
26
|
+
- `Transaction#to_beef` / `Transaction.from_beef` convenience methods
|
|
27
|
+
- Extended Format (EF) transaction serialisation (`to_ef`, `to_ef_hex`, `from_ef`, `from_ef_hex`)
|
|
28
|
+
- `VerificationError` with typed error codes for SPV verification failures
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- ECIES refactored to use `PrivateKey#derive_shared_secret` internally (no API change)
|
|
33
|
+
- `Transaction#estimated_size` made public for fee model access
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Nil `source_satoshis` now raises instead of silently coercing to zero in fee distribution and verification
|
|
38
|
+
- Script chunk round-trips preserve original push encoding
|
|
39
|
+
- `OP_RETURN` inside conditionals correctly checked for conditional balance
|
|
40
|
+
- Point x-coordinate extraction preserves leading zeros via octet string
|
|
41
|
+
- `Integer#nobits?` replaced with Ruby 2.7-compatible bitwise check
|
|
42
|
+
- Defensive parsing with descriptive errors for truncated binary input
|
|
43
|
+
|
|
44
|
+
### Testing
|
|
45
|
+
|
|
46
|
+
- BRC-42 conformance specs with 9 official specification test vectors
|
|
47
|
+
- ECDH conformance specs (commutativity, cross-method, pinned known-key vector)
|
|
48
|
+
- SPV verification conformance specs (merkle path, script execution, ancestry)
|
|
49
|
+
- Fee model conformance specs (formula validation, default rate, change distribution)
|
|
50
|
+
- Chain tracker conformance specs
|
|
51
|
+
- BEEF cross-SDK conformance vectors
|
|
52
|
+
- Schnorr (BRC-94) cross-SDK interoperability vectors
|
|
53
|
+
- 6 exact-match RFC 6979 vectors from Trezor/CoreBitcoin
|
|
54
|
+
- VarInt boundary tests at size-prefix transitions
|
|
55
|
+
- Script vectors converted to tracked known-failures system
|
|
56
|
+
|
|
8
57
|
## [0.1.0] - 2026-02-14
|
|
9
58
|
|
|
10
59
|
Initial release of the BSV Ruby SDK.
|
data/lib/bsv/primitives/curve.rb
CHANGED
|
@@ -65,9 +65,9 @@ module BSV
|
|
|
65
65
|
# @param point [OpenSSL::PKey::EC::Point] the curve point
|
|
66
66
|
# @return [OpenSSL::BN] the x-coordinate
|
|
67
67
|
def point_x(point)
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
OpenSSL::BN.new(
|
|
68
|
+
# Uncompressed octet string: 0x04 || X (32 bytes) || Y (32 bytes)
|
|
69
|
+
# Slicing raw bytes avoids BN#to_s(16) stripping leading zeros.
|
|
70
|
+
OpenSSL::BN.new(point.to_octet_string(:uncompressed)[1, 32], 2)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Reconstruct a curve point from its byte representation.
|
data/lib/bsv/primitives/ecies.rb
CHANGED
|
@@ -39,7 +39,7 @@ module BSV
|
|
|
39
39
|
ephemeral = private_key || PrivateKey.generate
|
|
40
40
|
ephemeral_pub = ephemeral.public_key
|
|
41
41
|
|
|
42
|
-
iv, key_e, key_m = derive_keys(
|
|
42
|
+
iv, key_e, key_m = derive_keys(ephemeral, public_key)
|
|
43
43
|
|
|
44
44
|
cipher = OpenSSL::Cipher.new('aes-128-cbc')
|
|
45
45
|
cipher.encrypt
|
|
@@ -76,7 +76,7 @@ module BSV
|
|
|
76
76
|
|
|
77
77
|
ephemeral_pub = PublicKey.from_bytes(ephemeral_pub_bytes)
|
|
78
78
|
|
|
79
|
-
iv, key_e, key_m = derive_keys(
|
|
79
|
+
iv, key_e, key_m = derive_keys(private_key, ephemeral_pub)
|
|
80
80
|
|
|
81
81
|
# Verify HMAC before decryption (encrypt-then-MAC)
|
|
82
82
|
payload = data[0...-32]
|
|
@@ -111,9 +111,9 @@ module BSV
|
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
def derive_keys(
|
|
115
|
-
|
|
116
|
-
ecdh_key =
|
|
114
|
+
def derive_keys(private_key, public_key)
|
|
115
|
+
shared = private_key.derive_shared_secret(public_key)
|
|
116
|
+
ecdh_key = shared.compressed
|
|
117
117
|
derived = Digest.sha512(ecdh_key)
|
|
118
118
|
|
|
119
119
|
iv = derived[0, 16]
|
|
@@ -127,6 +127,41 @@ module BSV
|
|
|
127
127
|
@public_key ||= PublicKey.new(Curve.multiply_generator(@bn))
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# Derive an ECDH shared secret with another party's public key.
|
|
131
|
+
#
|
|
132
|
+
# Computes the shared point by multiplying the given public key by
|
|
133
|
+
# this private key's scalar. The result is commutative:
|
|
134
|
+
# alice_priv.derive_shared_secret(bob_pub) ==
|
|
135
|
+
# bob_priv.derive_shared_secret(alice_pub)
|
|
136
|
+
#
|
|
137
|
+
# This is the foundational primitive for BRC-42 key derivation,
|
|
138
|
+
# BRC-77/78 messaging, and ECIES encryption.
|
|
139
|
+
#
|
|
140
|
+
# @param public_key [PublicKey] the other party's public key
|
|
141
|
+
# @return [PublicKey] the shared secret as a public key (curve point)
|
|
142
|
+
def derive_shared_secret(public_key)
|
|
143
|
+
shared_point = Curve.multiply_point(public_key.point, @bn)
|
|
144
|
+
PublicKey.new(shared_point)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Derive a child private key using BRC-42 key derivation.
|
|
148
|
+
#
|
|
149
|
+
# Computes HMAC-SHA256(key: ECDH_shared_secret, msg: invoice_number)
|
|
150
|
+
# and adds it to this private key's scalar mod n. The corresponding
|
|
151
|
+
# public key can be derived without the private key using
|
|
152
|
+
# {PublicKey#derive_child}.
|
|
153
|
+
#
|
|
154
|
+
# @param public_key [PublicKey] the counterparty's public key
|
|
155
|
+
# @param invoice_number [String] the invoice number (UTF-8)
|
|
156
|
+
# @return [PrivateKey] the derived child private key
|
|
157
|
+
def derive_child(public_key, invoice_number)
|
|
158
|
+
shared = derive_shared_secret(public_key)
|
|
159
|
+
hmac = Digest.hmac_sha256(shared.compressed, invoice_number.encode('UTF-8'))
|
|
160
|
+
hmac_bn = OpenSSL::BN.new(hmac.unpack1('H*'), 16)
|
|
161
|
+
child_bn = (@bn + hmac_bn).to_i % Curve::N.to_i
|
|
162
|
+
PrivateKey.new(OpenSSL::BN.new(child_bn.to_s))
|
|
163
|
+
end
|
|
164
|
+
|
|
130
165
|
# Sign a 32-byte hash using deterministic ECDSA (RFC 6979).
|
|
131
166
|
#
|
|
132
167
|
# @param hash [String] 32-byte message digest to sign
|
|
@@ -99,6 +99,42 @@ module BSV
|
|
|
99
99
|
Base58.check_encode(prefix + hash160)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
# Derive an ECDH shared secret with another party's private key.
|
|
103
|
+
#
|
|
104
|
+
# Computes the shared point by multiplying this public key by the
|
|
105
|
+
# given private key's scalar. The result is commutative:
|
|
106
|
+
# alice_pub.derive_shared_secret(bob_priv) ==
|
|
107
|
+
# bob_pub.derive_shared_secret(alice_priv)
|
|
108
|
+
#
|
|
109
|
+
# This is the foundational primitive for BRC-42 key derivation,
|
|
110
|
+
# BRC-77/78 messaging, and ECIES encryption.
|
|
111
|
+
#
|
|
112
|
+
# @param private_key [PrivateKey] the other party's private key
|
|
113
|
+
# @return [PublicKey] the shared secret as a public key (curve point)
|
|
114
|
+
def derive_shared_secret(private_key)
|
|
115
|
+
shared_point = Curve.multiply_point(@point, private_key.bn)
|
|
116
|
+
PublicKey.new(shared_point)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Derive a child public key using BRC-42 key derivation.
|
|
120
|
+
#
|
|
121
|
+
# Computes HMAC-SHA256(key: ECDH_shared_secret, msg: invoice_number)
|
|
122
|
+
# and adds the corresponding curve point to this public key. The result
|
|
123
|
+
# matches the public key of {PrivateKey#derive_child} with the same
|
|
124
|
+
# inputs, enabling public-key-only derivation.
|
|
125
|
+
#
|
|
126
|
+
# @param private_key [PrivateKey] the counterparty's private key
|
|
127
|
+
# @param invoice_number [String] the invoice number (UTF-8)
|
|
128
|
+
# @return [PublicKey] the derived child public key
|
|
129
|
+
def derive_child(private_key, invoice_number)
|
|
130
|
+
shared = derive_shared_secret(private_key)
|
|
131
|
+
hmac = Digest.hmac_sha256(shared.compressed, invoice_number.encode('UTF-8'))
|
|
132
|
+
hmac_bn = OpenSSL::BN.new(hmac.unpack1('H*'), 16)
|
|
133
|
+
hmac_point = Curve.multiply_generator(hmac_bn)
|
|
134
|
+
child_point = Curve.add_points(@point, hmac_point)
|
|
135
|
+
PublicKey.new(child_point)
|
|
136
|
+
end
|
|
137
|
+
|
|
102
138
|
# Verify an ECDSA signature against a message hash.
|
|
103
139
|
#
|
|
104
140
|
# @param hash [String] 32-byte message digest
|
|
@@ -50,7 +50,7 @@ module BSV
|
|
|
50
50
|
|
|
51
51
|
r_bytes = bytes[4, r_len]
|
|
52
52
|
raise ArgumentError, 'R has negative flag' if r_bytes[0] & 0x80 != 0
|
|
53
|
-
raise ArgumentError, 'R has excessive padding' if r_len > 1 && r_bytes[0].zero? && r_bytes[1].
|
|
53
|
+
raise ArgumentError, 'R has excessive padding' if r_len > 1 && r_bytes[0].zero? && (r_bytes[1] & 0x80).zero? # rubocop:disable Style/BitwisePredicate
|
|
54
54
|
|
|
55
55
|
# Parse S
|
|
56
56
|
s_offset = 4 + r_len
|
|
@@ -62,7 +62,7 @@ module BSV
|
|
|
62
62
|
|
|
63
63
|
s_bytes = bytes[s_offset + 2, s_len]
|
|
64
64
|
raise ArgumentError, 'S has negative flag' if s_bytes[0] & 0x80 != 0
|
|
65
|
-
raise ArgumentError, 'S has excessive padding' if s_len > 1 && s_bytes[0].zero? && s_bytes[1].
|
|
65
|
+
raise ArgumentError, 'S has excessive padding' if s_len > 1 && s_bytes[0].zero? && (s_bytes[1] & 0x80).zero? # rubocop:disable Style/BitwisePredicate
|
|
66
66
|
|
|
67
67
|
raise ArgumentError, 'trailing bytes' unless s_offset + 2 + s_len == bytes.length
|
|
68
68
|
|
data/lib/bsv/script/chunk.rb
CHANGED
|
@@ -29,12 +29,24 @@ module BSV
|
|
|
29
29
|
|
|
30
30
|
# Serialise this chunk back to raw script bytes.
|
|
31
31
|
#
|
|
32
|
+
# Preserves the original push encoding (including non-minimal pushes)
|
|
33
|
+
# so that round-tripping through parse/serialise does not alter the
|
|
34
|
+
# script bytes. This is critical for sighash computation.
|
|
35
|
+
#
|
|
32
36
|
# @return [String] binary script bytes for this chunk
|
|
33
37
|
def to_binary
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
return [@opcode].pack('C') unless @data
|
|
39
|
+
|
|
40
|
+
case @opcode
|
|
41
|
+
when Opcodes::OP_PUSHDATA1
|
|
42
|
+
[Opcodes::OP_PUSHDATA1, @data.bytesize].pack('CC') + @data
|
|
43
|
+
when Opcodes::OP_PUSHDATA2
|
|
44
|
+
[Opcodes::OP_PUSHDATA2].pack('C') + [@data.bytesize].pack('v') + @data
|
|
45
|
+
when Opcodes::OP_PUSHDATA4
|
|
46
|
+
[Opcodes::OP_PUSHDATA4].pack('C') + [@data.bytesize].pack('V') + @data
|
|
36
47
|
else
|
|
37
|
-
|
|
48
|
+
# Direct push: opcode IS the length (0x01..0x4b)
|
|
49
|
+
[@opcode].pack('C') + @data
|
|
38
50
|
end
|
|
39
51
|
end
|
|
40
52
|
|
|
@@ -56,22 +68,6 @@ module BSV
|
|
|
56
68
|
def ==(other)
|
|
57
69
|
other.is_a?(Chunk) && @opcode == other.opcode && @data == other.data
|
|
58
70
|
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def push_data_binary(data)
|
|
63
|
-
len = data.bytesize
|
|
64
|
-
|
|
65
|
-
if len <= 0x4b
|
|
66
|
-
[len].pack('C') + data
|
|
67
|
-
elsif len <= 0xff
|
|
68
|
-
[Opcodes::OP_PUSHDATA1, len].pack('CC') + data
|
|
69
|
-
elsif len <= 0xffff
|
|
70
|
-
[Opcodes::OP_PUSHDATA2, len].pack('Cv') + data
|
|
71
|
-
else
|
|
72
|
-
[Opcodes::OP_PUSHDATA4, len].pack('CV') + data
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
71
|
end
|
|
76
72
|
end
|
|
77
73
|
end
|
|
@@ -28,7 +28,7 @@ module BSV
|
|
|
28
28
|
# unlock_script: input.script, lock_script: prev_output.script,
|
|
29
29
|
# satoshis: prev_output.satoshis
|
|
30
30
|
# )
|
|
31
|
-
class Interpreter
|
|
31
|
+
class Interpreter
|
|
32
32
|
include Operations::DataPush
|
|
33
33
|
include Operations::StackOps
|
|
34
34
|
include Operations::FlowControl
|
|
@@ -80,7 +80,7 @@ module BSV
|
|
|
80
80
|
).execute
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
def execute
|
|
83
|
+
def execute
|
|
84
84
|
scripts = [@unlock_script, @lock_script]
|
|
85
85
|
|
|
86
86
|
scripts.each_with_index do |script, script_idx|
|
|
@@ -106,6 +106,7 @@ module BSV
|
|
|
106
106
|
# Reset state for next script
|
|
107
107
|
@last_code_sep = 0
|
|
108
108
|
@early_return = false
|
|
109
|
+
@after_op_return = false
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
check_final_stack
|
|
@@ -127,6 +128,7 @@ module BSV
|
|
|
127
128
|
@else_stack = []
|
|
128
129
|
@last_code_sep = 0
|
|
129
130
|
@early_return = false
|
|
131
|
+
@after_op_return = false
|
|
130
132
|
@current_script = nil
|
|
131
133
|
@current_chunk_idx = 0
|
|
132
134
|
end
|
|
@@ -134,6 +136,14 @@ module BSV
|
|
|
134
136
|
def execute_opcode(chunk)
|
|
135
137
|
opcode = chunk.opcode
|
|
136
138
|
|
|
139
|
+
# After OP_RETURN inside a conditional: only process flow control opcodes
|
|
140
|
+
# and OP_RETURN itself (which may terminate at top level once conditionals
|
|
141
|
+
# are balanced), matching Go SDK's branchExecuting semantics.
|
|
142
|
+
if @after_op_return
|
|
143
|
+
dispatch_opcode(opcode, chunk) if conditional_opcode?(opcode) || opcode == Opcodes::OP_RETURN
|
|
144
|
+
return
|
|
145
|
+
end
|
|
146
|
+
|
|
137
147
|
# In non-executing branch: only dispatch conditional opcodes (for nesting tracking).
|
|
138
148
|
# All other opcodes are skipped.
|
|
139
149
|
unless branch_executing?
|
|
@@ -261,7 +271,7 @@ module BSV
|
|
|
261
271
|
# Is the current conditional branch executing?
|
|
262
272
|
# Checks ALL entries — a :false anywhere means we're not executing.
|
|
263
273
|
def branch_executing?
|
|
264
|
-
@cond_stack.none? { |v| v == :false }
|
|
274
|
+
@cond_stack.none? { |v| v == :false }
|
|
265
275
|
end
|
|
266
276
|
|
|
267
277
|
def conditional_opcode?(opcode)
|
|
@@ -6,7 +6,7 @@ module BSV
|
|
|
6
6
|
module Operations
|
|
7
7
|
# Arithmetic and comparison operations including restored post-Genesis
|
|
8
8
|
# opcodes: MUL, DIV, MOD, LSHIFT, RSHIFT.
|
|
9
|
-
module Arithmetic
|
|
9
|
+
module Arithmetic
|
|
10
10
|
private
|
|
11
11
|
|
|
12
12
|
# OP_1ADD: increment top by 1
|
|
@@ -11,9 +11,9 @@ module BSV
|
|
|
11
11
|
# OP_IF: conditional execution
|
|
12
12
|
def op_if
|
|
13
13
|
if branch_executing?
|
|
14
|
-
@cond_stack.push(@dstack.pop_bool ? :true : :false)
|
|
14
|
+
@cond_stack.push(@dstack.pop_bool ? :true : :false)
|
|
15
15
|
else
|
|
16
|
-
@cond_stack.push(:false)
|
|
16
|
+
@cond_stack.push(:false)
|
|
17
17
|
end
|
|
18
18
|
@else_stack.push(false)
|
|
19
19
|
end
|
|
@@ -21,9 +21,9 @@ module BSV
|
|
|
21
21
|
# OP_NOTIF: inverse conditional execution
|
|
22
22
|
def op_notif
|
|
23
23
|
if branch_executing?
|
|
24
|
-
@cond_stack.push(@dstack.pop_bool ? :false : :true)
|
|
24
|
+
@cond_stack.push(@dstack.pop_bool ? :false : :true)
|
|
25
25
|
else
|
|
26
|
-
@cond_stack.push(:false)
|
|
26
|
+
@cond_stack.push(:false)
|
|
27
27
|
end
|
|
28
28
|
@else_stack.push(false)
|
|
29
29
|
end
|
|
@@ -38,8 +38,8 @@ module BSV
|
|
|
38
38
|
raise ScriptError.new(ScriptErrorCode::UNBALANCED_CONDITIONAL, 'duplicate OP_ELSE') if @else_stack.pop
|
|
39
39
|
|
|
40
40
|
case @cond_stack.last
|
|
41
|
-
when :true then @cond_stack[-1] = :false
|
|
42
|
-
when :false then @cond_stack[-1] = :true
|
|
41
|
+
when :true then @cond_stack[-1] = :false
|
|
42
|
+
when :false then @cond_stack[-1] = :true
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
@else_stack.push(true)
|
|
@@ -62,9 +62,16 @@ module BSV
|
|
|
62
62
|
raise ScriptError.new(ScriptErrorCode::VERIFY_FAILED, 'OP_VERIFY failed')
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# OP_RETURN: after-genesis early termination
|
|
65
|
+
# OP_RETURN: after-genesis early termination.
|
|
66
|
+
# At top level (outside conditionals): immediate success.
|
|
67
|
+
# Inside a conditional: remaining opcodes are skipped but conditional
|
|
68
|
+
# balance is still checked at script end.
|
|
66
69
|
def op_return
|
|
67
|
-
@
|
|
70
|
+
if @cond_stack.empty?
|
|
71
|
+
@early_return = true
|
|
72
|
+
else
|
|
73
|
+
@after_op_return = true
|
|
74
|
+
end
|
|
68
75
|
end
|
|
69
76
|
|
|
70
77
|
# OP_NOP and OP_NOP1..OP_NOP10 (including CLTV/CSV treated as NOP)
|
|
@@ -11,7 +11,7 @@ module BSV
|
|
|
11
11
|
# the MSB of the last byte, and the magnitude is stored little-endian.
|
|
12
12
|
# This class handles encoding/decoding, minimal encoding validation,
|
|
13
13
|
# and arithmetic operations as required by the script interpreter.
|
|
14
|
-
class ScriptNumber
|
|
14
|
+
class ScriptNumber
|
|
15
15
|
include Comparable
|
|
16
16
|
|
|
17
17
|
# Maximum byte length for script numbers (post-Genesis: 750 KB).
|
|
@@ -11,7 +11,7 @@ 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
|
-
class Stack
|
|
14
|
+
class Stack
|
|
15
15
|
def initialize
|
|
16
16
|
@items = []
|
|
17
17
|
end
|
|
@@ -168,7 +168,7 @@ module BSV
|
|
|
168
168
|
# --- Boolean conversion ---
|
|
169
169
|
|
|
170
170
|
# Bitcoin consensus boolean: false if empty, all-zero, or negative zero (0x80 last byte).
|
|
171
|
-
def self.cast_bool(bytes)
|
|
171
|
+
def self.cast_bool(bytes)
|
|
172
172
|
return false if bytes.nil? || bytes.empty?
|
|
173
173
|
|
|
174
174
|
bytes.each_byte.with_index do |byte, i|
|