bsv-sdk 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +4 -2
- data/lib/bsv/primitives/digest.rb +107 -13
- 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/script.rb +21 -2
- data/lib/bsv/storage/downloader.rb +1 -1
- data/lib/bsv/storage/utils.rb +1 -1
- data/lib/bsv/transaction/transaction_input.rb +77 -13
- data/lib/bsv/transaction/transaction_output.rb +46 -6
- data/lib/bsv/transaction/tx.rb +359 -28
- data/lib/bsv/version.rb +1 -1
- metadata +1 -2
- data/lib/bsv/secp256k1_native.bundle +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6152fa9bc473f3ff11eca96fa5a9f1302d88cdbe3d72782c13da1760a5507514
|
|
4
|
+
data.tar.gz: e3f45fa1d03d36f9943d172fb8db396869f2ad2ff333b8d071af161062e56fc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 802d2a0fe86432760fa64b12ac6787b4e8a9c3ce02ea5f66e19eee9e3b61bc050f0712fa8c98f5cfe0ca92550a7b7df7927624f5b120fb5274aac082acb66a0b
|
|
7
|
+
data.tar.gz: 7ecae6d4a729130f3d14e637b5822cec7bcab141a7e32abcd4d0275b4c81a0c545d64a2272a8684ef8702b85f4a676f6e3545437ccca5f38a1b57108099baf57
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to the `bsv-sdk` gem are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
6
6
|
and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 0.26.0 — 2026-07-08
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
- `Transaction::Tx#verify` — the `verified:` kwarg now accepts `nil | Hash{String => Boolean}`
|
|
12
|
+
(previously `nil | Set<String>`). The Hash is mutated in place, so a caller can both pre-seed
|
|
13
|
+
the already-verified wtxid set and read back the wtxids walked during verification. A frozen
|
|
14
|
+
Hash, or a Hash with a truthy `default`/`default_proc`, is rejected at entry (funds risk) (#904).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `Transaction::Tx#verify(verified:)` bidirectional verification cache: pre-seeding short-circuits
|
|
18
|
+
already-verified ancestor subtrees; post-read exposes the walked wtxid set for a persistent
|
|
19
|
+
verification cache (#904, #909, #910).
|
|
20
|
+
- Transaction memoisation: per-struct binary, component-hash, and wire-order memos across `Tx`,
|
|
21
|
+
inputs, and outputs, invalidated on mutation — repeated `to_binary`/`wtxid`/hash computation is
|
|
22
|
+
now cached (#881).
|
|
23
|
+
- Conformance canonical corpus: a pinned-revision loader plus fetch/sync tooling and cache layout,
|
|
24
|
+
with the BEEF, BUMP, sighash, SHA-256, ECIES, BRC-42 key-derivation, and script-test specs
|
|
25
|
+
migrated onto it (#840, #841, #842–#847).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Interpreter consensus hardening: correct Chronicle gating of `OP_VER`/`OP_VERIF` (with
|
|
29
|
+
tx_version), `OP_2MUL`/`OP_2DIV`, and multi-`OP_ELSE` (relaxed mode); enforce the `CLEANSTACK`
|
|
30
|
+
and `SIGPUSHONLY` verification flags; whitelist verification flags (#837, #850, #851, #852, #853,
|
|
31
|
+
#854).
|
|
32
|
+
- `Transaction::Tx#verify` — translate the fee-gate `ArgumentError` (missing source data) into a
|
|
33
|
+
`VerificationError` (`:missing_source`) so every failure mode raises a consistent error type
|
|
34
|
+
(#904).
|
|
35
|
+
- `Transaction::Tx#verify` — fix a funds-risk bug where a Hash with a truthy default could silently
|
|
36
|
+
short-circuit verification for unseen wtxids (#904).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- Performance: reuse OpenSSL digest contexts in primitives hashing (thread-safety documented); split
|
|
40
|
+
`clear_caches` from `invalidate_caches` in the transaction cache lifecycle (#881, #882).
|
|
41
|
+
|
|
8
42
|
## 0.25.0 — 2026-06-18
|
|
9
43
|
|
|
10
44
|
### Added
|
data/README.md
CHANGED
|
@@ -24,7 +24,7 @@ This Ruby SDK is a port of the official BSV Blockchain SDKs, which serve as its
|
|
|
24
24
|
|
|
25
25
|
The reference SDKs:
|
|
26
26
|
|
|
27
|
-
- [TypeScript SDK](https://github.com/bsv-blockchain/ts-
|
|
27
|
+
- [TypeScript SDK](https://github.com/bsv-blockchain/ts-stack)
|
|
28
28
|
- [Go SDK](https://github.com/bsv-blockchain/go-sdk)
|
|
29
29
|
- [Python SDK](https://github.com/bsv-blockchain/py-sdk)
|
|
30
30
|
|
|
@@ -89,8 +89,10 @@ locking_script = BSV::Script::Script.p2pkh_lock(pubkey_hash)
|
|
|
89
89
|
tx = BSV::Transaction::Tx.new
|
|
90
90
|
|
|
91
91
|
# Add an input referencing a previous transaction output
|
|
92
|
+
# wtxid_from_hex converts a display-order hex txid to wire-order binary
|
|
93
|
+
utxo_txid = '5884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4'
|
|
92
94
|
input = BSV::Transaction::TransactionInput.new(
|
|
93
|
-
|
|
95
|
+
prev_wtxid: BSV::Transaction::TransactionInput.wtxid_from_hex(utxo_txid),
|
|
94
96
|
prev_tx_out_index: 0
|
|
95
97
|
)
|
|
96
98
|
input.source_satoshis = 100_000
|
|
@@ -9,33 +9,111 @@ module BSV
|
|
|
9
9
|
# Thin wrappers around +OpenSSL::Digest+ and +OpenSSL::HMAC+ providing
|
|
10
10
|
# the hash algorithms used throughout the BSV protocol: SHA-1, SHA-256,
|
|
11
11
|
# double-SHA-256, SHA-512, RIPEMD-160, Hash160, HMAC, and PBKDF2.
|
|
12
|
+
#
|
|
13
|
+
# @note **Per-fibre cached OpenSSL contexts (via +Thread.current+)**
|
|
14
|
+
#
|
|
15
|
+
# +sha256+, +sha256d+, +sha1+, and +sha512+ each cache one
|
|
16
|
+
# +OpenSSL::Digest+ instance per fibre in +Thread.current+ under a
|
|
17
|
+
# namespaced key (e.g. +:bsv_sdk_sha256_digest+). Because MRI has no
|
|
18
|
+
# fibres in this SDK's +lib/+ today, "per fibre" and "per thread" are
|
|
19
|
+
# the same in practice — but the primitive is fibre-local, so the more
|
|
20
|
+
# precise term is used throughout. On every call the context is reset
|
|
21
|
+
# with +OpenSSL::Digest#reset+, which calls +EVP_DigestInit_ex+ against
|
|
22
|
+
# the +EVP_MD*+ already stored on the context. This skips the
|
|
23
|
+
# +EVP_get_digestbyname+ namemap lookup that
|
|
24
|
+
# +OpenSSL::Digest::SHA256.new+ (or +.digest+) performs on every fresh
|
|
25
|
+
# allocation. The namemap cost is paid exactly once per fibre per
|
|
26
|
+
# algorithm.
|
|
27
|
+
#
|
|
28
|
+
# **Fibre-local semantics.** +Thread.current[:key]+ is *fibre-local*
|
|
29
|
+
# in MRI (documented since Ruby 2.0, verified on 3.3 / 3.4 / 4.0) — each
|
|
30
|
+
# Fibre gets its own cached context. This is stronger than the SDK
|
|
31
|
+
# currently requires (no Fibres in +lib/+) and is correctly safe if
|
|
32
|
+
# Fibres are introduced later. The HLR body's "GVL makes it safe"
|
|
33
|
+
# rationale is subtly wrong; the correct reason is fibre-local scope.
|
|
34
|
+
#
|
|
35
|
+
# **Do not touch the cached instances externally.** The keys
|
|
36
|
+
# +:bsv_sdk_sha256_digest+, +:bsv_sdk_sha1_digest+, and
|
|
37
|
+
# +:bsv_sdk_sha512_digest+ are technically reachable via +Thread.current+
|
|
38
|
+
# but must never have +update+ called on them outside this module.
|
|
39
|
+
# Calling +update+ out-of-band would corrupt the next call's input.
|
|
40
|
+
#
|
|
41
|
+
# **User-visible invariants are unchanged:** return values are always
|
|
42
|
+
# +ASCII-8BIT+ binary strings; identical inputs always produce identical
|
|
43
|
+
# outputs; successive calls return distinct +String+ objects (output
|
|
44
|
+
# buffers are never cached).
|
|
45
|
+
#
|
|
46
|
+
# **Prefix convention.** +:bsv_sdk_<algo>_digest+ is the SDK-wide
|
|
47
|
+
# naming convention for any future +Thread.current+ usage in this
|
|
48
|
+
# codebase — the prefix avoids collision with other libraries that may
|
|
49
|
+
# also use +Thread.current+.
|
|
50
|
+
#
|
|
51
|
+
# **FIPS.** SHA-1, SHA-256, and SHA-512 remain FIPS-approved algorithms
|
|
52
|
+
# under this pattern; the algorithm fetch happens once at +.new+, not per
|
|
53
|
+
# call.
|
|
54
|
+
#
|
|
55
|
+
# **CI matrix.** Verified on MRI 3.3, 3.4, and 4.0. JRuby and
|
|
56
|
+
# TruffleRuby are not in the CI matrix — per-thread caching is correct on
|
|
57
|
+
# true-parallel Rubies (each thread has independent +Thread.current+) but
|
|
58
|
+
# unverified here.
|
|
59
|
+
#
|
|
60
|
+
# **Why other SDKs do not do this.** The TypeScript, Go, and Python SDKs
|
|
61
|
+
# allocate fresh digest contexts per call because their OpenSSL bindings
|
|
62
|
+
# cache the algorithm handle internally. MRI's +openssl+ gem bridges
|
|
63
|
+
# through +EVP_get_digestbyname+ on every allocation — we are patching
|
|
64
|
+
# the Ruby binding overhead, not diverging from BSV protocol semantics.
|
|
65
|
+
#
|
|
66
|
+
# **Out of scope with rationale:**
|
|
67
|
+
# - +ripemd160+ — pure-Ruby implementation; no OpenSSL context.
|
|
68
|
+
# - +hmac_sha256+ / +hmac_sha512+ — HMAC key changes per call. A future
|
|
69
|
+
# cache *must* key on wrapper-object identity (+object_id+), never on
|
|
70
|
+
# key bytes — a key-bytes cache lookup would create a secret-dependent
|
|
71
|
+
# side-channel via cache-hit timing.
|
|
72
|
+
# - +pbkdf2_hmac_sha512+ — one-shot at BIP-39 seed derivation; not a
|
|
73
|
+
# hot path.
|
|
12
74
|
module Digest
|
|
13
75
|
module_function
|
|
14
76
|
|
|
15
77
|
# Compute SHA-1 digest.
|
|
16
78
|
#
|
|
17
79
|
# @param data [String] binary data to hash
|
|
18
|
-
# @return [String] 20-byte digest
|
|
80
|
+
# @return [String] 20-byte digest (ASCII-8BIT)
|
|
19
81
|
def sha1(data)
|
|
20
|
-
OpenSSL::Digest
|
|
82
|
+
d = Thread.current[:bsv_sdk_sha1_digest] ||= OpenSSL::Digest.new('SHA1')
|
|
83
|
+
d.reset
|
|
84
|
+
d.update(data)
|
|
85
|
+
d.digest
|
|
21
86
|
end
|
|
22
87
|
|
|
23
88
|
# Compute SHA-256 digest.
|
|
24
89
|
#
|
|
25
90
|
# @param data [String] binary data to hash
|
|
26
|
-
# @return [String] 32-byte digest
|
|
91
|
+
# @return [String] 32-byte digest (ASCII-8BIT)
|
|
27
92
|
def sha256(data)
|
|
28
|
-
OpenSSL::Digest
|
|
93
|
+
d = Thread.current[:bsv_sdk_sha256_digest] ||= OpenSSL::Digest.new('SHA256')
|
|
94
|
+
d.reset
|
|
95
|
+
d.update(data)
|
|
96
|
+
d.digest
|
|
29
97
|
end
|
|
30
98
|
|
|
31
99
|
# Compute double-SHA-256 (SHA-256d) digest.
|
|
32
100
|
#
|
|
33
101
|
# Used extensively in Bitcoin for transaction and block hashing.
|
|
102
|
+
# Inlines the two-round chain against the same cached SHA-256 context,
|
|
103
|
+
# saving one context allocation, one namemap lookup, one intermediate
|
|
104
|
+
# +String+, and one +module_function+ dispatch per call compared with
|
|
105
|
+
# the naive +sha256(sha256(data))+ decomposition.
|
|
34
106
|
#
|
|
35
107
|
# @param data [String] binary data to hash
|
|
36
|
-
# @return [String] 32-byte digest
|
|
108
|
+
# @return [String] 32-byte digest (ASCII-8BIT)
|
|
37
109
|
def sha256d(data)
|
|
38
|
-
|
|
110
|
+
d = Thread.current[:bsv_sdk_sha256_digest] ||= OpenSSL::Digest.new('SHA256')
|
|
111
|
+
d.reset
|
|
112
|
+
d.update(data)
|
|
113
|
+
first = d.digest
|
|
114
|
+
d.reset # defensive: some Ruby versions internally reset after #digest
|
|
115
|
+
d.update(first)
|
|
116
|
+
d.digest
|
|
39
117
|
end
|
|
40
118
|
|
|
41
119
|
alias hash256 sha256d
|
|
@@ -44,15 +122,23 @@ module BSV
|
|
|
44
122
|
# Compute SHA-512 digest.
|
|
45
123
|
#
|
|
46
124
|
# @param data [String] binary data to hash
|
|
47
|
-
# @return [String] 64-byte digest
|
|
125
|
+
# @return [String] 64-byte digest (ASCII-8BIT)
|
|
48
126
|
def sha512(data)
|
|
49
|
-
OpenSSL::Digest
|
|
127
|
+
d = Thread.current[:bsv_sdk_sha512_digest] ||= OpenSSL::Digest.new('SHA512')
|
|
128
|
+
d.reset
|
|
129
|
+
d.update(data)
|
|
130
|
+
d.digest
|
|
50
131
|
end
|
|
51
132
|
|
|
52
133
|
# Compute RIPEMD-160 digest.
|
|
53
134
|
#
|
|
135
|
+
# @note Uses the pure-Ruby {BSV::Primitives::Ripemd160} implementation —
|
|
136
|
+
# no OpenSSL context is held or cached here. RIPEMD-160 is not in
|
|
137
|
+
# scope for the per-thread caching optimisation (different optimisation
|
|
138
|
+
# track; not currently a hot path).
|
|
139
|
+
#
|
|
54
140
|
# @param data [String] binary data to hash
|
|
55
|
-
# @return [String] 20-byte digest
|
|
141
|
+
# @return [String] 20-byte digest (ASCII-8BIT)
|
|
56
142
|
def ripemd160(data)
|
|
57
143
|
Ripemd160.digest(data)
|
|
58
144
|
end
|
|
@@ -62,25 +148,32 @@ module BSV
|
|
|
62
148
|
# Standard Bitcoin hash used for addresses and P2PKH script matching.
|
|
63
149
|
#
|
|
64
150
|
# @param data [String] binary data to hash
|
|
65
|
-
# @return [String] 20-byte digest
|
|
151
|
+
# @return [String] 20-byte digest (ASCII-8BIT)
|
|
66
152
|
def hash160(data)
|
|
67
153
|
ripemd160(sha256(data))
|
|
68
154
|
end
|
|
69
155
|
|
|
70
156
|
# Compute HMAC-SHA-256.
|
|
71
157
|
#
|
|
158
|
+
# @note HMAC context reuse is deferred — the key changes per call, so a
|
|
159
|
+
# future cache must key on wrapper-object identity (+object_id+), never
|
|
160
|
+
# on key bytes, to avoid a secret-dependent side-channel via cache-hit
|
|
161
|
+
# timing.
|
|
162
|
+
#
|
|
72
163
|
# @param key [String] HMAC key
|
|
73
164
|
# @param data [String] data to authenticate
|
|
74
|
-
# @return [String] 32-byte MAC
|
|
165
|
+
# @return [String] 32-byte MAC (ASCII-8BIT)
|
|
75
166
|
def hmac_sha256(key, data)
|
|
76
167
|
OpenSSL::HMAC.digest('SHA256', key, data)
|
|
77
168
|
end
|
|
78
169
|
|
|
79
170
|
# Compute HMAC-SHA-512.
|
|
80
171
|
#
|
|
172
|
+
# @note HMAC context reuse is deferred — see +hmac_sha256+ note.
|
|
173
|
+
#
|
|
81
174
|
# @param key [String] HMAC key
|
|
82
175
|
# @param data [String] data to authenticate
|
|
83
|
-
# @return [String] 64-byte MAC
|
|
176
|
+
# @return [String] 64-byte MAC (ASCII-8BIT)
|
|
84
177
|
def hmac_sha512(key, data)
|
|
85
178
|
OpenSSL::HMAC.digest('SHA512', key, data)
|
|
86
179
|
end
|
|
@@ -88,12 +181,13 @@ module BSV
|
|
|
88
181
|
# Derive a key using PBKDF2-HMAC-SHA-512.
|
|
89
182
|
#
|
|
90
183
|
# Used by BIP-39 to convert mnemonic phrases into seeds.
|
|
184
|
+
# One-shot at key creation — not a hot path; no context reuse applied.
|
|
91
185
|
#
|
|
92
186
|
# @param password [String] the password (mnemonic phrase)
|
|
93
187
|
# @param salt [String] the salt (+"mnemonic"+ + passphrase)
|
|
94
188
|
# @param iterations [Integer] iteration count (default: 2048 per BIP-39)
|
|
95
189
|
# @param key_length [Integer] desired output length in bytes (default: 64)
|
|
96
|
-
# @return [String] derived key bytes
|
|
190
|
+
# @return [String] derived key bytes (ASCII-8BIT)
|
|
97
191
|
def pbkdf2_hmac_sha512(password, salt, iterations: 2048, key_length: 64)
|
|
98
192
|
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, 'sha512')
|
|
99
193
|
end
|
|
@@ -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')
|