block_io 1.2.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/.appveyor.yml-disabled +26 -0
  3. data/.gitignore +2 -1
  4. data/.rspec +1 -0
  5. data/.travis.yml +14 -0
  6. data/LICENSE +1 -1
  7. data/README.md +24 -15
  8. data/block_io.gemspec +9 -7
  9. data/examples/basic.rb +32 -10
  10. data/examples/dtrust.rb +59 -42
  11. data/examples/proxy.rb +36 -0
  12. data/examples/sweeper.rb +24 -14
  13. data/lib/block_io.rb +16 -411
  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 +270 -44
  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: "50"
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