bsv-sdk 0.1.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +58 -0
  3. data/LICENCE +86 -0
  4. data/README.md +155 -0
  5. data/lib/bsv/attest/configuration.rb +9 -0
  6. data/lib/bsv/attest/response.rb +19 -0
  7. data/lib/bsv/attest/verification_error.rb +7 -0
  8. data/lib/bsv/attest/version.rb +7 -0
  9. data/lib/bsv/attest.rb +71 -0
  10. data/lib/bsv/network/arc.rb +113 -0
  11. data/lib/bsv/network/broadcast_error.rb +15 -0
  12. data/lib/bsv/network/broadcast_response.rb +29 -0
  13. data/lib/bsv/network/chain_provider_error.rb +14 -0
  14. data/lib/bsv/network/utxo.rb +28 -0
  15. data/lib/bsv/network/whats_on_chain.rb +82 -0
  16. data/lib/bsv/network.rb +12 -0
  17. data/lib/bsv/primitives/base58.rb +117 -0
  18. data/lib/bsv/primitives/bsm.rb +131 -0
  19. data/lib/bsv/primitives/curve.rb +115 -0
  20. data/lib/bsv/primitives/digest.rb +99 -0
  21. data/lib/bsv/primitives/ecdsa.rb +224 -0
  22. data/lib/bsv/primitives/ecies.rb +128 -0
  23. data/lib/bsv/primitives/extended_key.rb +315 -0
  24. data/lib/bsv/primitives/mnemonic/wordlist.rb +270 -0
  25. data/lib/bsv/primitives/mnemonic.rb +192 -0
  26. data/lib/bsv/primitives/private_key.rb +139 -0
  27. data/lib/bsv/primitives/public_key.rb +118 -0
  28. data/lib/bsv/primitives/schnorr.rb +108 -0
  29. data/lib/bsv/primitives/signature.rb +136 -0
  30. data/lib/bsv/primitives.rb +23 -0
  31. data/lib/bsv/script/builder.rb +73 -0
  32. data/lib/bsv/script/chunk.rb +77 -0
  33. data/lib/bsv/script/interpreter/error.rb +54 -0
  34. data/lib/bsv/script/interpreter/interpreter.rb +281 -0
  35. data/lib/bsv/script/interpreter/operations/arithmetic.rb +243 -0
  36. data/lib/bsv/script/interpreter/operations/bitwise.rb +68 -0
  37. data/lib/bsv/script/interpreter/operations/crypto.rb +209 -0
  38. data/lib/bsv/script/interpreter/operations/data_push.rb +34 -0
  39. data/lib/bsv/script/interpreter/operations/flow_control.rb +94 -0
  40. data/lib/bsv/script/interpreter/operations/splice.rb +89 -0
  41. data/lib/bsv/script/interpreter/operations/stack_ops.rb +112 -0
  42. data/lib/bsv/script/interpreter/script_number.rb +218 -0
  43. data/lib/bsv/script/interpreter/stack.rb +203 -0
  44. data/lib/bsv/script/opcodes.rb +165 -0
  45. data/lib/bsv/script/script.rb +424 -0
  46. data/lib/bsv/script.rb +20 -0
  47. data/lib/bsv/transaction/beef.rb +323 -0
  48. data/lib/bsv/transaction/merkle_path.rb +250 -0
  49. data/lib/bsv/transaction/p2pkh.rb +44 -0
  50. data/lib/bsv/transaction/sighash.rb +48 -0
  51. data/lib/bsv/transaction/transaction.rb +380 -0
  52. data/lib/bsv/transaction/transaction_input.rb +109 -0
  53. data/lib/bsv/transaction/transaction_output.rb +51 -0
  54. data/lib/bsv/transaction/unlocking_script_template.rb +36 -0
  55. data/lib/bsv/transaction/var_int.rb +50 -0
  56. data/lib/bsv/transaction.rb +21 -0
  57. data/lib/bsv/version.rb +5 -0
  58. data/lib/bsv/wallet/insufficient_funds_error.rb +15 -0
  59. data/lib/bsv/wallet/wallet.rb +119 -0
  60. data/lib/bsv/wallet.rb +8 -0
  61. data/lib/bsv-attest.rb +4 -0
  62. data/lib/bsv-sdk.rb +11 -0
  63. metadata +104 -0
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+
5
+ module BSV
6
+ module Script
7
+ # Bitcoin script number: arbitrary-precision integer with sign-magnitude
8
+ # little-endian byte encoding.
9
+ #
10
+ # Script numbers use a specialised encoding where the sign bit occupies
11
+ # the MSB of the last byte, and the magnitude is stored little-endian.
12
+ # This class handles encoding/decoding, minimal encoding validation,
13
+ # and arithmetic operations as required by the script interpreter.
14
+ class ScriptNumber # rubocop:disable Metrics/ClassLength
15
+ include Comparable
16
+
17
+ # Maximum byte length for script numbers (post-Genesis: 750 KB).
18
+ MAX_BYTE_LENGTH = 750_000
19
+
20
+ # Minimum 32-bit signed integer value.
21
+ INT32_MIN = -(2**31)
22
+
23
+ # Maximum 32-bit signed integer value.
24
+ INT32_MAX = (2**31) - 1
25
+
26
+ # @return [Integer] the numeric value
27
+ attr_reader :value
28
+
29
+ def initialize(value)
30
+ @value = value
31
+ end
32
+
33
+ # Decode little-endian sign-magnitude bytes into a ScriptNumber.
34
+ #
35
+ # Encoding: little-endian magnitude with sign bit in the MSB of the last byte.
36
+ # 127 -> [0x7f]
37
+ # -127 -> [0xff]
38
+ # 128 -> [0x80 0x00]
39
+ # -128 -> [0x80 0x80]
40
+ # 256 -> [0x00 0x01]
41
+ # -256 -> [0x00 0x81]
42
+ def self.from_bytes(bytes, max_length: MAX_BYTE_LENGTH, require_minimal: false)
43
+ bytes = bytes.b if bytes.encoding != Encoding::ASCII_8BIT
44
+ return new(0) if bytes.empty?
45
+
46
+ if bytes.bytesize > max_length
47
+ raise ScriptError.new(
48
+ ScriptErrorCode::NUMBER_TOO_BIG,
49
+ "script number overflow: #{bytes.bytesize} > #{max_length}"
50
+ )
51
+ end
52
+
53
+ check_minimal_encoding!(bytes) if require_minimal
54
+
55
+ # Accumulate little-endian magnitude
56
+ val = 0
57
+ bytes.each_byte.with_index do |byte, i|
58
+ val |= byte << (8 * i)
59
+ end
60
+
61
+ # Extract sign from MSB of last byte
62
+ if bytes.getbyte(bytes.bytesize - 1) & 0x80 != 0
63
+ val ^= 0x80 << (8 * (bytes.bytesize - 1))
64
+ val = -val
65
+ end
66
+
67
+ new(val)
68
+ end
69
+
70
+ # Encode as little-endian sign-magnitude bytes.
71
+ def to_bytes
72
+ return ''.b if @value.zero?
73
+
74
+ negative = @value.negative?
75
+ abs_val = @value.abs
76
+
77
+ result = []
78
+ v = abs_val
79
+ while v.positive?
80
+ result << (v & 0xff)
81
+ v >>= 8
82
+ end
83
+
84
+ if result.last & 0x80 != 0
85
+ # MSB conflicts with sign bit — add padding byte
86
+ result << (negative ? 0x80 : 0x00)
87
+ elsif negative
88
+ # Set sign bit in MSB
89
+ result[-1] |= 0x80
90
+ end
91
+
92
+ result.pack('C*')
93
+ end
94
+
95
+ # Strip trailing zero-padding while preserving sign.
96
+ def self.minimally_encode(data)
97
+ return ''.b if data.nil? || data.empty?
98
+
99
+ data = data.b
100
+ last = data.getbyte(data.bytesize - 1)
101
+
102
+ # If MSB has non-sign bits set, already minimal
103
+ return data.dup if last.anybits?(0x7f)
104
+
105
+ # Single byte with only sign/zero bits — encode as empty
106
+ return ''.b if data.bytesize == 1
107
+
108
+ # If second-to-last byte needs the sign extension, keep it
109
+ return data.dup if data.getbyte(data.bytesize - 2) & 0x80 != 0
110
+
111
+ # Scan backwards to find last non-zero byte
112
+ i = data.bytesize - 2
113
+ i -= 1 while i.positive? && data.getbyte(i).zero?
114
+
115
+ if data.getbyte(i).zero?
116
+ # All zeros
117
+ ''.b
118
+ elsif data.getbyte(i) & 0x80 != 0
119
+ # This byte has high bit set — keep sign extension byte
120
+ result = data.byteslice(0, i + 1)
121
+ result << [last].pack('C')
122
+ result
123
+ else
124
+ # Fold sign bit into this byte
125
+ result = data.byteslice(0, i + 1).b
126
+ result.setbyte(i, data.getbyte(i) | (last & 0x80))
127
+ result
128
+ end
129
+ end
130
+
131
+ # Clamp to int32 range (for opcodes that need bounded indices).
132
+ def to_i32
133
+ return INT32_MIN if @value < INT32_MIN
134
+ return INT32_MAX if @value > INT32_MAX
135
+
136
+ @value
137
+ end
138
+
139
+ def to_i
140
+ @value
141
+ end
142
+
143
+ def zero?
144
+ @value.zero?
145
+ end
146
+
147
+ # --- Arithmetic (returns new ScriptNumber) ---
148
+
149
+ def +(other)
150
+ self.class.new(@value + other.value)
151
+ end
152
+
153
+ def -(other)
154
+ self.class.new(@value - other.value)
155
+ end
156
+
157
+ def *(other)
158
+ self.class.new(@value * other.value)
159
+ end
160
+
161
+ # Truncated-toward-zero division (matching Bitcoin consensus).
162
+ def /(other)
163
+ raise ScriptError.new(ScriptErrorCode::DIVIDE_BY_ZERO, 'division by zero') if other.value.zero?
164
+
165
+ result = @value.abs / other.value.abs
166
+ result = -result if @value.negative? ^ other.value.negative?
167
+ self.class.new(result)
168
+ end
169
+
170
+ # Remainder with sign of dividend (matching Bitcoin consensus).
171
+ def %(other)
172
+ raise ScriptError.new(ScriptErrorCode::DIVIDE_BY_ZERO, 'modulo by zero') if other.value.zero?
173
+
174
+ result = @value.abs % other.value.abs
175
+ result = -result if @value.negative?
176
+ self.class.new(result)
177
+ end
178
+
179
+ def -@
180
+ self.class.new(-@value)
181
+ end
182
+
183
+ def abs
184
+ self.class.new(@value.abs)
185
+ end
186
+
187
+ def <=>(other)
188
+ case other
189
+ when ScriptNumber
190
+ @value <=> other.value
191
+ when Integer
192
+ @value <=> other
193
+ end
194
+ end
195
+
196
+ def self.check_minimal_encoding!(bytes)
197
+ return if bytes.empty?
198
+
199
+ msb = bytes.getbyte(bytes.bytesize - 1)
200
+
201
+ # If MSB has non-sign bits set, encoding is minimal
202
+ return if msb.anybits?(0x7f)
203
+
204
+ # Single byte that is pure sign/zero (0x00 or 0x80) — not minimal
205
+ if bytes.bytesize == 1
206
+ raise ScriptError.new(ScriptErrorCode::MINIMAL_DATA, 'non-minimal script number encoding')
207
+ end
208
+
209
+ # Padding is justified if second-to-last byte has high bit set
210
+ return if bytes.getbyte(bytes.bytesize - 2) & 0x80 != 0
211
+
212
+ raise ScriptError.new(ScriptErrorCode::MINIMAL_DATA, 'non-minimal script number encoding')
213
+ end
214
+
215
+ private_class_method :check_minimal_encoding!
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+ require_relative 'script_number'
5
+
6
+ module BSV
7
+ module Script
8
+ # Script execution stack providing push/pop/peek operations for bytes,
9
+ # integers ({ScriptNumber}), and booleans.
10
+ #
11
+ # Implements Forth-like stack manipulation operations (dup, drop, swap,
12
+ # rot, over, pick, roll, tuck) parameterised by count for the multi-element
13
+ # opcodes (OP_2DUP, OP_2SWAP, etc.).
14
+ class Stack # rubocop:disable Metrics/ClassLength
15
+ def initialize
16
+ @items = []
17
+ end
18
+
19
+ # --- Push ---
20
+
21
+ def push_bytes(data)
22
+ @items.push(data.b)
23
+ end
24
+
25
+ def push_int(script_number)
26
+ @items.push(script_number.to_bytes)
27
+ end
28
+
29
+ def push_bool(val)
30
+ @items.push(val ? "\x01".b : ''.b)
31
+ end
32
+
33
+ # --- Pop ---
34
+
35
+ def pop_bytes
36
+ stack_error!('stack empty') if @items.empty?
37
+
38
+ @items.pop
39
+ end
40
+
41
+ def pop_int(max_length: ScriptNumber::MAX_BYTE_LENGTH, require_minimal: false)
42
+ ScriptNumber.from_bytes(pop_bytes, max_length: max_length, require_minimal: require_minimal)
43
+ end
44
+
45
+ def pop_bool
46
+ self.class.cast_bool(pop_bytes)
47
+ end
48
+
49
+ # --- Peek ---
50
+
51
+ def peek_bytes(idx = 0)
52
+ check_index!(idx)
53
+
54
+ @items[@items.length - 1 - idx]
55
+ end
56
+
57
+ def peek_int(idx = 0, max_length: ScriptNumber::MAX_BYTE_LENGTH, require_minimal: false)
58
+ ScriptNumber.from_bytes(peek_bytes(idx), max_length: max_length, require_minimal: require_minimal)
59
+ end
60
+
61
+ def peek_bool(idx = 0)
62
+ self.class.cast_bool(peek_bytes(idx))
63
+ end
64
+
65
+ # --- Info ---
66
+
67
+ def depth
68
+ @items.length
69
+ end
70
+
71
+ def empty?
72
+ @items.empty?
73
+ end
74
+
75
+ def clear
76
+ @items.clear
77
+ end
78
+
79
+ def to_a
80
+ @items.dup
81
+ end
82
+
83
+ # --- FORTH-like operations ---
84
+
85
+ # Duplicate the top N items.
86
+ def dup_n(count)
87
+ check_count!(count, 'dup_n')
88
+ stack_error!("stack too small for dup_n(#{count})") if @items.length < count
89
+
90
+ start = @items.length - count
91
+ count.times { |i| @items.push(@items[start + i].dup) }
92
+ end
93
+
94
+ # Remove the top N items.
95
+ def drop_n(count)
96
+ check_count!(count, 'drop_n')
97
+ stack_error!("stack too small for drop_n(#{count})") if @items.length < count
98
+
99
+ @items.pop(count)
100
+ nil
101
+ end
102
+
103
+ # Remove item at offset idx from top (0 = top).
104
+ def nip_n(idx)
105
+ check_index!(idx)
106
+
107
+ @items.delete_at(@items.length - 1 - idx)
108
+ end
109
+
110
+ # Rotate: move the bottom N of the top 3N items to the top.
111
+ # OP_ROT (n=1): [x1 x2 x3] -> [x2 x3 x1]
112
+ # OP_2ROT (n=2): [x1 x2 x3 x4 x5 x6] -> [x3 x4 x5 x6 x1 x2]
113
+ def rot_n(count)
114
+ check_count!(count, 'rot_n')
115
+ stack_error!("stack too small for rot_n(#{count})") if @items.length < (3 * count)
116
+
117
+ removed = @items.slice!(@items.length - (3 * count), count)
118
+ @items.concat(removed)
119
+ end
120
+
121
+ # Swap the top N items with the next N.
122
+ # OP_SWAP (n=1): [x1 x2] -> [x2 x1]
123
+ # OP_2SWAP (n=2): [x1 x2 x3 x4] -> [x3 x4 x1 x2]
124
+ def swap_n(count)
125
+ check_count!(count, 'swap_n')
126
+ stack_error!("stack too small for swap_n(#{count})") if @items.length < (2 * count)
127
+
128
+ count.times do |i|
129
+ a = @items.length - count + i
130
+ b = @items.length - (2 * count) + i
131
+ @items[a], @items[b] = @items[b], @items[a]
132
+ end
133
+ end
134
+
135
+ # Copy N items from 2N depth to top.
136
+ # OP_OVER (n=1): [x1 x2] -> [x1 x2 x1]
137
+ # OP_2OVER (n=2): [x1 x2 x3 x4] -> [x1 x2 x3 x4 x1 x2]
138
+ def over_n(count)
139
+ check_count!(count, 'over_n')
140
+ stack_error!("stack too small for over_n(#{count})") if @items.length < (2 * count)
141
+
142
+ start = @items.length - (2 * count)
143
+ count.times { |i| @items.push(@items[start + i].dup) }
144
+ end
145
+
146
+ # Copy item at index n to top (0 = top).
147
+ def pick_n(idx)
148
+ check_index!(idx)
149
+
150
+ @items.push(@items[@items.length - 1 - idx].dup)
151
+ end
152
+
153
+ # Move item at index n to top (0 = top).
154
+ def roll_n(idx)
155
+ check_index!(idx)
156
+
157
+ val = @items.delete_at(@items.length - 1 - idx)
158
+ @items.push(val)
159
+ end
160
+
161
+ # Copy top and insert before second: [x1 x2] -> [x2 x1 x2]
162
+ def tuck
163
+ stack_error!('stack too small for tuck') if @items.length < 2
164
+
165
+ @items.insert(@items.length - 2, @items.last.dup)
166
+ end
167
+
168
+ # --- Boolean conversion ---
169
+
170
+ # Bitcoin consensus boolean: false if empty, all-zero, or negative zero (0x80 last byte).
171
+ def self.cast_bool(bytes) # rubocop:disable Naming/PredicateMethod
172
+ return false if bytes.nil? || bytes.empty?
173
+
174
+ bytes.each_byte.with_index do |byte, i|
175
+ next if byte.zero?
176
+
177
+ # Negative zero: last byte is exactly 0x80
178
+ return !(i == bytes.bytesize - 1 && byte == 0x80)
179
+ end
180
+
181
+ false
182
+ end
183
+
184
+ private
185
+
186
+ def stack_error!(message)
187
+ raise ScriptError.new(ScriptErrorCode::INVALID_STACK_OPERATION, message)
188
+ end
189
+
190
+ def check_index!(idx)
191
+ return if idx >= 0 && idx < @items.length
192
+
193
+ stack_error!("index #{idx} invalid for stack size #{@items.length}")
194
+ end
195
+
196
+ def check_count!(count, operation)
197
+ return if count >= 1
198
+
199
+ stack_error!("#{operation} requires n >= 1, got #{count}")
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Script
5
+ # All Bitcoin script opcodes as integer constants.
6
+ #
7
+ # Includes data push operations, flow control, stack manipulation,
8
+ # splice, bitwise, arithmetic, and cryptographic opcodes. Also
9
+ # provides {.name_for} for reverse lookup (opcode byte → name).
10
+ module Opcodes
11
+ # Push value
12
+ OP_0 = 0x00
13
+ OP_FALSE = OP_0
14
+ OP_PUSHDATA1 = 0x4c
15
+ OP_PUSHDATA2 = 0x4d
16
+ OP_PUSHDATA4 = 0x4e
17
+ OP_1NEGATE = 0x4f
18
+ OP_RESERVED = 0x50
19
+ OP_1 = 0x51
20
+ OP_TRUE = OP_1
21
+ OP_2 = 0x52
22
+ OP_3 = 0x53
23
+ OP_4 = 0x54
24
+ OP_5 = 0x55
25
+ OP_6 = 0x56
26
+ OP_7 = 0x57
27
+ OP_8 = 0x58
28
+ OP_9 = 0x59
29
+ OP_10 = 0x5a
30
+ OP_11 = 0x5b
31
+ OP_12 = 0x5c
32
+ OP_13 = 0x5d
33
+ OP_14 = 0x5e
34
+ OP_15 = 0x5f
35
+ OP_16 = 0x60
36
+
37
+ # Flow control
38
+ OP_NOP = 0x61
39
+ OP_VER = 0x62
40
+ OP_IF = 0x63
41
+ OP_NOTIF = 0x64
42
+ OP_VERIF = 0x65
43
+ OP_VERNOTIF = 0x66
44
+ OP_ELSE = 0x67
45
+ OP_ENDIF = 0x68
46
+ OP_VERIFY = 0x69
47
+ OP_RETURN = 0x6a
48
+
49
+ # Stack
50
+ OP_TOALTSTACK = 0x6b
51
+ OP_FROMALTSTACK = 0x6c
52
+ OP_2DROP = 0x6d
53
+ OP_2DUP = 0x6e
54
+ OP_3DUP = 0x6f
55
+ OP_2OVER = 0x70
56
+ OP_2ROT = 0x71
57
+ OP_2SWAP = 0x72
58
+ OP_IFDUP = 0x73
59
+ OP_DEPTH = 0x74
60
+ OP_DROP = 0x75
61
+ OP_DUP = 0x76
62
+ OP_NIP = 0x77
63
+ OP_OVER = 0x78
64
+ OP_PICK = 0x79
65
+ OP_ROLL = 0x7a
66
+ OP_ROT = 0x7b
67
+ OP_SWAP = 0x7c
68
+ OP_TUCK = 0x7d
69
+
70
+ # Splice
71
+ OP_CAT = 0x7e
72
+ OP_SPLIT = 0x7f
73
+ OP_NUM2BIN = 0x80
74
+ OP_BIN2NUM = 0x81
75
+ OP_SIZE = 0x82
76
+
77
+ # Bitwise logic
78
+ OP_INVERT = 0x83
79
+ OP_AND = 0x84
80
+ OP_OR = 0x85
81
+ OP_XOR = 0x86
82
+ OP_EQUAL = 0x87
83
+ OP_EQUALVERIFY = 0x88
84
+ OP_RESERVED1 = 0x89
85
+ OP_RESERVED2 = 0x8a
86
+
87
+ # Arithmetic
88
+ OP_1ADD = 0x8b
89
+ OP_1SUB = 0x8c
90
+ OP_2MUL = 0x8d
91
+ OP_2DIV = 0x8e
92
+ OP_NEGATE = 0x8f
93
+ OP_ABS = 0x90
94
+ OP_NOT = 0x91
95
+ OP_0NOTEQUAL = 0x92
96
+ OP_ADD = 0x93
97
+ OP_SUB = 0x94
98
+ OP_MUL = 0x95
99
+ OP_DIV = 0x96
100
+ OP_MOD = 0x97
101
+ OP_LSHIFT = 0x98
102
+ OP_RSHIFT = 0x99
103
+ OP_BOOLAND = 0x9a
104
+ OP_BOOLOR = 0x9b
105
+ OP_NUMEQUAL = 0x9c
106
+ OP_NUMEQUALVERIFY = 0x9d
107
+ OP_NUMNOTEQUAL = 0x9e
108
+ OP_LESSTHAN = 0x9f
109
+ OP_GREATERTHAN = 0xa0
110
+ OP_LESSTHANOREQUAL = 0xa1
111
+ OP_GREATERTHANOREQUAL = 0xa2
112
+ OP_MIN = 0xa3
113
+ OP_MAX = 0xa4
114
+ OP_WITHIN = 0xa5
115
+
116
+ # Crypto
117
+ OP_RIPEMD160 = 0xa6
118
+ OP_SHA1 = 0xa7
119
+ OP_SHA256 = 0xa8
120
+ OP_HASH160 = 0xa9
121
+ OP_HASH256 = 0xaa
122
+ OP_CODESEPARATOR = 0xab
123
+ OP_CHECKSIG = 0xac
124
+ OP_CHECKSIGVERIFY = 0xad
125
+ OP_CHECKMULTISIG = 0xae
126
+ OP_CHECKMULTISIGVERIFY = 0xaf
127
+
128
+ # Expansion
129
+ OP_NOP1 = 0xb0
130
+ OP_CHECKLOCKTIMEVERIFY = 0xb1
131
+ OP_CHECKSEQUENCEVERIFY = 0xb2
132
+ OP_NOP4 = 0xb3
133
+ OP_NOP5 = 0xb4
134
+ OP_NOP6 = 0xb5
135
+ OP_NOP7 = 0xb6
136
+ OP_NOP8 = 0xb7
137
+ OP_NOP9 = 0xb8
138
+ OP_NOP10 = 0xb9
139
+
140
+ # Pseudo-words (not used in scripts)
141
+ OP_PUBKEYHASH = 0xfd
142
+ OP_PUBKEY = 0xfe
143
+ OP_INVALIDOPCODE = 0xff
144
+
145
+ # Reverse lookup: opcode byte → name string.
146
+ # Sorted so canonical names (OP_0, OP_1) win over aliases (OP_FALSE, OP_TRUE)
147
+ # regardless of Module#constants enumeration order.
148
+ NAME = constants
149
+ .select { |c| c.to_s.start_with?('OP_') }
150
+ .sort
151
+ .each_with_object({}) { |c, h| h[const_get(c)] ||= c.to_s }
152
+ .freeze
153
+
154
+ module_function
155
+
156
+ # Look up the canonical name for an opcode byte.
157
+ #
158
+ # @param opcode [Integer] the opcode byte value
159
+ # @return [String, nil] the opcode name (e.g. +"OP_DUP"+), or +nil+ if unknown
160
+ def name_for(opcode)
161
+ NAME[opcode]
162
+ end
163
+ end
164
+ end
165
+ end