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.
Files changed (107) hide show
  1. checksums.yaml +5 -5
  2. data/.appveyor.yml-disabled +26 -0
  3. data/.gitignore +5 -1
  4. data/.rspec +1 -0
  5. data/.travis.yml +14 -0
  6. data/LICENSE +1 -1
  7. data/README.md +23 -13
  8. data/block_io.gemspec +9 -9
  9. data/examples/basic.rb +38 -10
  10. data/examples/dtrust.rb +60 -42
  11. data/examples/proxy.rb +36 -0
  12. data/examples/sweeper.rb +24 -14
  13. data/lib/block_io.rb +16 -408
  14. data/lib/block_io/api_exception.rb +11 -0
  15. data/lib/block_io/chainparams/BTC.yml +8 -0
  16. data/lib/block_io/chainparams/BTCTEST.yml +8 -0
  17. data/lib/block_io/chainparams/DOGE.yml +8 -0
  18. data/lib/block_io/chainparams/DOGETEST.yml +8 -0
  19. data/lib/block_io/chainparams/LTC.yml +8 -0
  20. data/lib/block_io/chainparams/LTCTEST.yml +8 -0
  21. data/lib/block_io/client.rb +244 -0
  22. data/lib/block_io/extended_bitcoinrb.rb +127 -0
  23. data/lib/block_io/helper.rb +322 -0
  24. data/lib/block_io/key.rb +38 -0
  25. data/lib/block_io/version.rb +1 -1
  26. data/spec/client_misc_spec.rb +76 -0
  27. data/spec/client_spec.rb +68 -0
  28. data/spec/dtrust_spec.rb +167 -0
  29. data/spec/helper_spec.rb +154 -0
  30. data/spec/key_spec.rb +92 -0
  31. data/spec/larger_transaction_spec.rb +351 -0
  32. data/spec/spec_helper.rb +5 -0
  33. data/spec/sweep_spec.rb +115 -0
  34. data/spec/test-cases/.gitignore +2 -0
  35. data/spec/test-cases/LICENSE +21 -0
  36. data/spec/test-cases/README.md +2 -0
  37. data/spec/test-cases/json/create_and_sign_transaction_response.json +61 -0
  38. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1261 -0
  39. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1266 -0
  40. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1271 -0
  41. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +3816 -0
  42. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_3of5_195inputs.json +2931 -0
  43. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_4of5_195inputs.json +5 -0
  44. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_251inputs.json +3771 -0
  45. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_252inputs.json +3786 -0
  46. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_253inputs.json +3801 -0
  47. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_251inputs.json +5 -0
  48. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_252inputs.json +5 -0
  49. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_253inputs.json +5 -0
  50. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_251inputs.json +3771 -0
  51. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_252inputs.json +3786 -0
  52. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_253inputs.json +3801 -0
  53. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_251inputs.json +5 -0
  54. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_252inputs.json +5 -0
  55. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_253inputs.json +5 -0
  56. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_3_of_5_keys.json +21 -0
  57. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_4_of_5_keys.json +5 -0
  58. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_3_of_5_keys.json +21 -0
  59. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_4_of_5_keys.json +5 -0
  60. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3_of_5_keys.json +36 -0
  61. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_251outputs.json +591 -0
  62. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_252outputs.json +576 -0
  63. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_253outputs.json +531 -0
  64. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4_of_5_keys.json +5 -0
  65. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_251outputs.json +5 -0
  66. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_252outputs.json +5 -0
  67. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_253outputs.json +5 -0
  68. data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2pkh.json +5 -0
  69. data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh.json +5 -0
  70. data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh_over_p2sh.json +5 -0
  71. data/spec/test-cases/json/create_and_sign_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +21 -0
  72. data/spec/test-cases/json/get_balance_response.json +8 -0
  73. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_3of5_195inputs.json +1397 -0
  74. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_4of5_195inputs.json +1397 -0
  75. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_251inputs.json +1795 -0
  76. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_252inputs.json +1802 -0
  77. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_253inputs.json +1809 -0
  78. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_251inputs.json +1789 -0
  79. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_252inputs.json +1802 -0
  80. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_253inputs.json +1809 -0
  81. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_251inputs.json +1795 -0
  82. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_252inputs.json +1802 -0
  83. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_253inputs.json +1809 -0
  84. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_251inputs.json +1795 -0
  85. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_252inputs.json +1802 -0
  86. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_253inputs.json +1809 -0
  87. data/spec/test-cases/json/prepare_dtrust_transaction_response_p2sh.json +45 -0
  88. data/spec/test-cases/json/prepare_dtrust_transaction_response_p2wsh_over_p2sh.json +45 -0
  89. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0.json +52 -0
  90. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_251outputs.json +1805 -0
  91. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_252outputs.json +1804 -0
  92. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_253outputs.json +1789 -0
  93. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_251outputs.json +1805 -0
  94. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_252outputs.json +1804 -0
  95. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_253outputs.json +1789 -0
  96. data/spec/test-cases/json/prepare_sweep_transaction_response_p2pkh.json +35 -0
  97. data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh.json +35 -0
  98. data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh_over_p2sh.json +35 -0
  99. data/spec/test-cases/json/prepare_transaction_response.json +164 -0
  100. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1796 -0
  101. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1803 -0
  102. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1810 -0
  103. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +5367 -0
  104. data/spec/test-cases/json/prepare_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +76 -0
  105. data/spec/test-cases/json/summarize_prepared_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +6 -0
  106. metadata +264 -73
  107. data/examples/change.rb +0 -117
@@ -0,0 +1,8 @@
1
+ --- !ruby/object:Bitcoin::ChainParams
2
+ network: "BTCTEST"
3
+ address_version: "6f"
4
+ p2sh_version: "c4"
5
+ bech32_hrp: 'tb'
6
+ privkey_version: "ef"
7
+ extended_privkey_version: "04358394"
8
+ extended_pubkey_version: "043587cf"
@@ -0,0 +1,8 @@
1
+ --- !ruby/object:Bitcoin::ChainParams
2
+ network: "DOGE"
3
+ address_version: "1e"
4
+ p2sh_version: "16"
5
+ bech32_hrp: "doge"
6
+ privkey_version: "9e"
7
+ extended_privkey_version: "02fac398"
8
+ extended_pubkey_version: "02facafd"
@@ -0,0 +1,8 @@
1
+ --- !ruby/object:Bitcoin::ChainParams
2
+ network: "DOGETEST"
3
+ address_version: "71"
4
+ p2sh_version: "c4"
5
+ bech32_hrp: "tdge"
6
+ privkey_version: "f1"
7
+ extended_privkey_version: "0432a243"
8
+ extended_pubkey_version: "0432a9a8"
@@ -0,0 +1,8 @@
1
+ --- !ruby/object:Bitcoin::ChainParams
2
+ network: "LTC"
3
+ address_version: "30"
4
+ p2sh_version: "05"
5
+ bech32_hrp: 'ltc'
6
+ privkey_version: "b0"
7
+ extended_privkey_version: "019d9cfe"
8
+ extended_pubkey_version: "019da462"
@@ -0,0 +1,8 @@
1
+ --- !ruby/object:Bitcoin::ChainParams
2
+ network: "LTCTEST"
3
+ address_version: "6f"
4
+ p2sh_version: "3a"
5
+ bech32_hrp: 'tltc'
6
+ privkey_version: "ef"
7
+ extended_privkey_version: "0436f6e1"
8
+ extended_pubkey_version: "0436ef7d"
@@ -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