block_io 1.0.8 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.appveyor.yml-disabled +26 -0
- data/.gitignore +5 -1
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/LICENSE +1 -1
- data/README.md +23 -13
- data/block_io.gemspec +9 -9
- data/examples/basic.rb +38 -10
- data/examples/dtrust.rb +60 -42
- data/examples/proxy.rb +36 -0
- data/examples/sweeper.rb +24 -14
- data/lib/block_io.rb +16 -408
- data/lib/block_io/api_exception.rb +11 -0
- data/lib/block_io/chainparams/BTC.yml +8 -0
- data/lib/block_io/chainparams/BTCTEST.yml +8 -0
- data/lib/block_io/chainparams/DOGE.yml +8 -0
- data/lib/block_io/chainparams/DOGETEST.yml +8 -0
- data/lib/block_io/chainparams/LTC.yml +8 -0
- data/lib/block_io/chainparams/LTCTEST.yml +8 -0
- data/lib/block_io/client.rb +244 -0
- data/lib/block_io/extended_bitcoinrb.rb +127 -0
- data/lib/block_io/helper.rb +322 -0
- data/lib/block_io/key.rb +38 -0
- data/lib/block_io/version.rb +1 -1
- data/spec/client_misc_spec.rb +76 -0
- data/spec/client_spec.rb +68 -0
- data/spec/dtrust_spec.rb +167 -0
- data/spec/helper_spec.rb +154 -0
- data/spec/key_spec.rb +92 -0
- data/spec/larger_transaction_spec.rb +351 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/sweep_spec.rb +115 -0
- data/spec/test-cases/.gitignore +2 -0
- data/spec/test-cases/LICENSE +21 -0
- data/spec/test-cases/README.md +2 -0
- data/spec/test-cases/json/create_and_sign_transaction_response.json +61 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1261 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1266 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1271 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +3816 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_3of5_195inputs.json +2931 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_4of5_195inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_251inputs.json +3771 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_252inputs.json +3786 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_253inputs.json +3801 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_251inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_252inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_253inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_251inputs.json +3771 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_252inputs.json +3786 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_253inputs.json +3801 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_251inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_252inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_253inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_3_of_5_keys.json +21 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_3_of_5_keys.json +21 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3_of_5_keys.json +36 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_251outputs.json +591 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_252outputs.json +576 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_253outputs.json +531 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_251outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_252outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_253outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2pkh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh_over_p2sh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +21 -0
- data/spec/test-cases/json/get_balance_response.json +8 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_3of5_195inputs.json +1397 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_4of5_195inputs.json +1397 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_251inputs.json +1789 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_p2sh.json +45 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_p2wsh_over_p2sh.json +45 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0.json +52 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_251outputs.json +1805 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_252outputs.json +1804 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_253outputs.json +1789 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_251outputs.json +1805 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_252outputs.json +1804 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_253outputs.json +1789 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2pkh.json +35 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh.json +35 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh_over_p2sh.json +35 -0
- data/spec/test-cases/json/prepare_transaction_response.json +164 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1796 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1803 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1810 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +5367 -0
- data/spec/test-cases/json/prepare_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +76 -0
- data/spec/test-cases/json/summarize_prepared_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +6 -0
- metadata +264 -73
- data/examples/change.rb +0 -117
@@ -0,0 +1,244 @@
|
|
1
|
+
module BlockIo
|
2
|
+
|
3
|
+
class Client
|
4
|
+
|
5
|
+
attr_reader :api_key, :version, :network
|
6
|
+
|
7
|
+
def initialize(args = {})
|
8
|
+
# api_key
|
9
|
+
# pin
|
10
|
+
# version
|
11
|
+
# hostname
|
12
|
+
# proxy
|
13
|
+
# pool_size
|
14
|
+
# keys
|
15
|
+
|
16
|
+
raise "Must provide an API Key." unless args.key?(:api_key) and args[:api_key].to_s.size > 0
|
17
|
+
|
18
|
+
@api_key = args[:api_key]
|
19
|
+
@pin = args[:pin]
|
20
|
+
@version = args[:version] || 2
|
21
|
+
@hostname = args[:hostname] || "block.io"
|
22
|
+
@proxy = args[:proxy] || {}
|
23
|
+
@keys = {}
|
24
|
+
|
25
|
+
raise Exception.new("Must specify hostname, port, username, password if using a proxy.") if @proxy.keys.size > 0 and [:hostname, :port, :username, :password].any?{|x| !@proxy.key?(x)}
|
26
|
+
|
27
|
+
@conn = ConnectionPool.new(:size => args[:pool_size] || 5) { http = HTTP.headers(:accept => "application/json", :user_agent => "gem:block_io:#{VERSION}");
|
28
|
+
http = http.via(args.dig(:proxy, :hostname), args.dig(:proxy, :port), args.dig(:proxy, :username), args.dig(:proxy, :password)) if @proxy.key?(:hostname);
|
29
|
+
http = http.persistent("https://#{@hostname}");
|
30
|
+
http }
|
31
|
+
|
32
|
+
# this will get populated after a successful API call
|
33
|
+
@network = nil
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(m, *args)
|
38
|
+
|
39
|
+
method_name = m.to_s
|
40
|
+
|
41
|
+
raise Exception.new("Must provide arguments as a Hash.") unless args.size <= 1 and args.all?{|x| x.is_a?(Hash)}
|
42
|
+
raise Exception.new("Parameter keys must be symbols. For instance: :label => 'default' instead of 'label' => 'default'") unless args[0].nil? or args[0].keys.all?{|x| x.is_a?(Symbol)}
|
43
|
+
raise Exception.new("Cannot pass PINs to any calls. PINs can only be set when initiating this library.") if !args[0].nil? and args[0].key?(:pin)
|
44
|
+
raise Exception.new("Do not specify API Keys here. Initiate a new BlockIo object instead if you need to use another API Key.") if !args[0].nil? and args[0].key?(:api_key)
|
45
|
+
|
46
|
+
if method_name.eql?("prepare_sweep_transaction") then
|
47
|
+
# we need to ensure @network is set before we allow this
|
48
|
+
# we need to send only the public key, not the given private key
|
49
|
+
# we're sweeping from an address
|
50
|
+
internal_prepare_sweep_transaction(args[0], method_name)
|
51
|
+
else
|
52
|
+
api_call({:method_name => method_name, :params => args[0] || {}})
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def summarize_prepared_transaction(data)
|
58
|
+
# takes the response from prepare_transaction/prepare_dtrust_transaction/prepare_sweep_transaction
|
59
|
+
# returns the network fee being paid, the blockio fee being paid, amounts being sent
|
60
|
+
|
61
|
+
input_sum = data['data']['inputs'].map{|input| BigDecimal(input['input_value'])}.inject(:+)
|
62
|
+
|
63
|
+
output_values = [BigDecimal(0)]
|
64
|
+
blockio_fees = [BigDecimal(0)]
|
65
|
+
change_amounts = [BigDecimal(0)]
|
66
|
+
|
67
|
+
data['data']['outputs'].each do |output|
|
68
|
+
if output['output_category'] == 'blockio-fee' then
|
69
|
+
blockio_fees << BigDecimal(output['output_value'])
|
70
|
+
elsif output['output_category'] == 'change' then
|
71
|
+
change_amounts << BigDecimal(output['output_value'])
|
72
|
+
else
|
73
|
+
# user-specified
|
74
|
+
output_values << BigDecimal(output['output_value'])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
output_sum = output_values.inject(:+)
|
79
|
+
blockio_fee = blockio_fees.inject(:+)
|
80
|
+
change_amount = change_amounts.inject(:+)
|
81
|
+
|
82
|
+
network_fee = input_sum - output_sum - blockio_fee - change_amount
|
83
|
+
|
84
|
+
{
|
85
|
+
'network' => data['data']['network'],
|
86
|
+
'network_fee' => '%0.8f' % network_fee,
|
87
|
+
"blockio_fee" => '%0.8f' % blockio_fee,
|
88
|
+
"total_amount_to_send" => '%0.8f' % output_sum
|
89
|
+
}
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_and_sign_transaction(data, keys = [])
|
94
|
+
# takes data from prepare_transaction, prepare_dtrust_transaction, prepare_sweep_transaction
|
95
|
+
# creates the transaction given the inputs and outputs from data
|
96
|
+
# signs the transaction using keys (if not provided, decrypts the key using the PIN)
|
97
|
+
|
98
|
+
set_network(data['data']['network']) if data['data'].key?('network')
|
99
|
+
|
100
|
+
raise "Data must be contain one or more inputs" unless data['data']['inputs'].size > 0
|
101
|
+
raise "Data must contain one or more outputs" unless data['data']['outputs'].size > 0
|
102
|
+
raise "Data must contain information about addresses" unless data['data']['input_address_data'].size > 0 # TODO make stricter
|
103
|
+
|
104
|
+
private_keys = keys.map{|x| Key.from_private_key_hex(x)}
|
105
|
+
|
106
|
+
# TODO debug all of this
|
107
|
+
|
108
|
+
inputs = data['data']['inputs']
|
109
|
+
outputs = data['data']['outputs']
|
110
|
+
|
111
|
+
tx = Bitcoin::Tx.new
|
112
|
+
|
113
|
+
# populate the inputs
|
114
|
+
inputs.each do |input|
|
115
|
+
tx.in << Bitcoin::TxIn.new(:out_point => Bitcoin::OutPoint.from_txid(input['previous_txid'], input['previous_output_index']))
|
116
|
+
end
|
117
|
+
|
118
|
+
# populate the outputs
|
119
|
+
outputs.each do |output|
|
120
|
+
tx.out << Bitcoin::TxOut.new(:value => (BigDecimal(output['output_value']) * BigDecimal(100000000)).to_i, :script_pubkey => Bitcoin::Script.parse_from_addr(output['receiving_address']))
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# some protection against misbehaving machines and/or code
|
125
|
+
raise Exception.new("Expected unsigned transaction ID mismatch. Please report this error to support@block.io.") unless (data['data']['expected_unsigned_txid'].nil? or
|
126
|
+
data['data']['expected_unsigned_txid'] == tx.txid)
|
127
|
+
|
128
|
+
# extract key
|
129
|
+
encrypted_key = data['data']['user_key']
|
130
|
+
|
131
|
+
if !encrypted_key.nil? and !@keys.key?(encrypted_key['public_key']) then
|
132
|
+
# decrypt the key with PIN
|
133
|
+
|
134
|
+
raise Exception.new("PIN not set and no keys provided. Cannot sign transaction.") unless !@pin.nil? or @keys.size > 0
|
135
|
+
|
136
|
+
key = Helper.dynamicExtractKey(encrypted_key, @pin)
|
137
|
+
|
138
|
+
raise Exception.new("Public key mismatch for requested signer and ourselves. Invalid Secret PIN detected.") unless key.public_key_hex.eql?(encrypted_key["public_key"])
|
139
|
+
|
140
|
+
# store this key for later use
|
141
|
+
@keys[key.public_key_hex] = key
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
# store the provided keys, if any, for later use
|
146
|
+
private_keys.each{|key| @keys[key.public_key_hex] = key}
|
147
|
+
|
148
|
+
signatures = []
|
149
|
+
|
150
|
+
if @keys.size > 0 then
|
151
|
+
# try to sign whatever we can here and give the user the data back
|
152
|
+
# Block.io will check to see if all signatures are present, or return an error otherwise saying insufficient signatures provided
|
153
|
+
|
154
|
+
i = 0
|
155
|
+
while i < inputs.size do
|
156
|
+
input = inputs[i]
|
157
|
+
|
158
|
+
input_address_data = data['data']['input_address_data'].detect{|d| d['address'] == input['spending_address']}
|
159
|
+
sighash_for_input = Helper.getSigHashForInput(tx, i, input, input_address_data) # in bytes
|
160
|
+
|
161
|
+
input_address_data['public_keys'].each do |signer_public_key|
|
162
|
+
# sign what we can and append signatures to the signatures object
|
163
|
+
|
164
|
+
next unless @keys.key?(signer_public_key)
|
165
|
+
|
166
|
+
signature = @keys[signer_public_key].sign(sighash_for_input).unpack("H*")[0] # in hex
|
167
|
+
signatures << {"input_index" => i, "public_key" => signer_public_key, "signature" => signature}
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
i += 1 # go to next input
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
# if we have everything we need for this transaction, just finalize the transaction
|
177
|
+
if Helper.allSignaturesPresent?(tx, inputs, signatures, data['data']['input_address_data']) then
|
178
|
+
Helper.finalizeTransaction(tx, inputs, signatures, data['data']['input_address_data'])
|
179
|
+
signatures = [] # no signatures left to append
|
180
|
+
end
|
181
|
+
|
182
|
+
# reset keys
|
183
|
+
@keys = {}
|
184
|
+
|
185
|
+
# the response for submitting the transaction
|
186
|
+
{"tx_type" => data['data']['tx_type'], "tx_hex" => tx.to_hex, "signatures" => (signatures.size == 0 ? nil : signatures)}
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def internal_prepare_sweep_transaction(args = {}, method_name = "prepare_sweep_transaction")
|
193
|
+
|
194
|
+
# set the network first if not already known
|
195
|
+
api_call({:method_name => "get_balance", :params => {}}) if @network.nil?
|
196
|
+
|
197
|
+
raise Exception.new("No private_key provided.") unless args.key?(:private_key) and (args[:private_key] || "").size > 0
|
198
|
+
|
199
|
+
# ensure the private key never goes to Block.io
|
200
|
+
key = Key.from_wif(args[:private_key])
|
201
|
+
sanitized_args = args.merge({:public_key => key.public_key_hex})
|
202
|
+
sanitized_args.delete(:private_key)
|
203
|
+
|
204
|
+
@keys[key.public_key_hex] = key # store this in our set of keys for later use
|
205
|
+
|
206
|
+
api_call({:method_name => method_name, :params => sanitized_args})
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
def set_network(network)
|
211
|
+
# load the chain_params for this network
|
212
|
+
@network ||= network
|
213
|
+
Bitcoin.chain_params = @network unless @network.to_s.size == 0
|
214
|
+
end
|
215
|
+
|
216
|
+
def api_call(args)
|
217
|
+
|
218
|
+
raise Exception.new("No connections left to perform API call. Please re-initialize BlockIo::Client with :pool_size greater than #{@conn.size}.") unless @conn.available > 0
|
219
|
+
|
220
|
+
response = @conn.with {|http| http.post("/api/v#{@version}/#{args[:method_name]}", :json => args[:params].merge({:api_key => @api_key}))}
|
221
|
+
|
222
|
+
begin
|
223
|
+
body = Oj.safe_load(response.to_s)
|
224
|
+
rescue
|
225
|
+
body = {"status" => "fail", "data" => {"error_message" => "Unknown error occurred. Please report this to support@block.io. Status #{response.code}."}}
|
226
|
+
end
|
227
|
+
|
228
|
+
if !body["status"].eql?("success") then
|
229
|
+
# raise an exception on error for easy handling
|
230
|
+
# user can extract raw response using e.raw_data
|
231
|
+
e = APIException.new("#{body["data"]["error_message"]}")
|
232
|
+
e.set_raw_data(body)
|
233
|
+
raise e
|
234
|
+
end
|
235
|
+
|
236
|
+
set_network(body['data']['network']) if body['data'].key?('network')
|
237
|
+
|
238
|
+
body
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Bitcoin
|
4
|
+
|
5
|
+
# set network chain params
|
6
|
+
def self.chain_params=(name)
|
7
|
+
raise "chain params for #{name} is not defined." unless %i(BTC DOGE LTC BTCTEST DOGETEST LTCTEST).include?(name.to_sym)
|
8
|
+
@current_chain = nil
|
9
|
+
@chain_param = name.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
# current network chain params.
|
13
|
+
def self.chain_params
|
14
|
+
return @current_chain if @current_chain
|
15
|
+
return (@current_chain = Bitcoin::ChainParams.get(@chain_param.to_s))
|
16
|
+
end
|
17
|
+
|
18
|
+
class ChainParams
|
19
|
+
def self.get(network)
|
20
|
+
init(network)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.init(name)
|
24
|
+
i = YAML.load(File.open("#{__dir__}/chainparams/#{name}.yml"))
|
25
|
+
i.dust_relay_fee ||= Bitcoin::DUST_RELAY_TX_FEE
|
26
|
+
i
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Secp256k1
|
31
|
+
|
32
|
+
module Ruby
|
33
|
+
|
34
|
+
module_function
|
35
|
+
|
36
|
+
def sign_ecdsa(data, privkey, extra_entropy)
|
37
|
+
privkey = privkey.htb
|
38
|
+
private_key = ECDSA::Format::IntegerOctetString.decode(privkey)
|
39
|
+
extra_entropy ||= ''
|
40
|
+
nonce = RFC6979.generate_rfc6979_nonce(privkey + data, extra_entropy)
|
41
|
+
|
42
|
+
# port form ecdsa gem.
|
43
|
+
r_point = GROUP.new_point(nonce)
|
44
|
+
|
45
|
+
point_field = ECDSA::PrimeField.new(GROUP.order)
|
46
|
+
r = point_field.mod(r_point.x)
|
47
|
+
return nil if r.zero?
|
48
|
+
|
49
|
+
e = ECDSA.normalize_digest(data, GROUP.bit_length)
|
50
|
+
s = point_field.mod(point_field.inverse(nonce) * (e + r * private_key))
|
51
|
+
|
52
|
+
# covert to low-s
|
53
|
+
s = GROUP.order - s if s > (GROUP.order / 2)
|
54
|
+
|
55
|
+
return nil if s.zero?
|
56
|
+
|
57
|
+
signature = ECDSA::Signature.new(r, s).to_der
|
58
|
+
|
59
|
+
# comment lines below lead to performance issues
|
60
|
+
# public_key = Bitcoin::Key.new(priv_key: privkey.bth, :key_type => Bitcoin::Key::TYPES[:compressed]).pubkey # get rid of the key_type warning
|
61
|
+
# raise 'Creation of signature failed.' unless Bitcoin::Secp256k1::Ruby.verify_sig(data, signature, public_key)
|
62
|
+
|
63
|
+
signature
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Key
|
70
|
+
|
71
|
+
def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false)
|
72
|
+
# override so enforce compressed keys
|
73
|
+
|
74
|
+
raise "key_type must always be Bitcoin::KEY::TYPES[:compressed]" unless key_type == TYPES[:compressed]
|
75
|
+
puts "[Warning] Use key_type parameter instead of compressed. compressed parameter removed in the future." if key_type.nil? && !compressed.nil? && pubkey.nil?
|
76
|
+
if key_type
|
77
|
+
@key_type = key_type
|
78
|
+
compressed = @key_type != TYPES[:uncompressed]
|
79
|
+
else
|
80
|
+
@key_type = compressed ? TYPES[:compressed] : TYPES[:uncompressed]
|
81
|
+
end
|
82
|
+
@secp256k1_module = Bitcoin.secp_impl
|
83
|
+
@priv_key = priv_key
|
84
|
+
if @priv_key
|
85
|
+
raise ArgumentError, Errors::Messages::INVALID_PRIV_KEY unless validate_private_key_range(@priv_key)
|
86
|
+
end
|
87
|
+
if pubkey
|
88
|
+
@pubkey = pubkey
|
89
|
+
else
|
90
|
+
@pubkey = generate_pubkey(priv_key, compressed: compressed) if priv_key
|
91
|
+
end
|
92
|
+
raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless fully_valid_pubkey?(allow_hybrid)
|
93
|
+
end
|
94
|
+
|
95
|
+
def public_key_hex
|
96
|
+
@pubkey
|
97
|
+
end
|
98
|
+
|
99
|
+
def private_key_hex
|
100
|
+
@priv_key
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
module Bech32
|
108
|
+
# override so we can parse non-Bitcoin Bech32 addresses
|
109
|
+
|
110
|
+
class SegwitAddr
|
111
|
+
|
112
|
+
private
|
113
|
+
def parse_addr(addr)
|
114
|
+
@hrp, data, spec = Bech32.decode(addr)
|
115
|
+
raise 'Invalid address.' if hrp.nil? || data[0].nil? || !['bc', 'ltc', 'tb', 'tltc', 'doge', 'tdge'].include?(hrp) # HRP_MAINNET, HRP_TESTNET, HRP_REGTEST].include?(hrp)
|
116
|
+
@ver = data[0]
|
117
|
+
raise 'Invalid witness version' if @ver > 16
|
118
|
+
@prog = convert_bits(data[1..-1], 5, 8, false)
|
119
|
+
raise 'Invalid witness program' if @prog.nil? || @prog.length < 2 || @prog.length > 40
|
120
|
+
raise 'Invalid witness program with version 0' if @ver == 0 && (@prog.length != 20 && @prog.length != 32)
|
121
|
+
raise 'Witness version and encoding spec do not match' if (@ver == 0 && spec != Bech32::Encoding::BECH32) || (@ver != 0 && spec != Bech32::Encoding::BECH32M)
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
@@ -0,0 +1,322 @@
|
|
1
|
+
module BlockIo
|
2
|
+
|
3
|
+
class Helper
|
4
|
+
|
5
|
+
LEGACY_DECRYPTION_ALGORITHM = {
|
6
|
+
:pbkdf2_salt => "",
|
7
|
+
:pbkdf2_iterations => 2048,
|
8
|
+
:pbkdf2_hash_function => "SHA256",
|
9
|
+
:pbkdf2_phase1_key_length => 16,
|
10
|
+
:pbkdf2_phase2_key_length => 32,
|
11
|
+
:aes_iv => nil,
|
12
|
+
:aes_cipher => "AES-256-ECB",
|
13
|
+
:aes_auth_tag => nil,
|
14
|
+
:aes_auth_data => nil
|
15
|
+
}
|
16
|
+
|
17
|
+
def self.allSignaturesPresent?(tx, inputs, signatures, input_address_data)
|
18
|
+
# returns true if transaction has all signatures present
|
19
|
+
|
20
|
+
all_signatures_present = false
|
21
|
+
|
22
|
+
inputs.each do |input|
|
23
|
+
# check if each input has its required signatures
|
24
|
+
|
25
|
+
spending_address = input['spending_address']
|
26
|
+
current_input_address_data = input_address_data.detect{|x| x['address'] == spending_address}
|
27
|
+
required_signatures = current_input_address_data['required_signatures']
|
28
|
+
public_keys = current_input_address_data['public_keys']
|
29
|
+
|
30
|
+
signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
|
31
|
+
|
32
|
+
# break the loop if all signatures are not present for this input
|
33
|
+
all_signatures_present = (signatures_present.keys.size >= required_signatures)
|
34
|
+
break unless all_signatures_present
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
all_signatures_present
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.isSegwitAddressType?(address_type)
|
43
|
+
|
44
|
+
case address_type
|
45
|
+
when /^P2WPKH(-over-P2SH)?$/
|
46
|
+
true
|
47
|
+
when /^P2WSH(-over-P2SH)?$/
|
48
|
+
true
|
49
|
+
when /^WITNESS_V(\d)$/
|
50
|
+
true
|
51
|
+
else
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.finalizeTransaction(tx, inputs, signatures, input_address_data)
|
58
|
+
# append signatures to the transaction and return its hexadecimal representation
|
59
|
+
|
60
|
+
inputs.each do |input|
|
61
|
+
# for each input
|
62
|
+
|
63
|
+
signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
|
64
|
+
address_data = input_address_data.detect{|x| x['address'] == input['spending_address']} # contains public keys (ordered) and the address type
|
65
|
+
input_index = input['input_index']
|
66
|
+
is_segwit = isSegwitAddressType?(address_data['address_type'])
|
67
|
+
script_stack = (is_segwit ? tx.in[input_index].script_witness.stack : tx.in[input_index].script_sig)
|
68
|
+
|
69
|
+
if ['P2PKH', 'P2WPKH', 'P2WPKH-over-P2SH'].include?(address_data['address_type']) then
|
70
|
+
# P2PKH will use script_sig as script_stack
|
71
|
+
# P2WPKH input, or P2WPKH-over-P2SH input will use script_witness.stack as script_stack
|
72
|
+
|
73
|
+
current_public_key = address_data['public_keys'][0]
|
74
|
+
current_signature = signatures_present[current_public_key]
|
75
|
+
|
76
|
+
# no blank push necessary for P2PKH, P2WPKH, P2WPKH-over-P2SH
|
77
|
+
script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
|
78
|
+
script_stack << [current_public_key].pack("H*")
|
79
|
+
|
80
|
+
# P2WPKH-over-P2SH required script_sig still
|
81
|
+
tx.in[input_index].script_sig << (
|
82
|
+
Bitcoin::Script.to_p2wpkh(
|
83
|
+
Bitcoin::Key.new(:pubkey => current_public_key, :key_type => Bitcoin::Key::TYPES[:compressed]).hash160 # hash160 of the compressed pubkey
|
84
|
+
).to_payload
|
85
|
+
) if address_data['address_type'] == "P2WPKH-over-P2SH"
|
86
|
+
|
87
|
+
elsif ['P2SH', 'WITNESS_V0', 'P2WSH-over-P2SH'].include?(address_data['address_type']) then
|
88
|
+
# P2SH will use script_sig as script_stack
|
89
|
+
# P2WSH or P2WSH-over-P2SH input will use script_witness.stack as script_stack
|
90
|
+
|
91
|
+
script = Bitcoin::Script.to_p2sh_multisig_script(address_data['required_signatures'], address_data['public_keys'])
|
92
|
+
|
93
|
+
script_stack << '' # blank push for scripthash always
|
94
|
+
|
95
|
+
signatures_added = 0
|
96
|
+
|
97
|
+
address_data['public_keys'].each do |public_key|
|
98
|
+
next unless signatures_present.key?(public_key)
|
99
|
+
|
100
|
+
# append signatures, no sighash needed, in correct order of public keys
|
101
|
+
current_signature = signatures_present[public_key]
|
102
|
+
script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
|
103
|
+
|
104
|
+
signatures_added += 1
|
105
|
+
|
106
|
+
# required signatures added? break loop and move on
|
107
|
+
break if signatures_added == address_data['required_signatures']
|
108
|
+
end
|
109
|
+
|
110
|
+
script_stack << script.last.to_payload
|
111
|
+
|
112
|
+
# P2WSH-over-P2SH needs script_sig populated still
|
113
|
+
tx.in[input_index].script_sig << Bitcoin::Script.to_p2wsh(script.last).to_payload if address_data['address_type'] == "P2WSH-over-P2SH"
|
114
|
+
|
115
|
+
else
|
116
|
+
raise "Unrecognized input address: #{address_data['address_type']}"
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
tx.to_hex
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.getSigHashForInput(tx, input_index, input_data, input_address_data)
|
126
|
+
# returns the sighash for the given input in bytes
|
127
|
+
|
128
|
+
address_type = input_address_data["address_type"]
|
129
|
+
input_value = (BigDecimal(input_data['input_value']) * BigDecimal(100000000)).to_i # in sats
|
130
|
+
sighash = nil
|
131
|
+
|
132
|
+
if address_type == "P2SH" then
|
133
|
+
# P2SH addresses
|
134
|
+
|
135
|
+
script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
|
136
|
+
sighash = tx.sighash_for_input(input_index, script.last)
|
137
|
+
|
138
|
+
elsif address_type == "P2WSH-over-P2SH" or address_type == "WITNESS_V0" then
|
139
|
+
# P2WSH-over-P2SH addresses
|
140
|
+
# WITNESS_V0 addresses
|
141
|
+
|
142
|
+
script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
|
143
|
+
sighash = tx.sighash_for_input(input_index, script.last, amount: input_value, sig_version: :witness_v0)
|
144
|
+
|
145
|
+
elsif address_type == "P2WPKH-over-P2SH" or address_type == "P2WPKH" then
|
146
|
+
# P2WPKH-over-P2SH addresses
|
147
|
+
# P2WPKH addresses
|
148
|
+
|
149
|
+
pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
|
150
|
+
script = Bitcoin::Script.to_p2wpkh(pub_key.hash160)
|
151
|
+
sighash = tx.sighash_for_input(input_index, script, amount: input_value, sig_version: :witness_v0)
|
152
|
+
|
153
|
+
elsif address_type == "P2PKH" then
|
154
|
+
# P2PKH addresses
|
155
|
+
|
156
|
+
pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
|
157
|
+
script = Bitcoin::Script.to_p2pkh(pub_key.hash160)
|
158
|
+
sighash = tx.sighash_for_input(input_index, script)
|
159
|
+
|
160
|
+
else
|
161
|
+
raise "Unrecognize address type: #{address_type}"
|
162
|
+
end
|
163
|
+
|
164
|
+
sighash
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.getDecryptionAlgorithm(user_key_algorithm = nil)
|
169
|
+
# mainly used so existing unit tests do not break
|
170
|
+
|
171
|
+
algorithm = ({}).merge(LEGACY_DECRYPTION_ALGORITHM)
|
172
|
+
|
173
|
+
if !user_key_algorithm.nil? then
|
174
|
+
algorithm[:pbkdf2_salt] = user_key_algorithm['pbkdf2_salt']
|
175
|
+
algorithm[:pbkdf2_iterations] = user_key_algorithm['pbkdf2_iterations']
|
176
|
+
algorithm[:pbkdf2_hash_function] = user_key_algorithm['pbkdf2_hash_function']
|
177
|
+
algorithm[:pbkdf2_phase1_key_length] = user_key_algorithm['pbkdf2_phase1_key_length']
|
178
|
+
algorithm[:pbkdf2_phase2_key_length] = user_key_algorithm['pbkdf2_phase2_key_length']
|
179
|
+
algorithm[:aes_iv] = user_key_algorithm['aes_iv']
|
180
|
+
algorithm[:aes_cipher] = user_key_algorithm['aes_cipher']
|
181
|
+
algorithm[:aes_auth_tag] = user_key_algorithm['aes_auth_tag']
|
182
|
+
algorithm[:aes_auth_data] = user_key_algorithm['aes_auth_data']
|
183
|
+
end
|
184
|
+
|
185
|
+
algorithm
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.dynamicExtractKey(user_key, pin)
|
190
|
+
# user_key object contains the encrypted user key and decryption algorithm
|
191
|
+
|
192
|
+
algorithm = self.getDecryptionAlgorithm(user_key['algorithm'])
|
193
|
+
|
194
|
+
aes_key = self.pinToAesKey(pin, algorithm[:pbkdf2_iterations],
|
195
|
+
algorithm[:pbkdf2_salt],
|
196
|
+
algorithm[:pbkdf2_hash_function],
|
197
|
+
algorithm[:pbkdf2_phase1_key_length],
|
198
|
+
algorithm[:pbkdf2_phase2_key_length])
|
199
|
+
|
200
|
+
decrypted = self.decrypt(user_key['encrypted_passphrase'], aes_key, algorithm[:aes_iv], algorithm[:aes_cipher], algorithm[:aes_auth_tag], algorithm[:aes_auth_data])
|
201
|
+
|
202
|
+
Key.from_passphrase(decrypted)
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.extractKey(encrypted_data, b64_enc_key)
|
207
|
+
# passphrase is in plain text
|
208
|
+
# encrypted_data is in base64, as it was stored on Block.io
|
209
|
+
# returns the private key extracted from the given encrypted data
|
210
|
+
|
211
|
+
decrypted = self.decrypt(encrypted_data, b64_enc_key)
|
212
|
+
|
213
|
+
Key.from_passphrase(decrypted)
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.sha256(value)
|
218
|
+
# returns the hex of the hash of the given value
|
219
|
+
OpenSSL::Digest::SHA256.digest(value).unpack("H*")[0]
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.pinToAesKey(secret_pin, iterations = 2048, salt = "", hash_function = "SHA256", pbkdf2_phase1_key_length = 16, pbkdf2_phase2_key_length = 32)
|
223
|
+
# converts the pincode string to PBKDF2
|
224
|
+
# returns a base64 version of PBKDF2 pincode
|
225
|
+
|
226
|
+
raise Exception.new("Unknown hash function specified. Are you using current version of this library?") unless hash_function == "SHA256"
|
227
|
+
|
228
|
+
part1 = OpenSSL::PKCS5.pbkdf2_hmac(
|
229
|
+
secret_pin,
|
230
|
+
salt,
|
231
|
+
iterations/2,
|
232
|
+
pbkdf2_phase1_key_length,
|
233
|
+
OpenSSL::Digest::SHA256.new
|
234
|
+
).unpack("H*")[0]
|
235
|
+
|
236
|
+
part2 = OpenSSL::PKCS5.pbkdf2_hmac(
|
237
|
+
part1,
|
238
|
+
salt,
|
239
|
+
iterations/2,
|
240
|
+
pbkdf2_phase2_key_length,
|
241
|
+
OpenSSL::Digest::SHA256.new
|
242
|
+
) # binary
|
243
|
+
|
244
|
+
[part2].pack("m0") # the base64 encryption key
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
# Decrypts a block of data (encrypted_data) given an encryption key
|
249
|
+
def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_tag = nil, auth_data = nil)
|
250
|
+
|
251
|
+
raise Exception.new("Auth tag must be 16 bytes exactly.") unless auth_tag.nil? or auth_tag.size == 32
|
252
|
+
|
253
|
+
response = nil
|
254
|
+
|
255
|
+
begin
|
256
|
+
aes = OpenSSL::Cipher.new(cipher_type.downcase)
|
257
|
+
aes.decrypt
|
258
|
+
aes.key = b64_enc_key.unpack("m0")[0]
|
259
|
+
aes.iv = [iv].pack("H*") unless iv.nil?
|
260
|
+
aes.auth_tag = [auth_tag].pack("H*") unless auth_tag.nil?
|
261
|
+
aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
|
262
|
+
response = aes.update(encrypted_data.unpack("m0")[0]) << aes.final
|
263
|
+
rescue Exception => e
|
264
|
+
# decryption failed, must be an invalid Secret PIN
|
265
|
+
raise Exception.new("Invalid Secret PIN provided.")
|
266
|
+
end
|
267
|
+
|
268
|
+
response
|
269
|
+
end
|
270
|
+
|
271
|
+
# Encrypts a block of data given an encryption key
|
272
|
+
def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_data = nil)
|
273
|
+
aes = OpenSSL::Cipher.new(cipher_type.downcase)
|
274
|
+
aes.encrypt
|
275
|
+
aes.key = b64_enc_key.unpack("m0")[0]
|
276
|
+
aes.iv = [iv].pack("H*") unless iv.nil?
|
277
|
+
aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
|
278
|
+
result = [aes.update(data) << aes.final].pack("m0")
|
279
|
+
auth_tag = (cipher_type.end_with?("-GCM") ? aes.auth_tag.unpack("H*")[0] : nil)
|
280
|
+
|
281
|
+
{:aes_auth_tag => auth_tag, :aes_cipher_text => result, :aes_iv => iv, :aes_cipher => cipher_type, :aes_auth_data => auth_data}
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
# courtesy bitcoin-ruby
|
286
|
+
|
287
|
+
def self.int_to_base58(int_val, leading_zero_bytes=0)
|
288
|
+
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
289
|
+
base58_val, base = "", alpha.size
|
290
|
+
while int_val > 0
|
291
|
+
int_val, remainder = int_val.divmod(base)
|
292
|
+
base58_val = alpha[remainder] << base58_val
|
293
|
+
end
|
294
|
+
base58_val
|
295
|
+
end
|
296
|
+
|
297
|
+
def self.base58_to_int(base58_val)
|
298
|
+
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
299
|
+
int_val, base = 0, alpha.size
|
300
|
+
base58_val.reverse.each_char.with_index do |char,index|
|
301
|
+
raise ArgumentError, "Value not a valid Base58 String." unless char_index = alpha.index(char)
|
302
|
+
int_val += char_index*(base**index)
|
303
|
+
end
|
304
|
+
int_val
|
305
|
+
end
|
306
|
+
|
307
|
+
def self.encode_base58(hex)
|
308
|
+
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : "").size / 2
|
309
|
+
("1"*leading_zero_bytes) << Helper.int_to_base58( hex.to_i(16) )
|
310
|
+
end
|
311
|
+
|
312
|
+
def self.decode_base58(base58_val)
|
313
|
+
s = Helper.base58_to_int(base58_val).to_s(16)
|
314
|
+
s = (s.bytesize.odd? ? ("0" << s) : s)
|
315
|
+
s = "" if s == "00"
|
316
|
+
leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : "").size
|
317
|
+
s = ("00"*leading_zero_bytes) << s if leading_zero_bytes > 0
|
318
|
+
s
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|