btcruby 0.0.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +18 -0
- data/.travis.yml +7 -0
- data/FAQ.md +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +18 -0
- data/HOWTO.md +17 -0
- data/LICENSE +19 -0
- data/README.md +59 -0
- data/Rakefile +6 -0
- data/TODO.txt +40 -0
- data/bin/console +19 -0
- data/btcruby.gemspec +20 -0
- data/documentation/address.md +73 -0
- data/documentation/base58.md +52 -0
- data/documentation/block.md +127 -0
- data/documentation/block_header.md +120 -0
- data/documentation/constants.md +88 -0
- data/documentation/data.md +54 -0
- data/documentation/diagnostics.md +90 -0
- data/documentation/extensions.md +76 -0
- data/documentation/hash_functions.md +58 -0
- data/documentation/hash_id.md +22 -0
- data/documentation/index.md +230 -0
- data/documentation/key.md +177 -0
- data/documentation/keychain.md +180 -0
- data/documentation/network.md +75 -0
- data/documentation/opcode.md +220 -0
- data/documentation/openssl.md +7 -0
- data/documentation/p2pkh.md +71 -0
- data/documentation/p2sh.md +64 -0
- data/documentation/proof_of_work.md +84 -0
- data/documentation/script.md +280 -0
- data/documentation/signature.md +71 -0
- data/documentation/transaction.md +213 -0
- data/documentation/transaction_builder.md +188 -0
- data/documentation/transaction_input.md +133 -0
- data/documentation/transaction_output.md +130 -0
- data/documentation/wif.md +72 -0
- data/documentation/wire_format.md +70 -0
- data/lib/btcruby/address.rb +296 -0
- data/lib/btcruby/base58.rb +108 -0
- data/lib/btcruby/big_number.rb +47 -0
- data/lib/btcruby/block.rb +170 -0
- data/lib/btcruby/block_header.rb +231 -0
- data/lib/btcruby/constants.rb +59 -0
- data/lib/btcruby/currency_formatter.rb +64 -0
- data/lib/btcruby/data.rb +98 -0
- data/lib/btcruby/diagnostics.rb +92 -0
- data/lib/btcruby/errors.rb +8 -0
- data/lib/btcruby/extensions.rb +65 -0
- data/lib/btcruby/hash_functions.rb +54 -0
- data/lib/btcruby/hash_id.rb +18 -0
- data/lib/btcruby/key.rb +517 -0
- data/lib/btcruby/keychain.rb +464 -0
- data/lib/btcruby/network.rb +73 -0
- data/lib/btcruby/opcode.rb +197 -0
- data/lib/btcruby/open_assets/asset.rb +35 -0
- data/lib/btcruby/open_assets/asset_address.rb +49 -0
- data/lib/btcruby/open_assets/asset_definition.rb +75 -0
- data/lib/btcruby/open_assets/asset_id.rb +24 -0
- data/lib/btcruby/open_assets/asset_marker.rb +94 -0
- data/lib/btcruby/open_assets/asset_processor.rb +377 -0
- data/lib/btcruby/open_assets/asset_transaction.rb +184 -0
- data/lib/btcruby/open_assets/asset_transaction_builder/errors.rb +15 -0
- data/lib/btcruby/open_assets/asset_transaction_builder/provider.rb +32 -0
- data/lib/btcruby/open_assets/asset_transaction_builder/result.rb +47 -0
- data/lib/btcruby/open_assets/asset_transaction_builder.rb +418 -0
- data/lib/btcruby/open_assets/asset_transaction_input.rb +64 -0
- data/lib/btcruby/open_assets/asset_transaction_output.rb +140 -0
- data/lib/btcruby/open_assets.rb +26 -0
- data/lib/btcruby/openssl.rb +536 -0
- data/lib/btcruby/proof_of_work.rb +110 -0
- data/lib/btcruby/safety.rb +26 -0
- data/lib/btcruby/script.rb +733 -0
- data/lib/btcruby/signature_hashtype.rb +37 -0
- data/lib/btcruby/transaction.rb +511 -0
- data/lib/btcruby/transaction_builder/errors.rb +15 -0
- data/lib/btcruby/transaction_builder/provider.rb +54 -0
- data/lib/btcruby/transaction_builder/result.rb +73 -0
- data/lib/btcruby/transaction_builder/signer.rb +28 -0
- data/lib/btcruby/transaction_builder.rb +520 -0
- data/lib/btcruby/transaction_input.rb +298 -0
- data/lib/btcruby/transaction_outpoint.rb +30 -0
- data/lib/btcruby/transaction_output.rb +315 -0
- data/lib/btcruby/version.rb +3 -0
- data/lib/btcruby/wif.rb +118 -0
- data/lib/btcruby/wire_format.rb +362 -0
- data/lib/btcruby.rb +44 -2
- data/sample_code/creating_a_p2sh_multisig_address.rb +21 -0
- data/sample_code/creating_a_transaction_manually.rb +44 -0
- data/sample_code/generating_an_address.rb +20 -0
- data/sample_code/using_transaction_builder.rb +49 -0
- data/spec/address_spec.rb +206 -0
- data/spec/all.rb +6 -0
- data/spec/base58_spec.rb +83 -0
- data/spec/block_header_spec.rb +18 -0
- data/spec/block_spec.rb +18 -0
- data/spec/currency_formatter_spec.rb +46 -0
- data/spec/data_spec.rb +50 -0
- data/spec/diagnostics_spec.rb +41 -0
- data/spec/key_spec.rb +205 -0
- data/spec/keychain_spec.rb +261 -0
- data/spec/network_spec.rb +48 -0
- data/spec/open_assets/asset_address_spec.rb +33 -0
- data/spec/open_assets/asset_id_spec.rb +15 -0
- data/spec/open_assets/asset_marker_spec.rb +47 -0
- data/spec/open_assets/asset_processor_spec.rb +567 -0
- data/spec/open_assets/asset_transaction_builder_spec.rb +273 -0
- data/spec/open_assets/asset_transaction_spec.rb +70 -0
- data/spec/proof_of_work_spec.rb +53 -0
- data/spec/script_spec.rb +66 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/transaction_builder_spec.rb +338 -0
- data/spec/transaction_spec.rb +162 -0
- data/spec/wire_format_spec.rb +283 -0
- metadata +141 -7
@@ -0,0 +1,733 @@
|
|
1
|
+
module BTC
|
2
|
+
class Script
|
3
|
+
|
4
|
+
# Serialized binary form of the script (payload)
|
5
|
+
attr_reader :data
|
6
|
+
|
7
|
+
# List of public keys if it is a multisig script.
|
8
|
+
# If it is not a multisig script, returns nil.
|
9
|
+
# See also #multisig_script?
|
10
|
+
attr_reader :multisig_public_keys
|
11
|
+
|
12
|
+
# Number of signatures required if it is a multisig script.
|
13
|
+
# If it is not a multisig script, returns nil.
|
14
|
+
# See also #multisig_script?
|
15
|
+
attr_reader :multisig_signatures_required
|
16
|
+
|
17
|
+
def initialize(hex: nil, # raw script data in hex
|
18
|
+
data: nil, # raw script data in binary
|
19
|
+
op_return: nil, # binary string for OP_RETURN script (or array of binary string)
|
20
|
+
public_keys: nil, signatures_required: nil # multisig
|
21
|
+
)
|
22
|
+
if data || hex
|
23
|
+
data ||= BTC::Data.data_from_hex(hex)
|
24
|
+
data = BTC::Data.ensure_binary_encoding(data)
|
25
|
+
@chunks = []
|
26
|
+
offset = 0
|
27
|
+
while offset < data.bytesize
|
28
|
+
chunk = Chunk.with_data(data, offset: offset)
|
29
|
+
if !chunk.canonical?
|
30
|
+
Diagnostics.current.add_message("BTC::Script: decoded non-canonical chunk at offset #{offset}: #{chunk.to_s}")
|
31
|
+
end
|
32
|
+
offset += chunk.size
|
33
|
+
@chunks << chunk
|
34
|
+
end
|
35
|
+
elsif op_return
|
36
|
+
@chunks = []
|
37
|
+
self << OP_RETURN << op_return
|
38
|
+
elsif public_keys || signatures_required
|
39
|
+
if !public_keys || public_keys.size < 1
|
40
|
+
raise ArgumentError, "Public keys must be an array of at least 1 pubkey"
|
41
|
+
end
|
42
|
+
if !signatures_required || signatures_required < 1
|
43
|
+
raise ArgumentError, "Number of required signatures must be greater than zero"
|
44
|
+
end
|
45
|
+
if signatures_required > public_keys.size
|
46
|
+
raise ArgumentError, "Number of required signatures must not exceed number of pubkeys."
|
47
|
+
end
|
48
|
+
if public_keys.size > 16
|
49
|
+
raise ArgumentError, "Maximum number of public keys exceeded (16)"
|
50
|
+
end
|
51
|
+
|
52
|
+
n_opcode = Opcode.opcode_for_small_integer(public_keys.size)
|
53
|
+
m_opcode = Opcode.opcode_for_small_integer(signatures_required)
|
54
|
+
|
55
|
+
@chunks = []
|
56
|
+
self << m_opcode << public_keys << n_opcode << OP_CHECKMULTISIG
|
57
|
+
else
|
58
|
+
# Empty script
|
59
|
+
@chunks = []
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Initializes a script with its binary representation.
|
64
|
+
# Returns nil if data is not a well-formed script.
|
65
|
+
def self.with_data(data)
|
66
|
+
raise ArgumentError, "Use Script.new(data: ...) instead"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Initializes a script of form "OP_RETURN <data>"
|
70
|
+
def self.op_return_script(data)
|
71
|
+
raise ArgumentError, "Use Script.new(op_return:...) instead"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Initializes a multisignature script "OP_<M> <pubkey1> ... <pubkeyN> OP_<N> OP_CHECKMULTISIG"
|
75
|
+
# N must be >= M, M and N should be from 1 to 16.
|
76
|
+
# If you need a more customized transaction with OP_CHECKMULTISIG, create it using other methods.
|
77
|
+
# `public_keys` is an array of binary strings.
|
78
|
+
def self.multisig(public_keys: [], signatures_required: 0)
|
79
|
+
self.new(public_keys: public_keys, signatures_required: signatures_required)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Representation and conversion
|
84
|
+
# -----------------------------
|
85
|
+
|
86
|
+
# Returns true if this is a standard script
|
87
|
+
# As of September 2014, standard = public_key_hash_script? ||
|
88
|
+
# p2sh_script? ||
|
89
|
+
# standard_multisig_script? ||
|
90
|
+
# public_key_script? ||
|
91
|
+
# standard_op_return_script?
|
92
|
+
def standard?
|
93
|
+
public_key_hash_script? ||
|
94
|
+
script_hash_script? ||
|
95
|
+
standard_multisig_script? ||
|
96
|
+
public_key_script? ||
|
97
|
+
standard_op_return_script?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns true if the script is a pay-to-pubkey script:
|
101
|
+
# "<pubkey> OP_CHECKSIG"
|
102
|
+
def public_key_script?
|
103
|
+
return false if @chunks.size != 2
|
104
|
+
return @chunks[0].pushdata? &&
|
105
|
+
@chunks[0].pushdata.size >= 33 &&
|
106
|
+
@chunks[1].opcode == OP_CHECKSIG
|
107
|
+
end
|
108
|
+
|
109
|
+
def p2pk?
|
110
|
+
return public_key_script?
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns a raw public key if this script is public_key_script?
|
114
|
+
def public_key
|
115
|
+
@chunks[0] && @chunks[0].pushdata
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns true if the script is a P2PKH script:
|
119
|
+
# "OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG"
|
120
|
+
def public_key_hash_script?
|
121
|
+
return false if @chunks.size != 5
|
122
|
+
return @chunks[0].opcode == OP_DUP &&
|
123
|
+
@chunks[1].opcode == OP_HASH160 &&
|
124
|
+
@chunks[2].size == 21 &&
|
125
|
+
@chunks[3].opcode == OP_EQUALVERIFY &&
|
126
|
+
@chunks[4].opcode == OP_CHECKSIG
|
127
|
+
end
|
128
|
+
|
129
|
+
def p2pkh?
|
130
|
+
return public_key_hash_script?
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns public key hash if this script is public_key_hash_script?
|
134
|
+
def public_key_hash
|
135
|
+
@chunks[2] && @chunks[2].pushdata
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns true if the script is a P2SH script:
|
139
|
+
# "OP_HASH160 <20-byte hash> OP_EQUAL"
|
140
|
+
def script_hash_script?
|
141
|
+
return false if @chunks.size != 3
|
142
|
+
return @chunks[0].opcode == OP_HASH160 &&
|
143
|
+
@chunks[1].size == 21 &&
|
144
|
+
@chunks[2].opcode == OP_EQUAL
|
145
|
+
end
|
146
|
+
|
147
|
+
def p2sh?
|
148
|
+
return script_hash_script?
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns p2sh hash if this script is script_hash_script?
|
152
|
+
def script_hash
|
153
|
+
@chunks[1] && @chunks[1].pushdata
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns true if the script is "OP_<M> <pubkey1> ... <pubkeyN> OP_<N> OP_CHECKMULTISIG"
|
157
|
+
# where N is up to 15.
|
158
|
+
# Scripts with up to 15 signatures are considered standard and relayed quickly,
|
159
|
+
# but you are allowed to create more complex ones.
|
160
|
+
def standard_multisig_script?
|
161
|
+
return false if !multisig_script?
|
162
|
+
|
163
|
+
# Check chunks directly so we make sure OP_<N> are used, not pushdata.
|
164
|
+
# Bitcoin allows encoding multisig N and M parameters as pushdata
|
165
|
+
# (which will be interpreted as a little-endian bignum)
|
166
|
+
m_opcode = @chunks[0].opcode
|
167
|
+
n_opcode = @chunks[-2].opcode
|
168
|
+
if n_opcode >= OP_1 && n_opcode <= OP_15
|
169
|
+
if m_opcode >= OP_1 && m_opcode <= n_opcode
|
170
|
+
return true
|
171
|
+
end
|
172
|
+
end
|
173
|
+
return false
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns true if the script is in form "<M> <pubkey1> ... <pubkeyN> <N> OP_CHECKMULTISIG"
|
177
|
+
def multisig_script?
|
178
|
+
detect_multisig_if_needed
|
179
|
+
@is_multisig
|
180
|
+
end
|
181
|
+
|
182
|
+
# List of public keys if it is a multisig script.
|
183
|
+
# If it is not a multisig script, returns nil.
|
184
|
+
# See also #multisig_script?
|
185
|
+
def multisig_public_keys
|
186
|
+
detect_multisig_if_needed
|
187
|
+
@multisig_public_keys
|
188
|
+
end
|
189
|
+
|
190
|
+
# Number of signatures required if it is a multisig script.
|
191
|
+
# If it is not a multisig script, returns nil.
|
192
|
+
# See also #multisig_script?
|
193
|
+
def multisig_signatures_required
|
194
|
+
detect_multisig_if_needed
|
195
|
+
@multisig_signatures_required
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns true if this script is a 'OP_RETURN <data>' script and
|
199
|
+
# data size is within 40 bytes.
|
200
|
+
def standard_op_return_script?
|
201
|
+
retun false if !op_return_script? || @chunks.size != 2
|
202
|
+
@chunks[1].pushdata.bytesize <= 40
|
203
|
+
end
|
204
|
+
|
205
|
+
# Returns true if this script is of form 'OP_RETURN <data>'
|
206
|
+
def op_return_script?
|
207
|
+
return @chunks.size >= 2 &&
|
208
|
+
@chunks[0].opcode == OP_RETURN &&
|
209
|
+
@chunks[1..-1].all?{|c| c.pushdata? }
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns first data chunk if this script is 'OP_RETURN <data>'.
|
213
|
+
# Otherwise returns nil.
|
214
|
+
def op_return_data
|
215
|
+
return nil if !op_return_script?
|
216
|
+
return @chunks[1].pushdata
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns all data chunks if this script is 'OP_RETURN <data> [<data>...]'
|
220
|
+
# Most commonly returned array contains one binary string.
|
221
|
+
def op_return_items
|
222
|
+
return nil if !op_return_script?
|
223
|
+
return @chunks[1, @chunks.size-1].map{|c| c.pushdata}
|
224
|
+
end
|
225
|
+
|
226
|
+
# Returns `true` if this script may be a valid OpenAssets marker.
|
227
|
+
# Only checks the prefix and minimal length, does not validate the content.
|
228
|
+
# Use this method to quickly filter out non-asset transactions.
|
229
|
+
def open_assets_marker?
|
230
|
+
return false if !op_return_script?
|
231
|
+
data = op_return_data
|
232
|
+
return false if !data || data.bytesize < 6
|
233
|
+
if data[0, AssetMarker::PREFIX_V1.bytesize] == AssetMarker::PREFIX_V1
|
234
|
+
return true
|
235
|
+
end
|
236
|
+
false
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns pushdata if script starts with <pushdata> OP_DROP
|
240
|
+
# Returns nil if the script starts with some other opcodes or shorter than 2 opcodes.
|
241
|
+
def dropped_prefix_data
|
242
|
+
if @chunks.size >= 2 && @chunks[0].pushdata? && @chunks[1].opcode == OP_DROP
|
243
|
+
return @chunks[0].pushdata
|
244
|
+
end
|
245
|
+
nil
|
246
|
+
end
|
247
|
+
|
248
|
+
# If script starts with `<pushdata> OP_DROP`, these two opcodes are removed
|
249
|
+
# and a new script instance is returned.
|
250
|
+
def without_dropped_prefix_data
|
251
|
+
if dropped_prefix_data
|
252
|
+
return Script.new << @chunks[2..-1]
|
253
|
+
end
|
254
|
+
self
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns true if the script consists of push data operations only
|
258
|
+
# (including OP_<N>). Aka isPushOnly in BitcoinQT.
|
259
|
+
# Used in BIP16 (P2SH) implementation.
|
260
|
+
def data_only?
|
261
|
+
# Include PUSHDATA ops and OP_0..OP_16 literals.
|
262
|
+
@chunks.each do |chunk|
|
263
|
+
return false if chunk.opcode > OP_16
|
264
|
+
end
|
265
|
+
return true
|
266
|
+
end
|
267
|
+
|
268
|
+
# Serialized binary form of the script (payload)
|
269
|
+
def data
|
270
|
+
@chunks.inject("".b){|buf,c| buf << c.raw_data}
|
271
|
+
end
|
272
|
+
|
273
|
+
# Human-readable textual representation of the script
|
274
|
+
# (e.g. "OP_DUP OP_HASH160 5a73e920b7836c74f9e740a5bb885e8580557038 OP_EQUALVERIFY OP_CHECKSIG")
|
275
|
+
def to_s
|
276
|
+
@chunks.map{|c| c.to_s }.join(" ")
|
277
|
+
end
|
278
|
+
|
279
|
+
def to_hex
|
280
|
+
BTC::Data.hex_from_data(self.data)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Returns an array of opcodes or pushdata strings.
|
284
|
+
# Integers are opcodes, strings are pushdata binary strings.
|
285
|
+
# OP_0 is treated as a zero-length pushdata.
|
286
|
+
def to_a
|
287
|
+
@chunks.map{|c| c.pushdata? ? c.pushdata : c.opcode }
|
288
|
+
end
|
289
|
+
|
290
|
+
# Complete copy of a script.
|
291
|
+
def dup
|
292
|
+
BTC::Script.new(data: self.data)
|
293
|
+
end
|
294
|
+
|
295
|
+
def ==(other)
|
296
|
+
return false if other == nil
|
297
|
+
self.data == other.data
|
298
|
+
end
|
299
|
+
alias_method :eql?, :==
|
300
|
+
|
301
|
+
def inspect
|
302
|
+
%{#<#{self.class.name} #{to_s.inspect} (#{self.data.bytesize} bytes)>}
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
|
307
|
+
# Conversion
|
308
|
+
# ----------
|
309
|
+
|
310
|
+
|
311
|
+
# Returns BTC::PublicKeyAddress or BTC::ScriptHashAddress if
|
312
|
+
# the script is a standard output script for these addresses.
|
313
|
+
# If the script is something different, returns nil.
|
314
|
+
def standard_address(network: nil)
|
315
|
+
if public_key_hash_script?
|
316
|
+
return BTC::PublicKeyAddress.new(hash: @chunks[2].pushdata, network: network)
|
317
|
+
elsif script_hash_script?
|
318
|
+
return BTC::ScriptHashAddress.new(hash: @chunks[1].pushdata, network: network)
|
319
|
+
elsif public_key_script?
|
320
|
+
return BTC::PublicKeyAddress.new(hash: BTC.hash160(@chunks[0].pushdata), network: network)
|
321
|
+
end
|
322
|
+
nil
|
323
|
+
end
|
324
|
+
|
325
|
+
# Wraps the recipient into an output P2SH script
|
326
|
+
# (OP_HASH160 <20-byte hash of the recipient> OP_EQUAL).
|
327
|
+
def p2sh_script
|
328
|
+
Script.new << OP_HASH160 << BTC.hash160(self.data) << OP_EQUAL
|
329
|
+
end
|
330
|
+
|
331
|
+
|
332
|
+
# Returns a dummy script matching this script on the input with
|
333
|
+
# the same size as an intended signature script.
|
334
|
+
# Only a few standard script types are supported.
|
335
|
+
# Set `strict` to false to allow imprecise guess for P2SH script.
|
336
|
+
# Returns nil if could not determine a matching script.
|
337
|
+
def simulated_signature_script(strict: true)
|
338
|
+
if public_key_hash_script?
|
339
|
+
# assuming non-compressed pubkeys to be conservative
|
340
|
+
return Script.new << Script.simulated_signature(hashtype: SIGHASH_ALL) << Script.simulated_uncompressed_pubkey
|
341
|
+
|
342
|
+
elsif public_key_script?
|
343
|
+
return Script.new << Script.simulated_signature(hashtype: SIGHASH_ALL)
|
344
|
+
|
345
|
+
elsif script_hash_script? && !strict
|
346
|
+
# This is a wild approximation, but works well if most p2sh scripts are 2-of-3 multisig scripts.
|
347
|
+
# If you have a very particular smart contract scheme you should not use TransactionBuilder which estimates fees this way.
|
348
|
+
return Script.new << OP_0 << [Script.simulated_signature(hashtype: SIGHASH_ALL)]*2 << Script.simulated_multisig_script(2,3).data
|
349
|
+
|
350
|
+
elsif multisig_script?
|
351
|
+
return Script.new << OP_0 << [Script.simulated_signature(hashtype: SIGHASH_ALL)]*self.multisig_signatures_required
|
352
|
+
else
|
353
|
+
return nil
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# Returns a simulated signature with an optional hashtype byte attached
|
358
|
+
def self.simulated_signature(hashtype: nil)
|
359
|
+
"\x30" + "\xff"*71 + (hashtype ? WireFormat.encode_uint8(hashtype) : "")
|
360
|
+
end
|
361
|
+
|
362
|
+
# Returns a dummy uncompressed pubkey (65 bytes).
|
363
|
+
def self.simulated_uncompressed_pubkey
|
364
|
+
"\x04" + "\xff"*64
|
365
|
+
end
|
366
|
+
|
367
|
+
# Returns a dummy compressed pubkey (33 bytes).
|
368
|
+
def self.simulated_compressed_pubkey
|
369
|
+
"\x02" + "\xff"*32
|
370
|
+
end
|
371
|
+
|
372
|
+
# Returns a dummy script that simulates m-of-n multisig script
|
373
|
+
def self.simulated_multisig_script(m,n)
|
374
|
+
Script.new <<
|
375
|
+
Opcode.opcode_for_small_integer(m) <<
|
376
|
+
[simulated_uncompressed_pubkey]*n << # assuming non-compressed pubkeys to be conservative
|
377
|
+
Opcode.opcode_for_small_integer(n) <<
|
378
|
+
OP_CHECKMULTISIG
|
379
|
+
end
|
380
|
+
|
381
|
+
|
382
|
+
# Modification
|
383
|
+
# ------------
|
384
|
+
|
385
|
+
# Appends a non-pushdata opcode to the script.
|
386
|
+
def append_opcode(opcode)
|
387
|
+
raise ArgumentError, "Invalid opcode value." if opcode > 0xff || opcode < 0
|
388
|
+
if opcode > 0 && opcode <= OP_PUSHDATA4
|
389
|
+
raise ArgumentError, "Cannot add pushdata opcode without data"
|
390
|
+
end
|
391
|
+
@chunks << Chunk.new(opcode.chr)
|
392
|
+
return self
|
393
|
+
end
|
394
|
+
|
395
|
+
# Appends a pushdata opcode with the most compact encoding.
|
396
|
+
# Optional opcode may be equal to OP_PUSHDATA1, OP_PUSHDATA2, or OP_PUSHDATA4.
|
397
|
+
# ArgumentError is raised if opcode does not represent a given data length.
|
398
|
+
def append_pushdata(pushdata, opcode: nil)
|
399
|
+
raise ArgumentError, "No pushdata is given" if !pushdata
|
400
|
+
encoded_pushdata = self.class.encode_pushdata(pushdata, opcode: opcode)
|
401
|
+
if !encoded_pushdata
|
402
|
+
raise ArgumentError, "Cannot encode pushdata with opcode #{opcode}"
|
403
|
+
end
|
404
|
+
@chunks << Chunk.new(encoded_pushdata)
|
405
|
+
return self
|
406
|
+
end
|
407
|
+
|
408
|
+
# Removes all occurences of opcode. Typically it's OP_CODESEPARATOR.
|
409
|
+
def delete_opcode(opcode)
|
410
|
+
@chunks = @chunks.inject([]) do |list, chunk|
|
411
|
+
list << chunk if chunk.opcode != opcode
|
412
|
+
list
|
413
|
+
end
|
414
|
+
return self
|
415
|
+
end
|
416
|
+
|
417
|
+
# Removes all occurences of a given pushdata.
|
418
|
+
def delete_pushdata(pushdata)
|
419
|
+
@chunks = @chunks.inject([]) do |list, chunk|
|
420
|
+
list << chunk if chunk.pushdata != pushdata
|
421
|
+
list
|
422
|
+
end
|
423
|
+
return self
|
424
|
+
end
|
425
|
+
|
426
|
+
# Appends script to the current script.
|
427
|
+
def append_script(script)
|
428
|
+
raise ArgumentError, "No script is given" if !script
|
429
|
+
@chunks += script.chunks
|
430
|
+
return self
|
431
|
+
end
|
432
|
+
|
433
|
+
# Appends an opcode (Integer), pushdata (String) or Script and returns self.
|
434
|
+
# If Array is passed, this method is recursively called for each element in the array.
|
435
|
+
def <<(object)
|
436
|
+
if object.is_a?(BTC::Script)
|
437
|
+
append_script(object)
|
438
|
+
elsif object.is_a?(Integer)
|
439
|
+
append_opcode(object)
|
440
|
+
elsif object.is_a?(String)
|
441
|
+
append_pushdata(object.b)
|
442
|
+
elsif object.is_a?(Array)
|
443
|
+
object.each do |element|
|
444
|
+
self << element
|
445
|
+
end
|
446
|
+
elsif object.is_a?(Chunk)
|
447
|
+
if object.pushdata?
|
448
|
+
append_pushdata(object.pushdata)
|
449
|
+
else
|
450
|
+
append_opcode(object.opcode)
|
451
|
+
end
|
452
|
+
else
|
453
|
+
raise ArgumentError, "Operand must be an integer, a string a BTC::Script instance or an array of any of those."
|
454
|
+
end
|
455
|
+
return self
|
456
|
+
end
|
457
|
+
|
458
|
+
# Returns a new instance with concatenation of two scripts.
|
459
|
+
def +(other)
|
460
|
+
self.dup << other
|
461
|
+
end
|
462
|
+
|
463
|
+
|
464
|
+
|
465
|
+
|
466
|
+
|
467
|
+
|
468
|
+
# Private API
|
469
|
+
# -----------
|
470
|
+
|
471
|
+
attr_reader :chunks
|
472
|
+
|
473
|
+
# If opcode is nil, then the most compact encoding will be chosen.
|
474
|
+
# Returns nil if opcode can't be used for data, or data is nil or too big.
|
475
|
+
def self.encode_pushdata(pushdata, opcode: nil)
|
476
|
+
raise ArgumentError, "Pushdata is missing" if !pushdata
|
477
|
+
if pushdata.bytesize < OP_PUSHDATA1 && opcode == nil
|
478
|
+
return BTC::WireFormat.encode_uint8(pushdata.bytesize) + pushdata
|
479
|
+
elsif pushdata.bytesize < 0xff && (opcode == nil || opcode == OP_PUSHDATA1)
|
480
|
+
return BTC::WireFormat.encode_uint8(OP_PUSHDATA1) +
|
481
|
+
BTC::WireFormat.encode_uint8(pushdata.bytesize) +
|
482
|
+
pushdata
|
483
|
+
elsif pushdata.bytesize < 0xffff && (opcode == nil || opcode == OP_PUSHDATA2)
|
484
|
+
return BTC::WireFormat.encode_uint8(OP_PUSHDATA2) +
|
485
|
+
BTC::WireFormat.encode_uint16le(pushdata.bytesize) +
|
486
|
+
pushdata
|
487
|
+
elsif pushdata.bytesize < 0xffffffff && (opcode == nil || opcode == OP_PUSHDATA4)
|
488
|
+
return BTC::WireFormat.encode_uint8(OP_PUSHDATA4) +
|
489
|
+
BTC::WireFormat.encode_uint32le(pushdata.bytesize) +
|
490
|
+
pushdata
|
491
|
+
else
|
492
|
+
raise ArgumentError, "Invalid opcode or data is too big"
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
def detect_multisig_if_needed
|
497
|
+
return if @is_multisig != nil
|
498
|
+
@is_multisig = detect_multisig
|
499
|
+
end
|
500
|
+
|
501
|
+
def detect_multisig
|
502
|
+
# multisig script must have at least 4 ops ("OP_1 <pubkey> OP_1 OP_CHECKMULTISIG")
|
503
|
+
return false if @chunks.size < 4
|
504
|
+
return false if @chunks.last.opcode != OP_CHECKMULTISIG
|
505
|
+
|
506
|
+
m_chunk = @chunks[0]
|
507
|
+
n_chunk = @chunks[-2]
|
508
|
+
m_opcode = m_chunk.opcode
|
509
|
+
n_opcode = n_chunk.opcode
|
510
|
+
|
511
|
+
m = Opcode.small_integer_from_opcode(m_opcode)
|
512
|
+
n = Opcode.small_integer_from_opcode(n_opcode)
|
513
|
+
|
514
|
+
# If m or n is not OP_<int>, but a pushdata with little-endian bignum.
|
515
|
+
if !m
|
516
|
+
return false if !m_chunk.pushdata?
|
517
|
+
m = BTC::BigNumber.new(signed_little_endian: m_chunk.pushdata).integer
|
518
|
+
end
|
519
|
+
|
520
|
+
if !n
|
521
|
+
return false if !n_chunk.pushdata?
|
522
|
+
n = BTC::BigNumber.new(signed_little_endian: n_chunk.pushdata).integer
|
523
|
+
end
|
524
|
+
|
525
|
+
return false if m < 1 || n < 1 || m > n
|
526
|
+
|
527
|
+
# We must have correct number of pubkeys in the script. 3 extra ops: OP_<M>, OP_<N> and OP_CHECKMULTISIG
|
528
|
+
return false if @chunks.size != (3 + n)
|
529
|
+
|
530
|
+
pubkeys = []
|
531
|
+
@chunks[1, n].each do |chunk|
|
532
|
+
return false if !chunk.pushdata? || chunk.pushdata.bytesize == 0
|
533
|
+
pubkeys << chunk.pushdata
|
534
|
+
end
|
535
|
+
|
536
|
+
# Now we extracted all pubkeys and verified the numbers.
|
537
|
+
@multisig_public_keys = pubkeys
|
538
|
+
@multisig_signatures_required = m
|
539
|
+
return true
|
540
|
+
end
|
541
|
+
|
542
|
+
# Script::Chunk represents either an opcode or a pushdata command.
|
543
|
+
class Chunk
|
544
|
+
# Raw data for this chunk.
|
545
|
+
# 1 byte for regular opcode, 1 or more bytes for pushdata command.
|
546
|
+
# We do not call it 'data' to avoid confusion with `pushdata` (see below).
|
547
|
+
# The encoding is guaranteed to be binary.
|
548
|
+
attr_reader :raw_data
|
549
|
+
|
550
|
+
# Opcode for this chunk (first byte of the raw_data).
|
551
|
+
attr_reader :opcode
|
552
|
+
|
553
|
+
# If opcode is OP_PUSHDATA*, contains pure data being pushed.
|
554
|
+
# If opcode is OP_0, returns an empty string.
|
555
|
+
# If opcode is not pushdata, returns nil.
|
556
|
+
attr_reader :pushdata
|
557
|
+
|
558
|
+
# Length of raw_data in bytes.
|
559
|
+
attr_reader :size
|
560
|
+
alias :length :size
|
561
|
+
|
562
|
+
# Returns true if this is a non-pushdata (also not OP_0) opcode.
|
563
|
+
def opcode?
|
564
|
+
!pushdata?
|
565
|
+
end
|
566
|
+
|
567
|
+
# Returns true if this is a pushdata chunk (or OP_0 opcode).
|
568
|
+
def pushdata?
|
569
|
+
# Compact pushdata opcodes are "virtual", just length prefixes.
|
570
|
+
# Attention: OP_0 is also "pushdata" code that pushes empty data.
|
571
|
+
self.opcode <= OP_PUSHDATA4
|
572
|
+
end
|
573
|
+
|
574
|
+
# Returns true if this chunk is in canonical form (the most compact one).
|
575
|
+
# Returns false if it contains pushdata with too big length prefix.
|
576
|
+
# Example of non-canonical chunk: 75 bytes pushed with OP_PUSHDATA1 instead
|
577
|
+
# of simple 0x4b prefix.
|
578
|
+
def canonical?
|
579
|
+
opcode = self.opcode
|
580
|
+
if opcode < OP_PUSHDATA1
|
581
|
+
return true # most compact pushdata is always canonical.
|
582
|
+
elsif opcode == OP_PUSHDATA1
|
583
|
+
return (self.raw_data.bytesize - (1+1)) >= OP_PUSHDATA1
|
584
|
+
elsif opcode == OP_PUSHDATA2
|
585
|
+
return (self.raw_data.bytesize - (1+2)) > 0xff
|
586
|
+
elsif opcode == OP_PUSHDATA4
|
587
|
+
return (self.raw_data.bytesize - (1+4)) > 0xffff
|
588
|
+
else
|
589
|
+
return true # all other opcodes are canonical (just 1 byte code)
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
def opcode
|
594
|
+
# raises StopIteration if raw_data is empty,
|
595
|
+
# but we don't allow empty raw_data for chunks.
|
596
|
+
raw_data.each_byte.next
|
597
|
+
end
|
598
|
+
|
599
|
+
def pushdata
|
600
|
+
return nil if !pushdata?
|
601
|
+
opcode = self.opcode
|
602
|
+
offset = 1 # by default, opcode is just a length prefix.
|
603
|
+
if opcode == OP_PUSHDATA1
|
604
|
+
offset += 1
|
605
|
+
elsif opcode == OP_PUSHDATA2
|
606
|
+
offset += 2
|
607
|
+
elsif opcode == OP_PUSHDATA4
|
608
|
+
offset += 4
|
609
|
+
end
|
610
|
+
self.raw_data[offset, self.raw_data.size - offset]
|
611
|
+
end
|
612
|
+
|
613
|
+
def size
|
614
|
+
self.raw_data.bytesize
|
615
|
+
end
|
616
|
+
|
617
|
+
def to_s
|
618
|
+
opcode = self.opcode
|
619
|
+
|
620
|
+
if self.opcode?
|
621
|
+
return "OP_0" if opcode == OP_0
|
622
|
+
return "OP_1NEGATE" if opcode == OP_1NEGATE
|
623
|
+
if opcode >= OP_1 && opcode <= OP_16
|
624
|
+
return "OP_#{opcode + 1 - OP_1}"
|
625
|
+
end
|
626
|
+
return Opcode.name_for_opcode(opcode)
|
627
|
+
end
|
628
|
+
|
629
|
+
pushdata = self.pushdata
|
630
|
+
return "OP_0" if pushdata.bytesize == 0 # Empty data is encoded as OP_0.
|
631
|
+
|
632
|
+
string = ""
|
633
|
+
|
634
|
+
# If it's some weird readable string, show it as a readable string.
|
635
|
+
if data_is_ascii_printable?(pushdata) && (pushdata.bytesize < 20 || pushdata.bytesize > 65)
|
636
|
+
string = pushdata.encode(Encoding::ASCII)
|
637
|
+
# Escape escapes & single quote characters.
|
638
|
+
string.gsub!("\\", "\\\\")
|
639
|
+
string.gsub!("'", "\\'")
|
640
|
+
# Wrap in single quotes. Why not double? Because they are already used in JSON and we don't want to multiply the mess.
|
641
|
+
string = "'#{string}'"
|
642
|
+
else
|
643
|
+
string = BTC::Data.hex_from_data(pushdata)
|
644
|
+
# Shorter than 128-bit chunks are wrapped in square brackets to avoid ambiguity with big all-decimal numbers.
|
645
|
+
if (pushdata.bytesize < 16)
|
646
|
+
string = "[#{string}]"
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
# Pushdata with non-compact encoding will have explicit length prefix (1 for OP_PUSHDATA1, 2 for OP_PUSHDATA2 and 4 for OP_PUSHDATA4).
|
651
|
+
if !canonical?
|
652
|
+
prefix = 1
|
653
|
+
prefix = 2 if opcode == OP_PUSHDATA2
|
654
|
+
prefix = 4 if opcode == OP_PUSHDATA4
|
655
|
+
string = "#{prefix}:#{string}"
|
656
|
+
end
|
657
|
+
|
658
|
+
return string
|
659
|
+
end
|
660
|
+
|
661
|
+
# Parses the chunk with binary data. Assumes the encoding is binary.
|
662
|
+
def self.with_data(data, offset: 0)
|
663
|
+
raise ArgumentError, "Data is missing" if !data
|
664
|
+
|
665
|
+
opcode, _ = BTC::WireFormat.read_uint8(data: data, offset: offset)
|
666
|
+
|
667
|
+
raise ArgumentError, "Failed to read opcode of the script chunk" if !opcode
|
668
|
+
|
669
|
+
# push data opcode
|
670
|
+
if opcode <= OP_PUSHDATA4
|
671
|
+
|
672
|
+
length = data.bytesize
|
673
|
+
|
674
|
+
if opcode < OP_PUSHDATA1
|
675
|
+
pushdata_length = opcode
|
676
|
+
chunk_length = 1 + pushdata_length
|
677
|
+
if offset + chunk_length > length
|
678
|
+
raise ArgumentError, "PUSHDATA is longer than we have bytes available"
|
679
|
+
end
|
680
|
+
return self.new(data[offset, chunk_length])
|
681
|
+
elsif opcode == OP_PUSHDATA1
|
682
|
+
pushdata_length, _ = BTC::WireFormat.read_uint8(data: data, offset: offset + 1)
|
683
|
+
if !pushdata_length
|
684
|
+
raise ArgumentError, "Failed to read length for PUSHDATA1"
|
685
|
+
end
|
686
|
+
chunk_length = 1 + 1 + pushdata_length
|
687
|
+
if offset + chunk_length > length
|
688
|
+
raise ArgumentError, "PUSHDATA1 is longer than we have bytes available"
|
689
|
+
end
|
690
|
+
return self.new(data[offset, chunk_length])
|
691
|
+
elsif (opcode == OP_PUSHDATA2)
|
692
|
+
pushdata_length, _ = BTC::WireFormat.read_uint16le(data: data, offset: offset + 1)
|
693
|
+
if !pushdata_length
|
694
|
+
raise ArgumentError, "Failed to read length for PUSHDATA2"
|
695
|
+
end
|
696
|
+
chunk_length = 1 + 2 + pushdata_length
|
697
|
+
if offset + chunk_length > length
|
698
|
+
raise ArgumentError, "PUSHDATA2 is longer than we have bytes available"
|
699
|
+
end
|
700
|
+
return self.new(data[offset, chunk_length])
|
701
|
+
elsif (opcode == OP_PUSHDATA4)
|
702
|
+
pushdata_length, _ = BTC::WireFormat.read_uint32le(data: data, offset: offset + 1)
|
703
|
+
if !pushdata_length
|
704
|
+
raise ArgumentError, "Failed to read length for PUSHDATA4"
|
705
|
+
end
|
706
|
+
chunk_length = 1 + 4 + pushdata_length
|
707
|
+
if offset + chunk_length > length
|
708
|
+
raise ArgumentError, "PUSHDATA4 is longer than we have bytes available"
|
709
|
+
end
|
710
|
+
return self.new(data[offset, chunk_length])
|
711
|
+
end
|
712
|
+
else
|
713
|
+
# simple opcode - 1 byte
|
714
|
+
return self.new(data[offset, 1])
|
715
|
+
end
|
716
|
+
end
|
717
|
+
|
718
|
+
def initialize(raw_data)
|
719
|
+
@raw_data = raw_data
|
720
|
+
end
|
721
|
+
|
722
|
+
protected
|
723
|
+
|
724
|
+
def data_is_ascii_printable?(data)
|
725
|
+
data.each_byte do |byte|
|
726
|
+
return false if !(byte >= 0x20 && byte <= 0x7E)
|
727
|
+
end
|
728
|
+
return true
|
729
|
+
end
|
730
|
+
|
731
|
+
end # Chunk
|
732
|
+
end # Script
|
733
|
+
end # BTC
|