block_io 1.0.6 → 3.0.0

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 (106) 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 +22 -13
  8. data/block_io.gemspec +9 -8
  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 -399
  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 +243 -0
  22. data/lib/block_io/extended_bitcoinrb.rb +127 -0
  23. data/lib/block_io/helper.rb +262 -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 +44 -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 +249 -43
@@ -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,243 @@
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
+ @encryption_key = Helper.pinToAesKey(args[:pin] || "") if args.key?(: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 @encryption_key or @keys.size > 0
135
+
136
+ key = Helper.extractKey(encrypted_key['encrypted_passphrase'], @encryption_key)
137
+ 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"])
138
+
139
+ # store this key for later use
140
+ @keys[key.public_key_hex] = key
141
+
142
+ end
143
+
144
+ # store the provided keys, if any, for later use
145
+ private_keys.each{|key| @keys[key.public_key_hex] = key}
146
+
147
+ signatures = []
148
+
149
+ if @keys.size > 0 then
150
+ # try to sign whatever we can here and give the user the data back
151
+ # Block.io will check to see if all signatures are present, or return an error otherwise saying insufficient signatures provided
152
+
153
+ i = 0
154
+ while i < inputs.size do
155
+ input = inputs[i]
156
+
157
+ input_address_data = data['data']['input_address_data'].detect{|d| d['address'] == input['spending_address']}
158
+ sighash_for_input = Helper.getSigHashForInput(tx, i, input, input_address_data) # in bytes
159
+
160
+ input_address_data['public_keys'].each do |signer_public_key|
161
+ # sign what we can and append signatures to the signatures object
162
+
163
+ next unless @keys.key?(signer_public_key)
164
+
165
+ signature = @keys[signer_public_key].sign(sighash_for_input).unpack("H*")[0] # in hex
166
+ signatures << {"input_index" => i, "public_key" => signer_public_key, "signature" => signature}
167
+
168
+ end
169
+
170
+ i += 1 # go to next input
171
+ end
172
+
173
+ end
174
+
175
+ # if we have everything we need for this transaction, just finalize the transaction
176
+ if Helper.allSignaturesPresent?(tx, inputs, signatures, data['data']['input_address_data']) then
177
+ Helper.finalizeTransaction(tx, inputs, signatures, data['data']['input_address_data'])
178
+ signatures = [] # no signatures left to append
179
+ end
180
+
181
+ # reset keys
182
+ @keys = {}
183
+
184
+ # the response for submitting the transaction
185
+ {"tx_type" => data['data']['tx_type'], "tx_hex" => tx.to_hex, "signatures" => (signatures.size == 0 ? nil : signatures)}
186
+
187
+ end
188
+
189
+ private
190
+
191
+ def internal_prepare_sweep_transaction(args = {}, method_name = "prepare_sweep_transaction")
192
+
193
+ # set the network first if not already known
194
+ api_call({:method_name => "get_balance", :params => {}}) if @network.nil?
195
+
196
+ raise Exception.new("No private_key provided.") unless args.key?(:private_key) and (args[:private_key] || "").size > 0
197
+
198
+ # ensure the private key never goes to Block.io
199
+ key = Key.from_wif(args[:private_key])
200
+ sanitized_args = args.merge({:public_key => key.public_key_hex})
201
+ sanitized_args.delete(:private_key)
202
+
203
+ @keys[key.public_key_hex] = key # store this in our set of keys for later use
204
+
205
+ api_call({:method_name => method_name, :params => sanitized_args})
206
+
207
+ end
208
+
209
+ def set_network(network)
210
+ # load the chain_params for this network
211
+ @network ||= network
212
+ Bitcoin.chain_params = @network unless @network.to_s.size == 0
213
+ end
214
+
215
+ def api_call(args)
216
+
217
+ 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
218
+
219
+ response = @conn.with {|http| http.post("/api/v#{@version}/#{args[:method_name]}", :json => args[:params].merge({:api_key => @api_key}))}
220
+
221
+ begin
222
+ body = Oj.safe_load(response.to_s)
223
+ rescue
224
+ body = {"status" => "fail", "data" => {"error_message" => "Unknown error occurred. Please report this to support@block.io. Status #{response.code}."}}
225
+ end
226
+
227
+ if !body["status"].eql?("success") then
228
+ # raise an exception on error for easy handling
229
+ # user can extract raw response using e.raw_data
230
+ e = APIException.new("#{body["data"]["error_message"]}")
231
+ e.set_raw_data(body)
232
+ raise e
233
+ end
234
+
235
+ set_network(body['data']['network']) if body['data'].key?('network')
236
+
237
+ body
238
+
239
+ end
240
+
241
+ end
242
+
243
+ 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,262 @@
1
+ module BlockIo
2
+
3
+ class Helper
4
+
5
+ def self.allSignaturesPresent?(tx, inputs, signatures, input_address_data)
6
+ # returns true if transaction has all signatures present
7
+
8
+ all_signatures_present = false
9
+
10
+ inputs.each do |input|
11
+ # check if each input has its required signatures
12
+
13
+ spending_address = input['spending_address']
14
+ current_input_address_data = input_address_data.detect{|x| x['address'] == spending_address}
15
+ required_signatures = current_input_address_data['required_signatures']
16
+ public_keys = current_input_address_data['public_keys']
17
+
18
+ signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
19
+
20
+ # break the loop if all signatures are not present for this input
21
+ all_signatures_present = (signatures_present.keys.size >= required_signatures)
22
+ break unless all_signatures_present
23
+
24
+ end
25
+
26
+ all_signatures_present
27
+
28
+ end
29
+
30
+ def self.isSegwitAddressType?(address_type)
31
+
32
+ case address_type
33
+ when /^P2WPKH(-over-P2SH)?$/
34
+ true
35
+ when /^P2WSH(-over-P2SH)?$/
36
+ true
37
+ when /^WITNESS_V(\d)$/
38
+ true
39
+ else
40
+ false
41
+ end
42
+
43
+ end
44
+
45
+ def self.finalizeTransaction(tx, inputs, signatures, input_address_data)
46
+ # append signatures to the transaction and return its hexadecimal representation
47
+
48
+ inputs.each do |input|
49
+ # for each input
50
+
51
+ signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
52
+ address_data = input_address_data.detect{|x| x['address'] == input['spending_address']} # contains public keys (ordered) and the address type
53
+ input_index = input['input_index']
54
+ is_segwit = isSegwitAddressType?(address_data['address_type'])
55
+ script_stack = (is_segwit ? tx.in[input_index].script_witness.stack : tx.in[input_index].script_sig)
56
+
57
+ if ['P2PKH', 'P2WPKH', 'P2WPKH-over-P2SH'].include?(address_data['address_type']) then
58
+ # P2PKH will use script_sig as script_stack
59
+ # P2WPKH input, or P2WPKH-over-P2SH input will use script_witness.stack as script_stack
60
+
61
+ current_public_key = address_data['public_keys'][0]
62
+ current_signature = signatures_present[current_public_key]
63
+
64
+ # no blank push necessary for P2PKH, P2WPKH, P2WPKH-over-P2SH
65
+ script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
66
+ script_stack << [current_public_key].pack("H*")
67
+
68
+ # P2WPKH-over-P2SH required script_sig still
69
+ tx.in[input_index].script_sig << (
70
+ Bitcoin::Script.to_p2wpkh(
71
+ Bitcoin::Key.new(:pubkey => current_public_key, :key_type => Bitcoin::Key::TYPES[:compressed]).hash160 # hash160 of the compressed pubkey
72
+ ).to_payload
73
+ ) if address_data['address_type'] == "P2WPKH-over-P2SH"
74
+
75
+ elsif ['P2SH', 'WITNESS_V0', 'P2WSH-over-P2SH'].include?(address_data['address_type']) then
76
+ # P2SH will use script_sig as script_stack
77
+ # P2WSH or P2WSH-over-P2SH input will use script_witness.stack as script_stack
78
+
79
+ script = Bitcoin::Script.to_p2sh_multisig_script(address_data['required_signatures'], address_data['public_keys'])
80
+
81
+ script_stack << '' # blank push for scripthash always
82
+
83
+ signatures_added = 0
84
+
85
+ address_data['public_keys'].each do |public_key|
86
+ next unless signatures_present.key?(public_key)
87
+
88
+ # append signatures, no sighash needed, in correct order of public keys
89
+ current_signature = signatures_present[public_key]
90
+ script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
91
+
92
+ signatures_added += 1
93
+
94
+ # required signatures added? break loop and move on
95
+ break if signatures_added == address_data['required_signatures']
96
+ end
97
+
98
+ script_stack << script.last.to_payload
99
+
100
+ # P2WSH-over-P2SH needs script_sig populated still
101
+ tx.in[input_index].script_sig << Bitcoin::Script.to_p2wsh(script.last).to_payload if address_data['address_type'] == "P2WSH-over-P2SH"
102
+
103
+ else
104
+ raise "Unrecognized input address: #{address_data['address_type']}"
105
+ end
106
+
107
+ end
108
+
109
+ tx.to_hex
110
+
111
+ end
112
+
113
+ def self.getSigHashForInput(tx, input_index, input_data, input_address_data)
114
+ # returns the sighash for the given input in bytes
115
+
116
+ address_type = input_address_data["address_type"]
117
+ input_value = (BigDecimal(input_data['input_value']) * BigDecimal(100000000)).to_i # in sats
118
+ sighash = nil
119
+
120
+ if address_type == "P2SH" then
121
+ # P2SH addresses
122
+
123
+ script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
124
+ sighash = tx.sighash_for_input(input_index, script.last)
125
+
126
+ elsif address_type == "P2WSH-over-P2SH" or address_type == "WITNESS_V0" then
127
+ # P2WSH-over-P2SH addresses
128
+ # WITNESS_V0 addresses
129
+
130
+ script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
131
+ sighash = tx.sighash_for_input(input_index, script.last, amount: input_value, sig_version: :witness_v0)
132
+
133
+ elsif address_type == "P2WPKH-over-P2SH" or address_type == "P2WPKH" then
134
+ # P2WPKH-over-P2SH addresses
135
+ # P2WPKH addresses
136
+
137
+ pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
138
+ script = Bitcoin::Script.to_p2wpkh(pub_key.hash160)
139
+ sighash = tx.sighash_for_input(input_index, script, amount: input_value, sig_version: :witness_v0)
140
+
141
+ elsif address_type == "P2PKH" then
142
+ # P2PKH addresses
143
+
144
+ pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
145
+ script = Bitcoin::Script.to_p2pkh(pub_key.hash160)
146
+ sighash = tx.sighash_for_input(input_index, script)
147
+
148
+ else
149
+ raise "Unrecognize address type: #{address_type}"
150
+ end
151
+
152
+ sighash
153
+
154
+ end
155
+
156
+ def self.extractKey(encrypted_data, b64_enc_key)
157
+ # passphrase is in plain text
158
+ # encrypted_data is in base64, as it was stored on Block.io
159
+ # returns the private key extracted from the given encrypted data
160
+
161
+ decrypted = self.decrypt(encrypted_data, b64_enc_key)
162
+
163
+ Key.from_passphrase(decrypted)
164
+
165
+ end
166
+
167
+ def self.sha256(value)
168
+ # returns the hex of the hash of the given value
169
+ OpenSSL::Digest::SHA256.digest(value).unpack("H*")[0]
170
+ end
171
+
172
+ def self.pinToAesKey(secret_pin, iterations = 2048)
173
+ # converts the pincode string to PBKDF2
174
+ # returns a base64 version of PBKDF2 pincode
175
+ salt = ""
176
+
177
+ part1 = OpenSSL::PKCS5.pbkdf2_hmac(
178
+ secret_pin,
179
+ "",
180
+ 1024,
181
+ 128/8,
182
+ OpenSSL::Digest::SHA256.new
183
+ ).unpack("H*")[0]
184
+
185
+ part2 = OpenSSL::PKCS5.pbkdf2_hmac(
186
+ part1,
187
+ "",
188
+ 1024,
189
+ 256/8,
190
+ OpenSSL::Digest::SHA256.new
191
+ ) # binary
192
+
193
+ [part2].pack("m0") # the base64 encryption key
194
+
195
+ end
196
+
197
+ # Decrypts a block of data (encrypted_data) given an encryption key
198
+ def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB")
199
+
200
+ response = nil
201
+
202
+ begin
203
+ aes = OpenSSL::Cipher.new(cipher_type)
204
+ aes.decrypt
205
+ aes.key = b64_enc_key.unpack("m0")[0]
206
+ aes.iv = iv unless iv.nil?
207
+ response = aes.update(encrypted_data.unpack("m0")[0]) << aes.final
208
+ rescue Exception => e
209
+ # decryption failed, must be an invalid Secret PIN
210
+ raise Exception.new("Invalid Secret PIN provided.")
211
+ end
212
+
213
+ response
214
+ end
215
+
216
+ # Encrypts a block of data given an encryption key
217
+ def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB")
218
+ aes = OpenSSL::Cipher.new(cipher_type)
219
+ aes.encrypt
220
+ aes.key = b64_enc_key.unpack("m0")[0]
221
+ aes.iv = iv unless iv.nil?
222
+ [aes.update(data) << aes.final].pack("m0")
223
+ end
224
+
225
+ # courtesy bitcoin-ruby
226
+
227
+ def self.int_to_base58(int_val, leading_zero_bytes=0)
228
+ alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
229
+ base58_val, base = "", alpha.size
230
+ while int_val > 0
231
+ int_val, remainder = int_val.divmod(base)
232
+ base58_val = alpha[remainder] << base58_val
233
+ end
234
+ base58_val
235
+ end
236
+
237
+ def self.base58_to_int(base58_val)
238
+ alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
239
+ int_val, base = 0, alpha.size
240
+ base58_val.reverse.each_char.with_index do |char,index|
241
+ raise ArgumentError, "Value not a valid Base58 String." unless char_index = alpha.index(char)
242
+ int_val += char_index*(base**index)
243
+ end
244
+ int_val
245
+ end
246
+
247
+ def self.encode_base58(hex)
248
+ leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : "").size / 2
249
+ ("1"*leading_zero_bytes) << Helper.int_to_base58( hex.to_i(16) )
250
+ end
251
+
252
+ def self.decode_base58(base58_val)
253
+ s = Helper.base58_to_int(base58_val).to_s(16)
254
+ s = (s.bytesize.odd? ? ("0" << s) : s)
255
+ s = "" if s == "00"
256
+ leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : "").size
257
+ s = ("00"*leading_zero_bytes) << s if leading_zero_bytes > 0
258
+ s
259
+ end
260
+ end
261
+
262
+ end