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,377 @@
|
|
1
|
+
# Implementation of OpenAssets protocol.
|
2
|
+
# https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki
|
3
|
+
require_relative 'asset_transaction.rb'
|
4
|
+
module BTC
|
5
|
+
# AssetProcessor implements verification and discovery of assets.
|
6
|
+
# This assumes that underlying Bitcoin transactions are valid and not double-spent.
|
7
|
+
# Only OpenAssets validation is applied to determine which outputs are holding which assets and how much.
|
8
|
+
class AssetProcessor
|
9
|
+
|
10
|
+
# AssetProcessorSource instance that provides transactions.
|
11
|
+
attr_accessor :source
|
12
|
+
|
13
|
+
# Network to use for encoding AssetIDs. Default is `Network.default`.
|
14
|
+
attr_accessor :network
|
15
|
+
|
16
|
+
def initialize(source: nil, network: nil)
|
17
|
+
raise ArgumentError, "Source is missing." if !source
|
18
|
+
@source = source
|
19
|
+
@network = network || Network.default
|
20
|
+
end
|
21
|
+
|
22
|
+
# Scans backwards and validates every AssetTransaction on the way.
|
23
|
+
# Does not verify Bitcoin transactions (assumes amounts and scripts are already validated).
|
24
|
+
# Updates verified flags on the asset transaction.
|
25
|
+
# Returns `true` if asset transaction is verified succesfully.
|
26
|
+
# Returns `false` otherwise.
|
27
|
+
def verify_asset_transaction(asset_transaction)
|
28
|
+
raise ArgumentError, "Asset Transaction is missing" if !asset_transaction
|
29
|
+
|
30
|
+
# Perform a depth-first scanning.
|
31
|
+
# When completed, we'll only have transactions that have all previous txs fully verified.
|
32
|
+
atx_stack = [ asset_transaction ]
|
33
|
+
i = 0
|
34
|
+
max_size = atx_stack.size
|
35
|
+
while i < atx_stack.size # note: array dynamically changes as we scan it
|
36
|
+
atx = atx_stack[i]
|
37
|
+
BTC::Invariant(atx.is_a?(AssetTransaction), "Must be AssetTransaction instance")
|
38
|
+
|
39
|
+
more_atxs_to_verify = partial_verify_asset_transaction(atx)
|
40
|
+
if more_atxs_to_verify == nil # Validation failed - return immediately
|
41
|
+
return false
|
42
|
+
elsif more_atxs_to_verify.size == 0 # atx is fully verifiable (issue-only or we used cached parents), remove it from the list
|
43
|
+
|
44
|
+
# outputs may not be all verified because they can be transfers with cached verified inputs.
|
45
|
+
# so we need to verify local balance and apply asset ids from inputs to outputs
|
46
|
+
if !verify_transfers(atx)
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
if i == 0 # that was the topmost transaction, we are done.
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
|
54
|
+
@source.asset_transaction_was_verified(atx)
|
55
|
+
atx_stack.delete_at(i)
|
56
|
+
i -= 1
|
57
|
+
# If this is was the last parent to check, then the previous item would be :parents marker
|
58
|
+
# Once we validate the child behind that marker, we might have another child or the marker again.
|
59
|
+
# Unroll the stack until the previous item is not a child with all parents verified.
|
60
|
+
while (child_atx = atx_stack[i]) == :parents
|
61
|
+
BTC::Invariant(i >= 1, ":parents marker should be preceded by an asset transaction")
|
62
|
+
atx_stack.delete_at(i)
|
63
|
+
i -= 1
|
64
|
+
child_atx = atx_stack.delete_at(i)
|
65
|
+
|
66
|
+
# Now all inputs are verified, we only need to verify the transfer outputs against them.
|
67
|
+
# This will make outputs verified for the later transactions (earlier in the list).
|
68
|
+
if !verify_transfers(child_atx)
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
# Now transaction is fully verified.
|
73
|
+
# Source can cache it if needed.
|
74
|
+
@source.asset_transaction_was_verified(child_atx)
|
75
|
+
|
76
|
+
if i == 0 # this was the topmost child, return
|
77
|
+
return true
|
78
|
+
end
|
79
|
+
i -= 1
|
80
|
+
end
|
81
|
+
else
|
82
|
+
# we have more things to verify - dig inside these transactions
|
83
|
+
# Start with the last one so once any tx is verifed, we can move back and color inputs of the child transaction.
|
84
|
+
atx_stack.insert(i+1, :parents, *more_atxs_to_verify)
|
85
|
+
max_size = atx_stack.size if atx_stack.size > max_size
|
86
|
+
j = i
|
87
|
+
i += more_atxs_to_verify.size + 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
return true
|
91
|
+
end
|
92
|
+
|
93
|
+
def color_transaction_inputs(atx)
|
94
|
+
atx.inputs.each do |ain|
|
95
|
+
if !ain.verified?
|
96
|
+
prev_atx = ain.previous_asset_transaction
|
97
|
+
BTC::Invariant(!!prev_atx, "Must have previous asset transaction")
|
98
|
+
if !prev_atx.verified?
|
99
|
+
#puts "\n Prev ATX not fully verified: #{prev_atx.inspect} -> input #{ain.index} of #{atx.inspect}"
|
100
|
+
end
|
101
|
+
BTC::Invariant(prev_atx.verified?, "Must have previous asset transaction outputs fully verified")
|
102
|
+
output = prev_atx.outputs[ain.transaction_input.previous_index]
|
103
|
+
BTC::Invariant(output && output.verified?, "Must have a valid reference to a previous verified output")
|
104
|
+
# Copy over color information. The output can be colored or uncolored.
|
105
|
+
ain.asset_id = output.asset_id
|
106
|
+
ain.value = output.value
|
107
|
+
ain.verified = true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# Returns a list of asset transactions remaining to verify.
|
114
|
+
# Returns an empty array if verification succeeded and there is nothing more to verify.
|
115
|
+
# Returns `nil` if verification failed.
|
116
|
+
def partial_verify_asset_transaction(asset_transaction)
|
117
|
+
raise ArgumentError, "Asset Transaction is missing" if !asset_transaction
|
118
|
+
|
119
|
+
# 1. Verify issuing transactions and collect transfer outputs
|
120
|
+
cache_transactions do
|
121
|
+
if !verify_issues(asset_transaction)
|
122
|
+
return nil
|
123
|
+
end
|
124
|
+
|
125
|
+
# No transfer outputs, this transaction is verified.
|
126
|
+
# If there are assets on some inputs, they are destroyed.
|
127
|
+
if asset_transaction.transfer_outputs.size == 0
|
128
|
+
# We keep inputs unverified to indicate that they were not even processed.
|
129
|
+
return []
|
130
|
+
end
|
131
|
+
|
132
|
+
# 2. Fetch parent transactions to verify.
|
133
|
+
# * Verify inputs from non-OpenAsset transactions.
|
134
|
+
# * Return OA transactions for verification.
|
135
|
+
# * Link each input to its OA transaction output.
|
136
|
+
prev_unverified_atxs_by_hash = {}
|
137
|
+
asset_transaction.inputs.each do |ain|
|
138
|
+
txin = ain.transaction_input
|
139
|
+
# Ask source if it has a cached verified transaction for this input.
|
140
|
+
prev_atx = @source.verified_asset_transaction_for_hash(txin.previous_hash)
|
141
|
+
if prev_atx
|
142
|
+
BTC::Invariant(prev_atx.verified?, "Cached verified tx must be fully verified")
|
143
|
+
end
|
144
|
+
prev_atx ||= prev_unverified_atxs_by_hash[txin.previous_hash]
|
145
|
+
if !prev_atx
|
146
|
+
prev_tx = transaction_for_input(txin)
|
147
|
+
if !prev_tx
|
148
|
+
Diagnostics.current.add_message("Failed to load previous transaction for input #{ain.index}: #{txin.previous_id}")
|
149
|
+
return nil
|
150
|
+
end
|
151
|
+
begin
|
152
|
+
prev_atx = AssetTransaction.new(transaction: prev_tx)
|
153
|
+
prev_unverified_atxs_by_hash[prev_atx.transaction_hash] = prev_atx
|
154
|
+
rescue FormatError => e
|
155
|
+
# Previous transaction is not a valid Open Assets transaction,
|
156
|
+
# so we mark the input as uncolored and verified as such.
|
157
|
+
ain.asset_id = nil
|
158
|
+
ain.value = nil
|
159
|
+
ain.verified = true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
# Remember a reference to this transaction so we can validate the whole `asset_transaction` when all previous ones are set and verified.
|
163
|
+
ain.previous_asset_transaction = prev_atx
|
164
|
+
end # each input
|
165
|
+
|
166
|
+
# Return all unverified transactions.
|
167
|
+
# Note: this won't include the already verified one.
|
168
|
+
prev_unverified_atxs_by_hash.values
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Attempts to verify issues. Fetches parent transactions to determine AssetID.
|
173
|
+
# Returns `true` if verified all issue outputs.
|
174
|
+
# Returns `false` if previous tx defining AssetID is not found.
|
175
|
+
def verify_issues(asset_transaction)
|
176
|
+
previous_txout = nil # fetch only when we have > 0 issue outputs
|
177
|
+
asset_transaction.outputs.each do |aout|
|
178
|
+
if !aout.verified?
|
179
|
+
if aout.value && aout.value > 0
|
180
|
+
if aout.issue?
|
181
|
+
previous_txout ||= transaction_output_for_input(asset_transaction.inputs[0].transaction_input)
|
182
|
+
if !previous_txout
|
183
|
+
Diagnostics.current.add_message("Failed to assign AssetID to issue output #{aout.index}: can't find output for input #0")
|
184
|
+
return false
|
185
|
+
end
|
186
|
+
aout.asset_id = AssetID.new(script: previous_txout.script, network: self.network)
|
187
|
+
# Output issues some known asset and amount and therefore it is verified.
|
188
|
+
aout.verified = true
|
189
|
+
else
|
190
|
+
# Transfer outputs must be matched with known asset ids on the inputs.
|
191
|
+
end
|
192
|
+
else
|
193
|
+
# Output without a value is uncolored.
|
194
|
+
aout.asset_id = nil
|
195
|
+
aout.value = nil
|
196
|
+
aout.verified = true
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
true
|
201
|
+
end # verify_issues
|
202
|
+
|
203
|
+
# Attempts to verify transfer transactions assuming all inputs are verified.
|
204
|
+
# Returns `true` if all transfers are verified (also updates `verified` and `asset_id` on them).
|
205
|
+
# Returns `false` if any transfer is invalid or some inputs are not verified.
|
206
|
+
def verify_transfers(asset_transaction)
|
207
|
+
# Do not verify colors on inputs if no transfers occur.
|
208
|
+
# Typically it's an issuance tx. If there are assets on inputs, they are destroyed.
|
209
|
+
if asset_transaction.transfer_outputs.size == 0
|
210
|
+
return true
|
211
|
+
end
|
212
|
+
|
213
|
+
color_transaction_inputs(asset_transaction)
|
214
|
+
|
215
|
+
current_asset_id = nil
|
216
|
+
|
217
|
+
inputs = asset_transaction.inputs.dup
|
218
|
+
current_asset_id = nil
|
219
|
+
current_input = nil
|
220
|
+
current_input_remainder = 0
|
221
|
+
|
222
|
+
asset_transaction.outputs.each do |aout|
|
223
|
+
# Only check outputs that can be colored (value > 0) and are transfer outputs (after the marker)
|
224
|
+
if aout.has_value? && !aout.marker? && !aout.issue?
|
225
|
+
|
226
|
+
aout.asset_id = nil
|
227
|
+
remaining_value = aout.value
|
228
|
+
|
229
|
+
# Try to fill in the output with available inputs.
|
230
|
+
while remaining_value > 0
|
231
|
+
|
232
|
+
BTC::Invariant((current_input_remainder == 0) ? (current_input == nil) : true,
|
233
|
+
"Remainder must be == 0 only when current_input is nil")
|
234
|
+
|
235
|
+
BTC::Invariant((current_input_remainder > 0) ? (current_input && current_input.colored?) : true,
|
236
|
+
"Remainder must be > 0 only when transfer input is colored")
|
237
|
+
|
238
|
+
current_input ||= inputs.shift
|
239
|
+
|
240
|
+
# skip uncolored inputs
|
241
|
+
while current_input && !current_input.colored?
|
242
|
+
current_input = inputs.shift
|
243
|
+
end
|
244
|
+
|
245
|
+
if !current_input
|
246
|
+
Diagnostics.current.add_message("Failed to assign AssetID to transfer output #{aout.index}: not enough colored inputs (#{remaining_value} missing for output #{aout.index}).")
|
247
|
+
return false
|
248
|
+
end
|
249
|
+
|
250
|
+
# Need to consume aout.value units from inputs and extract the asset ID
|
251
|
+
if !current_input.verified?
|
252
|
+
Diagnostics.current.add_message("Failed to assign AssetID to transfer output #{aout.index}: input #{current_input.index} is not verified.")
|
253
|
+
return false
|
254
|
+
end
|
255
|
+
|
256
|
+
# Make sure asset ID matches.
|
257
|
+
# If output gets assigned units from 2 or more inputs, all asset ids must be the same.
|
258
|
+
if !aout.asset_id
|
259
|
+
aout.asset_id = current_input.asset_id
|
260
|
+
else
|
261
|
+
if aout.asset_id != current_input.asset_id
|
262
|
+
Diagnostics.current.add_message("Failed to assign AssetID to transfer output #{aout.index}: already assigned another AssetID.")
|
263
|
+
return false
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
# If we have remainder from the previous output, use it.
|
268
|
+
# Otherwise use the whole input's value.
|
269
|
+
qty = if current_input_remainder > 0
|
270
|
+
current_input_remainder
|
271
|
+
else
|
272
|
+
current_input.value
|
273
|
+
end
|
274
|
+
|
275
|
+
if qty <= remaining_value
|
276
|
+
remaining_value -= qty
|
277
|
+
# choose next input, clear remainder.
|
278
|
+
current_input_remainder = 0
|
279
|
+
current_input = nil
|
280
|
+
else
|
281
|
+
current_input_remainder = qty - remaining_value
|
282
|
+
remaining_value = 0
|
283
|
+
# keep the current input to use with `current_input_remainder` in the next output
|
284
|
+
end
|
285
|
+
end # filling in the output
|
286
|
+
|
287
|
+
aout.verified = true
|
288
|
+
|
289
|
+
BTC::Invariant(aout.verified && aout.asset_id && aout.value > 0, "Transfer output should be fully verified")
|
290
|
+
end # only transfer outputs with value > 0
|
291
|
+
end # each output
|
292
|
+
|
293
|
+
# Some inputs may have remained. If those have some assets, they'll be destroyed.
|
294
|
+
if current_input_remainder > 0
|
295
|
+
Diagnostics.current.add_message("Warning: #{current_input_remainder} units left over from input #{current_input.index} will be destroyed.")
|
296
|
+
end
|
297
|
+
|
298
|
+
while current_input = inputs.shift
|
299
|
+
if current_input.colored?
|
300
|
+
Diagnostics.current.add_message("Warning: #{current_input.value} units from input #{current_input.index} will be destroyed.")
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
return true
|
305
|
+
end # verify_transfers
|
306
|
+
|
307
|
+
# scans forward and validates every AssetTransaction on the way.
|
308
|
+
def discover_asset(asset_id: nil)
|
309
|
+
# TODO: ...
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
protected
|
314
|
+
|
315
|
+
def cache_transactions(&block)
|
316
|
+
begin
|
317
|
+
@cached_txs_depth ||= 0
|
318
|
+
@cached_txs_depth += 1
|
319
|
+
@cached_txs ||= {}
|
320
|
+
result = yield
|
321
|
+
ensure
|
322
|
+
@cached_txs_depth -= 1
|
323
|
+
@cached_txs = nil if @cached_txs_depth <= 0
|
324
|
+
end
|
325
|
+
result
|
326
|
+
end
|
327
|
+
|
328
|
+
def transaction_for_input(txin)
|
329
|
+
transaction_for_hash(txin.previous_hash)
|
330
|
+
end
|
331
|
+
|
332
|
+
def transaction_output_for_input(txin)
|
333
|
+
tx = transaction_for_input(txin)
|
334
|
+
if tx
|
335
|
+
tx.outputs[txin.previous_index]
|
336
|
+
else
|
337
|
+
nil
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def transaction_for_hash(hash)
|
342
|
+
if @cached_txs && (tx = @cached_txs[hash])
|
343
|
+
return tx
|
344
|
+
end
|
345
|
+
tx = @source.transaction_for_hash(hash)
|
346
|
+
if @cached_txs && tx
|
347
|
+
@cached_txs[tx.transaction_hash] = tx
|
348
|
+
end
|
349
|
+
tx
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
::BTC::AssetTransactionInput # make sure AssetTransaction is defined
|
354
|
+
class AssetTransactionInput
|
355
|
+
attr_accessor :previous_asset_transaction
|
356
|
+
end
|
357
|
+
|
358
|
+
module AssetProcessorSource
|
359
|
+
|
360
|
+
# Override this method if you can provide already verified transaction.
|
361
|
+
def verified_asset_transaction_for_hash(hash)
|
362
|
+
nil
|
363
|
+
end
|
364
|
+
|
365
|
+
# Override this to cache verified asset transaction.
|
366
|
+
def asset_transaction_was_verified(atx)
|
367
|
+
nil
|
368
|
+
end
|
369
|
+
|
370
|
+
# Override this to provide a transaction with a given hash.
|
371
|
+
# If transaction is not found or not available, return nil.
|
372
|
+
# This may cause asset transaction remain unverified.
|
373
|
+
def transaction_for_hash(hash)
|
374
|
+
raise "Not Implemented"
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# Implementation of OpenAssets protocol.
|
2
|
+
# https://github.com/OpenAssets/open-assets-protocol/blob/master/specification.mediawiki
|
3
|
+
module BTC
|
4
|
+
|
5
|
+
# Wrapper around Transaction that stores info about assets on inputs and outputs.
|
6
|
+
class AssetTransaction
|
7
|
+
|
8
|
+
# Raw BTC::Transaction containing asset transfer
|
9
|
+
attr_reader :transaction
|
10
|
+
|
11
|
+
# List of AssetTransactionInput instances.
|
12
|
+
attr_reader :inputs
|
13
|
+
|
14
|
+
# List of AssetTransactionOutput instances.
|
15
|
+
attr_reader :outputs
|
16
|
+
|
17
|
+
# AssetMarker instance describing the transaction
|
18
|
+
attr_reader :marker
|
19
|
+
|
20
|
+
# Metadata stored in AssetMarker
|
21
|
+
attr_reader :metadata
|
22
|
+
|
23
|
+
# Identifier of the underlying Bitcoin transaction.
|
24
|
+
attr_reader :transaction_hash
|
25
|
+
attr_reader :transaction_id
|
26
|
+
|
27
|
+
def initialize(transaction: nil, data: nil)
|
28
|
+
if data && !transaction
|
29
|
+
txdata, len = WireFormat.read_string(data: data, offset: 0)
|
30
|
+
raise ArgumentError, "Invalid data: tx data" if !txdata
|
31
|
+
transaction = Transaction.new(data: txdata)
|
32
|
+
data = data[len..-1]
|
33
|
+
end
|
34
|
+
raise ArgumentError, "Missing transaction" if !transaction
|
35
|
+
raise FormatError, "Transaction does not contain an AssetMarker" if !transaction.open_assets_transaction?
|
36
|
+
raise FormatError, "Coinbase transactions cannot be AssetTransactions" if transaction.coinbase?
|
37
|
+
raise FormatError, "Transactions with no inputs cannot be AssetTransactions" if transaction.inputs.size == 0
|
38
|
+
|
39
|
+
@transaction = transaction
|
40
|
+
|
41
|
+
# Extract the first marker output
|
42
|
+
marker_output = transaction.outputs.find{|txout| txout.script.open_assets_marker? }
|
43
|
+
@marker = AssetMarker.new(output: marker_output) # raises if marker is malformed
|
44
|
+
|
45
|
+
# Create unverified inputs.
|
46
|
+
@inputs = transaction.inputs.map do |txin|
|
47
|
+
AssetTransactionInput.new(transaction_input: txin)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create unverified outputs for all outputs.
|
51
|
+
@outputs = transaction.outputs.inject([]) do |array, txout|
|
52
|
+
# note: other outputs looking like markers are allowed, so we check for the first one only.
|
53
|
+
aout = AssetTransactionOutput.new(transaction_output: txout)
|
54
|
+
if txout == marker_output
|
55
|
+
aout.marker = true
|
56
|
+
aout.verified = true
|
57
|
+
else
|
58
|
+
if (txout.index < marker_output.index)
|
59
|
+
aout.issue = true
|
60
|
+
else
|
61
|
+
aout.transfer = true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
array << aout
|
65
|
+
end
|
66
|
+
|
67
|
+
# Check that the marker contains not more quantities than outputs available.
|
68
|
+
# Marker output does not count and is not included in quantities list.
|
69
|
+
if @marker.quantities.size > (@outputs.size - 1)
|
70
|
+
raise FormatError, "OpenAssets marker specifies more quantities than colorable outputs available: #{@marker.quantities.size} > #{@outputs.size-1}."
|
71
|
+
end
|
72
|
+
|
73
|
+
# Fill in assets amounts for outputs (but keep them unverified).
|
74
|
+
# Excessive outputs receive `nil` amount.
|
75
|
+
@outputs.find_all{|o|!o.marker?}.each_with_index do |aout, i|
|
76
|
+
aout.value = @marker.quantities[i]
|
77
|
+
end
|
78
|
+
|
79
|
+
# contains only color info about ins/outs
|
80
|
+
if data
|
81
|
+
offset = 0
|
82
|
+
@inputs.each do |ain|
|
83
|
+
offset = ain.parse_assets_data(data, offset: offset)
|
84
|
+
end
|
85
|
+
@outputs.each do |aout|
|
86
|
+
if !aout.marker?
|
87
|
+
offset = aout.parse_assets_data(data, offset: offset)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Serialized tx will have:
|
94
|
+
# - raw tx data
|
95
|
+
# - array of input colors (verified yes/no, asset id, units)
|
96
|
+
# - array of output colors (verified yes/no, asset id, units) - do not contain marker output
|
97
|
+
def data
|
98
|
+
data = "".b
|
99
|
+
txdata = @transaction.data
|
100
|
+
data << WireFormat.encode_string(txdata)
|
101
|
+
@inputs.each do |ain|
|
102
|
+
data << ain.assets_data
|
103
|
+
end
|
104
|
+
@outputs.each do |aout|
|
105
|
+
if !aout.marker?
|
106
|
+
data << aout.assets_data
|
107
|
+
end
|
108
|
+
end
|
109
|
+
data
|
110
|
+
end
|
111
|
+
|
112
|
+
def metadata
|
113
|
+
marker ? marker.metadata : nil
|
114
|
+
end
|
115
|
+
|
116
|
+
def transaction_hash
|
117
|
+
@transaction.transaction_hash
|
118
|
+
end
|
119
|
+
|
120
|
+
def transaction_id
|
121
|
+
@transaction.transaction_id
|
122
|
+
end
|
123
|
+
|
124
|
+
def issue_outputs
|
125
|
+
@outputs.find_all{|o| o.issue? }
|
126
|
+
end
|
127
|
+
|
128
|
+
def transfer_outputs
|
129
|
+
@outputs.find_all{|o| o.transfer? }
|
130
|
+
end
|
131
|
+
|
132
|
+
# AssetTransaction is considered verified when all outputs are verified.
|
133
|
+
# Inputs may remain unverified when their assets are destroyed.
|
134
|
+
def verified?
|
135
|
+
outputs_verified?
|
136
|
+
end
|
137
|
+
|
138
|
+
def inputs_verified?
|
139
|
+
@inputs.all?{|i|i.verified?}
|
140
|
+
end
|
141
|
+
|
142
|
+
def outputs_verified?
|
143
|
+
@outputs.all?{|o|o.verified?}
|
144
|
+
end
|
145
|
+
|
146
|
+
def output_at_raw_index(i) # deprecated
|
147
|
+
@outputs[i]
|
148
|
+
# @outputs.find do |out|
|
149
|
+
# raise ArgumentError, "Underlying BTC::TransactionOutput instance are expected to have `index` attribute." if !out.index
|
150
|
+
# out.index == i
|
151
|
+
# end
|
152
|
+
end
|
153
|
+
|
154
|
+
def dup
|
155
|
+
atx = AssetTransaction.new(transaction: self.transaction.dup)
|
156
|
+
self.inputs.each_with_index do |ain, i|
|
157
|
+
ain2 = atx.inputs[i]
|
158
|
+
ain2.asset_id = ain.asset_id
|
159
|
+
ain2.value = ain.value
|
160
|
+
ain2.verified = ain.verified
|
161
|
+
end
|
162
|
+
self.outputs.each_with_index do |aout, i|
|
163
|
+
aout2 = atx.outputs[i]
|
164
|
+
aout2.asset_id = aout.asset_id
|
165
|
+
aout2.value = aout.value
|
166
|
+
aout2.verified = aout.verified
|
167
|
+
aout2.kind = aout.kind
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def inspect
|
172
|
+
issues = issue_outputs.map{|out| out.value.inspect }.join(",")
|
173
|
+
ins = inputs.map{|inp| inp.value.inspect }.join(",")
|
174
|
+
outs = transfer_outputs.map{|out| out.value.inspect }.join(",")
|
175
|
+
%{#<#{self.class}:#{self.transaction_id[0,8]} issues [#{issues}] transfers [#{ins}] => [#{outs}]>}
|
176
|
+
end
|
177
|
+
|
178
|
+
protected
|
179
|
+
attr_writer :transaction
|
180
|
+
attr_writer :inputs
|
181
|
+
attr_writer :outputs
|
182
|
+
attr_writer :marker
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module BTC
|
2
|
+
AssetTransactionBuilder
|
3
|
+
class AssetTransactionBuilder
|
4
|
+
class Error < BTCError; end
|
5
|
+
|
6
|
+
# Change address is not specified
|
7
|
+
class MissingChangeAddressError < Error; end
|
8
|
+
|
9
|
+
# Unspent outputs are missing. Maybe because input_addresses are not specified.
|
10
|
+
class MissingUnspentOutputsError < Error; end
|
11
|
+
|
12
|
+
# Unspent outputs are not sufficient to build the transaction.
|
13
|
+
class InsufficientFundsError < Error; end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module BTC
|
2
|
+
AssetTransactionBuilder
|
3
|
+
class AssetTransactionBuilder
|
4
|
+
|
5
|
+
# Interface for providing unspent asset outputs to the asset transaction builder.
|
6
|
+
# You can use it instead of Enumerable attribute `asset_unspent_outputs` to customize which unspents to be used.
|
7
|
+
module Provider
|
8
|
+
# Returns an Enumerable object yielding unspent asset outputs for the given asset ID and amount.
|
9
|
+
# Each unspent output is a BTC::AssetTransactionOutput instance with non-nil `transaction_hash`, `index`, `asset_id`, `value` attributes.
|
10
|
+
# Additional information about outputs and fees may be used to optimize the set of unspents.
|
11
|
+
def asset_unspent_outputs(asset_id: nil, amount: nil)
|
12
|
+
[]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Creates a block-based provider:
|
16
|
+
# atxbuilder.provider = AssetTransactionBuilder::Provider.new {|builder| [...] }
|
17
|
+
def self.new(&block)
|
18
|
+
BlockProvider.new(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
class BlockProvider # private
|
22
|
+
include Provider
|
23
|
+
def initialize(&block)
|
24
|
+
@block = block
|
25
|
+
end
|
26
|
+
def asset_unspent_outputs(atxbuilder)
|
27
|
+
@block.call(atxbuilder)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module BTC
|
2
|
+
AssetTransactionBuilder
|
3
|
+
class AssetTransactionBuilder
|
4
|
+
|
5
|
+
# Result object containing transaction itself and various info about it.
|
6
|
+
# You get this object from `AssetTransactionBuilder#build` method.
|
7
|
+
class Result
|
8
|
+
# Array of BTC::Transaction instances.
|
9
|
+
# These may have unsigned inputs and must be published in the order.
|
10
|
+
# The last transaction is wrapped by the `asset_transaction`.
|
11
|
+
# Typically, this array contains just one transaction. When issuing an asset,
|
12
|
+
# it may contain two transactions.
|
13
|
+
attr_reader :transactions
|
14
|
+
|
15
|
+
# Array of arrays. Each top-level array refers to a list of input indexes to be signed.
|
16
|
+
# Some inputs can be signed already.
|
17
|
+
attr_reader :unsigned_input_indexes
|
18
|
+
|
19
|
+
# AssetTransaction instance with full details about asset issuance and transfer.
|
20
|
+
attr_reader :asset_transaction
|
21
|
+
|
22
|
+
# Total mining fee for all composed transactions.
|
23
|
+
attr_reader :fee
|
24
|
+
|
25
|
+
# Total cost of all issues and transfers (not including the mining fees and asset change outputs)
|
26
|
+
# All of that amount is owned by the asset holders and can be extracted or returned during re-sell.
|
27
|
+
attr_reader :assets_cost
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
self.transactions = []
|
31
|
+
self.unsigned_input_indexes = []
|
32
|
+
self.asset_transaction = nil
|
33
|
+
self.fee = 0
|
34
|
+
self.assets_cost = 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Internal-only setters.
|
39
|
+
class Result
|
40
|
+
attr_accessor :transactions
|
41
|
+
attr_accessor :unsigned_input_indexes
|
42
|
+
attr_accessor :asset_transaction
|
43
|
+
attr_accessor :fee
|
44
|
+
attr_accessor :assets_cost
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|