money-tree-openssl 1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.simplecov +7 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +258 -0
- data/Rakefile +6 -0
- data/certs/mattatgemco.pem +24 -0
- data/checksum/money-tree-0.9.0.gem.sha512 +1 -0
- data/donation_btc_qr_code.gif +0 -0
- data/lib/money-tree/address.rb +16 -0
- data/lib/money-tree/key.rb +265 -0
- data/lib/money-tree/networks.rb +35 -0
- data/lib/money-tree/node.rb +302 -0
- data/lib/money-tree/support.rb +127 -0
- data/lib/money-tree/version.rb +3 -0
- data/lib/money-tree.rb +12 -0
- data/lib/openssl_extensions.rb +33 -0
- data/money-tree.gemspec +38 -0
- data/spec/lib/money-tree/address_spec.rb +62 -0
- data/spec/lib/money-tree/node_spec.rb +807 -0
- data/spec/lib/money-tree/openssl_extensions_spec.rb +64 -0
- data/spec/lib/money-tree/private_key_spec.rb +121 -0
- data/spec/lib/money-tree/public_key_spec.rb +187 -0
- data/spec/lib/money-tree/support_spec.rb +32 -0
- data/spec/spec_helper.rb +3 -0
- metadata +175 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
module MoneyTree
|
2
|
+
NETWORKS =
|
3
|
+
begin
|
4
|
+
hsh = Hash.new do |_, key|
|
5
|
+
raise "#{key} is not a valid network!"
|
6
|
+
end.merge(
|
7
|
+
bitcoin: {
|
8
|
+
address_version: '00',
|
9
|
+
p2sh_version: '05',
|
10
|
+
p2sh_char: '3',
|
11
|
+
privkey_version: '80',
|
12
|
+
privkey_compression_flag: '01',
|
13
|
+
extended_privkey_version: "0488ade4",
|
14
|
+
extended_pubkey_version: "0488b21e",
|
15
|
+
compressed_wif_chars: %w(K L),
|
16
|
+
uncompressed_wif_chars: %w(5),
|
17
|
+
protocol_version: 70001
|
18
|
+
},
|
19
|
+
bitcoin_testnet: {
|
20
|
+
address_version: '6f',
|
21
|
+
p2sh_version: 'c4',
|
22
|
+
p2sh_char: '2',
|
23
|
+
privkey_version: 'ef',
|
24
|
+
privkey_compression_flag: '01',
|
25
|
+
extended_privkey_version: "04358394",
|
26
|
+
extended_pubkey_version: "043587cf",
|
27
|
+
compressed_wif_chars: %w(c),
|
28
|
+
uncompressed_wif_chars: %w(9),
|
29
|
+
protocol_version: 70001
|
30
|
+
}
|
31
|
+
)
|
32
|
+
hsh[:testnet3] = hsh[:bitcoin_testnet]
|
33
|
+
hsh
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
module MoneyTree
|
2
|
+
class Node
|
3
|
+
include Support
|
4
|
+
extend Support
|
5
|
+
attr_reader :private_key, :public_key, :chain_code,
|
6
|
+
:is_private, :depth, :index, :parent
|
7
|
+
|
8
|
+
class PublicDerivationFailure < StandardError; end
|
9
|
+
class InvalidKeyForIndex < StandardError; end
|
10
|
+
class ImportError < StandardError; end
|
11
|
+
class PrivatePublicMismatch < StandardError; end
|
12
|
+
|
13
|
+
def initialize(opts = {})
|
14
|
+
opts.each { |k, v| instance_variable_set "@#{k}", v }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.from_bip32(address, has_version: true)
|
18
|
+
hex = from_serialized_base58 address
|
19
|
+
hex.slice!(0..7) if has_version
|
20
|
+
self.new({
|
21
|
+
depth: hex.slice!(0..1).to_i(16),
|
22
|
+
parent_fingerprint: hex.slice!(0..7),
|
23
|
+
index: hex.slice!(0..7).to_i(16),
|
24
|
+
chain_code: hex.slice!(0..63).to_i(16)
|
25
|
+
}.merge(parse_out_key(hex)))
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.from_serialized_address(address)
|
29
|
+
puts 'Node.from_serialized_address is DEPRECATED. Please use .from_bip32 instead.'
|
30
|
+
from_bip32(address)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.parse_out_key(hex)
|
34
|
+
if hex.slice(0..1) == '00'
|
35
|
+
private_key = MoneyTree::PrivateKey.new(key: hex.slice(2..-1))
|
36
|
+
{
|
37
|
+
private_key: private_key,
|
38
|
+
public_key: MoneyTree::PublicKey.new(private_key)
|
39
|
+
}
|
40
|
+
elsif %w(02 03).include? hex.slice(0..1)
|
41
|
+
{ public_key: MoneyTree::PublicKey.new(hex) }
|
42
|
+
else
|
43
|
+
raise ImportError, 'Public or private key data does not match version type'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def is_private?
|
48
|
+
index >= 0x80000000 || index < 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def index_hex(i = index)
|
52
|
+
if i < 0
|
53
|
+
[i].pack('l>').unpack('H*').first
|
54
|
+
else
|
55
|
+
i.to_s(16).rjust(8, "0")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def depth_hex(depth)
|
60
|
+
depth.to_s(16).rjust(2, "0")
|
61
|
+
end
|
62
|
+
|
63
|
+
def private_derivation_message(i)
|
64
|
+
"\x00" + private_key.to_bytes + i_as_bytes(i)
|
65
|
+
end
|
66
|
+
|
67
|
+
def public_derivation_message(i)
|
68
|
+
public_key.to_bytes << i_as_bytes(i)
|
69
|
+
end
|
70
|
+
|
71
|
+
def i_as_bytes(i)
|
72
|
+
[i].pack('N')
|
73
|
+
end
|
74
|
+
|
75
|
+
def derive_private_key(i = 0)
|
76
|
+
message = i >= 0x80000000 || i < 0 ? private_derivation_message(i) : public_derivation_message(i)
|
77
|
+
hash = hmac_sha512 hex_to_bytes(chain_code_hex), message
|
78
|
+
left_int = left_from_hash(hash)
|
79
|
+
raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability
|
80
|
+
child_private_key = (left_int + private_key.to_i) % MoneyTree::Key::ORDER
|
81
|
+
raise InvalidKeyForIndex, 'equal to zero' if child_private_key == 0 # very low probability
|
82
|
+
child_chain_code = right_from_hash(hash)
|
83
|
+
return child_private_key, child_chain_code
|
84
|
+
end
|
85
|
+
|
86
|
+
def derive_public_key(i = 0)
|
87
|
+
raise PrivatePublicMismatch if i >= 0x80000000
|
88
|
+
message = public_derivation_message(i)
|
89
|
+
hash = hmac_sha512 hex_to_bytes(chain_code_hex), message
|
90
|
+
left_int = left_from_hash(hash)
|
91
|
+
raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability
|
92
|
+
factor = BN.new left_int.to_s
|
93
|
+
|
94
|
+
gen_point = public_key.uncompressed.group.generator.mul(factor)
|
95
|
+
|
96
|
+
sum_point_hex = MoneyTree::OpenSSLExtensions.add(gen_point, public_key.uncompressed.point)
|
97
|
+
child_public_key = OpenSSL::PKey::EC::Point.new(public_key.group, OpenSSL::BN.new(sum_point_hex, 16)).to_bn.to_i
|
98
|
+
|
99
|
+
raise InvalidKeyForIndex, 'at infinity' if child_public_key == 1/0.0 # very low probability
|
100
|
+
child_chain_code = right_from_hash(hash)
|
101
|
+
return child_public_key, child_chain_code
|
102
|
+
end
|
103
|
+
|
104
|
+
def left_from_hash(hash)
|
105
|
+
bytes_to_int hash.bytes.to_a[0..31]
|
106
|
+
end
|
107
|
+
|
108
|
+
def right_from_hash(hash)
|
109
|
+
bytes_to_int hash.bytes.to_a[32..-1]
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_serialized_hex(type = :public, network: :bitcoin)
|
113
|
+
raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
|
114
|
+
version_key = type.to_sym == :private ? :extended_privkey_version : :extended_pubkey_version
|
115
|
+
hex = NETWORKS[network][version_key] # version (4 bytes)
|
116
|
+
hex += depth_hex(depth) # depth (1 byte)
|
117
|
+
hex += parent_fingerprint # fingerprint of key (4 bytes)
|
118
|
+
hex += index_hex(index) # child number i (4 bytes)
|
119
|
+
hex += chain_code_hex
|
120
|
+
hex += type.to_sym == :private ? "00#{private_key.to_hex}" : public_key.compressed.to_hex
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_bip32(type = :public, network: :bitcoin)
|
124
|
+
raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
|
125
|
+
to_serialized_base58 to_serialized_hex(type, network: network)
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_serialized_address(type = :public, network: :bitcoin)
|
129
|
+
puts 'Node.to_serialized_address is DEPRECATED. Please use .to_bip32.'
|
130
|
+
to_bip32(type, network: network)
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_identifier(compressed=true)
|
134
|
+
key = compressed ? public_key.compressed : public_key.uncompressed
|
135
|
+
key.to_ripemd160
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_fingerprint
|
139
|
+
public_key.compressed.to_fingerprint
|
140
|
+
end
|
141
|
+
|
142
|
+
def parent_fingerprint
|
143
|
+
if @parent_fingerprint
|
144
|
+
@parent_fingerprint
|
145
|
+
else
|
146
|
+
depth.zero? ? '00000000' : parent.to_fingerprint
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def to_address(compressed=true, network: :bitcoin)
|
151
|
+
address = NETWORKS[network][:address_version] + to_identifier(compressed)
|
152
|
+
to_serialized_base58 address
|
153
|
+
end
|
154
|
+
|
155
|
+
def subnode(i = 0, opts = {})
|
156
|
+
if private_key.nil?
|
157
|
+
child_public_key, child_chain_code = derive_public_key(i)
|
158
|
+
child_public_key = MoneyTree::PublicKey.new child_public_key
|
159
|
+
else
|
160
|
+
child_private_key, child_chain_code = derive_private_key(i)
|
161
|
+
child_private_key = MoneyTree::PrivateKey.new key: child_private_key
|
162
|
+
child_public_key = MoneyTree::PublicKey.new child_private_key
|
163
|
+
end
|
164
|
+
|
165
|
+
MoneyTree::Node.new( depth: depth+1,
|
166
|
+
index: i,
|
167
|
+
private_key: private_key.nil? ? nil : child_private_key,
|
168
|
+
public_key: child_public_key,
|
169
|
+
chain_code: child_chain_code,
|
170
|
+
parent: self)
|
171
|
+
end
|
172
|
+
|
173
|
+
# path: a path of subkeys denoted by numbers and slashes. Use
|
174
|
+
# p or i<0 for private key derivation. End with .pub to force
|
175
|
+
# the key public.
|
176
|
+
#
|
177
|
+
# Examples:
|
178
|
+
# 1p/-5/2/1 would call subkey(i=1, is_prime=True).subkey(i=-5).
|
179
|
+
# subkey(i=2).subkey(i=1) and then yield the private key
|
180
|
+
# 0/0/458.pub would call subkey(i=0).subkey(i=0).subkey(i=458) and
|
181
|
+
# then yield the public key
|
182
|
+
#
|
183
|
+
# You should choose either the p or the negative number convention for private key derivation.
|
184
|
+
def node_for_path(path)
|
185
|
+
force_public = path[-4..-1] == '.pub'
|
186
|
+
path = path[0..-5] if force_public
|
187
|
+
parts = path.split('/')
|
188
|
+
nodes = []
|
189
|
+
parts.each_with_index do |part, depth|
|
190
|
+
if part =~ /m/i
|
191
|
+
nodes << self
|
192
|
+
else
|
193
|
+
i = parse_index(part)
|
194
|
+
node = nodes.last || self
|
195
|
+
nodes << node.subnode(i)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
if force_public or parts.first == 'M'
|
199
|
+
node = nodes.last
|
200
|
+
node.strip_private_info!
|
201
|
+
node
|
202
|
+
else
|
203
|
+
nodes.last
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def parse_index(path_part)
|
208
|
+
is_prime = %w(p ').include? path_part[-1]
|
209
|
+
i = path_part.to_i
|
210
|
+
|
211
|
+
i = if i < 0
|
212
|
+
i
|
213
|
+
elsif is_prime
|
214
|
+
i | 0x80000000
|
215
|
+
else
|
216
|
+
i & 0x7fffffff
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def strip_private_info!
|
221
|
+
@private_key = nil
|
222
|
+
end
|
223
|
+
|
224
|
+
def chain_code_hex
|
225
|
+
int_to_hex chain_code, 64
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class Master < Node
|
230
|
+
module SeedGeneration
|
231
|
+
class Failure < Exception; end
|
232
|
+
class RNGFailure < Failure; end
|
233
|
+
class LengthFailure < Failure; end
|
234
|
+
class ValidityError < Failure; end
|
235
|
+
class ImportError < Failure; end
|
236
|
+
class TooManyAttempts < Failure; end
|
237
|
+
end
|
238
|
+
|
239
|
+
HD_WALLET_BASE_KEY = "Bitcoin seed"
|
240
|
+
RANDOM_SEED_SIZE = 32
|
241
|
+
|
242
|
+
attr_reader :seed, :seed_hash
|
243
|
+
|
244
|
+
def initialize(opts = {})
|
245
|
+
@depth = 0
|
246
|
+
@index = 0
|
247
|
+
opts[:seed] = [opts[:seed_hex]].pack("H*") if opts[:seed_hex]
|
248
|
+
if opts[:seed]
|
249
|
+
@seed = opts[:seed]
|
250
|
+
@seed_hash = generate_seed_hash(@seed)
|
251
|
+
raise SeedGeneration::ImportError unless seed_valid?(@seed_hash)
|
252
|
+
set_seeded_keys
|
253
|
+
elsif opts[:private_key] || opts[:public_key]
|
254
|
+
raise ImportError, 'chain code required' unless opts[:chain_code]
|
255
|
+
@chain_code = opts[:chain_code]
|
256
|
+
if opts[:private_key]
|
257
|
+
@private_key = opts[:private_key]
|
258
|
+
@public_key = MoneyTree::PublicKey.new @private_key
|
259
|
+
else opts[:public_key]
|
260
|
+
@public_key = if opts[:public_key].is_a?(MoneyTree::PublicKey)
|
261
|
+
opts[:public_key]
|
262
|
+
else
|
263
|
+
MoneyTree::PublicKey.new(opts[:public_key])
|
264
|
+
end
|
265
|
+
end
|
266
|
+
else
|
267
|
+
generate_seed
|
268
|
+
set_seeded_keys
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def is_private?
|
273
|
+
true
|
274
|
+
end
|
275
|
+
|
276
|
+
def generate_seed
|
277
|
+
@seed = OpenSSL::Random.random_bytes(32)
|
278
|
+
@seed_hash = generate_seed_hash(@seed)
|
279
|
+
raise SeedGeneration::ValidityError unless seed_valid?(@seed_hash)
|
280
|
+
end
|
281
|
+
|
282
|
+
def generate_seed_hash(seed)
|
283
|
+
hmac_sha512 HD_WALLET_BASE_KEY, seed
|
284
|
+
end
|
285
|
+
|
286
|
+
def seed_valid?(seed_hash)
|
287
|
+
return false unless seed_hash.bytesize == 64
|
288
|
+
master_key = left_from_hash(seed_hash)
|
289
|
+
!master_key.zero? && master_key < MoneyTree::Key::ORDER
|
290
|
+
end
|
291
|
+
|
292
|
+
def set_seeded_keys
|
293
|
+
@private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash)
|
294
|
+
@chain_code = right_from_hash(seed_hash)
|
295
|
+
@public_key = MoneyTree::PublicKey.new @private_key
|
296
|
+
end
|
297
|
+
|
298
|
+
def seed_hex
|
299
|
+
bytes_to_hex(seed)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module MoneyTree
|
5
|
+
module Support
|
6
|
+
include OpenSSL
|
7
|
+
|
8
|
+
INT32_MAX = 256 ** [1].pack("L*").size
|
9
|
+
INT64_MAX = 256 ** [1].pack("Q*").size
|
10
|
+
BASE58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
11
|
+
|
12
|
+
def int_to_base58(int_val, leading_zero_bytes=0)
|
13
|
+
base58_val, base = '', BASE58_CHARS.size
|
14
|
+
while int_val > 0
|
15
|
+
int_val, remainder = int_val.divmod(base)
|
16
|
+
base58_val = BASE58_CHARS[remainder] + base58_val
|
17
|
+
end
|
18
|
+
base58_val
|
19
|
+
end
|
20
|
+
|
21
|
+
def base58_to_int(base58_val)
|
22
|
+
int_val, base = 0, BASE58_CHARS.size
|
23
|
+
base58_val.reverse.each_char.with_index do |char,index|
|
24
|
+
raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = BASE58_CHARS.index(char)
|
25
|
+
int_val += char_index*(base**index)
|
26
|
+
end
|
27
|
+
int_val
|
28
|
+
end
|
29
|
+
|
30
|
+
def encode_base58(hex)
|
31
|
+
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
|
32
|
+
("1"*leading_zero_bytes) + int_to_base58( hex.to_i(16) )
|
33
|
+
end
|
34
|
+
|
35
|
+
def decode_base58(base58_val)
|
36
|
+
s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? '0'+s : s)
|
37
|
+
s = '' if s == '00'
|
38
|
+
leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : '').size
|
39
|
+
s = ("00"*leading_zero_bytes) + s if leading_zero_bytes > 0
|
40
|
+
s
|
41
|
+
end
|
42
|
+
alias_method :base58_to_hex, :decode_base58
|
43
|
+
|
44
|
+
def to_serialized_base58(hex)
|
45
|
+
hash = sha256 hex
|
46
|
+
hash = sha256 hash
|
47
|
+
checksum = hash.slice(0..7)
|
48
|
+
address = hex + checksum
|
49
|
+
encode_base58 address
|
50
|
+
end
|
51
|
+
|
52
|
+
def from_serialized_base58(base58)
|
53
|
+
hex = decode_base58 base58
|
54
|
+
checksum = hex.slice!(-8..-1)
|
55
|
+
compare_checksum = sha256(sha256(hex)).slice(0..7)
|
56
|
+
raise EncodingError unless checksum == compare_checksum
|
57
|
+
hex
|
58
|
+
end
|
59
|
+
|
60
|
+
def digestify(digest_type, source, opts = {})
|
61
|
+
source = [source].pack("H*") unless opts[:ascii]
|
62
|
+
bytes_to_hex Digest.digest(digest_type, source)
|
63
|
+
end
|
64
|
+
|
65
|
+
def sha256(source, opts = {})
|
66
|
+
digestify('SHA256', source, opts)
|
67
|
+
end
|
68
|
+
|
69
|
+
def ripemd160(source, opts = {})
|
70
|
+
digestify('RIPEMD160', source, opts)
|
71
|
+
end
|
72
|
+
|
73
|
+
def encode_base64(hex)
|
74
|
+
Base64.encode64([hex].pack("H*")).chomp
|
75
|
+
end
|
76
|
+
|
77
|
+
def decode_base64(base64)
|
78
|
+
Base64.decode64(base64).unpack("H*")[0]
|
79
|
+
end
|
80
|
+
|
81
|
+
def hmac_sha512(key, message)
|
82
|
+
digest = Digest::SHA512.new
|
83
|
+
HMAC.digest digest, key, message
|
84
|
+
end
|
85
|
+
|
86
|
+
def hmac_sha512_hex(key, message)
|
87
|
+
md = hmac_sha512(key, message)
|
88
|
+
md.unpack("H*").first.rjust(64, '0')
|
89
|
+
end
|
90
|
+
|
91
|
+
def bytes_to_int(bytes, base = 16)
|
92
|
+
if bytes.is_a?(Array)
|
93
|
+
bytes = bytes.pack("C*")
|
94
|
+
end
|
95
|
+
bytes.unpack("H*")[0].to_i(16)
|
96
|
+
end
|
97
|
+
|
98
|
+
def int_to_hex(i, size=nil)
|
99
|
+
hex = i.to_s(16).downcase
|
100
|
+
if (hex.size % 2) != 0
|
101
|
+
hex = "#{0}#{hex}"
|
102
|
+
end
|
103
|
+
|
104
|
+
if size
|
105
|
+
hex.rjust(size, "0")
|
106
|
+
else
|
107
|
+
hex
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def int_to_bytes(i)
|
112
|
+
[int_to_hex(i)].pack("H*")
|
113
|
+
end
|
114
|
+
|
115
|
+
def bytes_to_hex(bytes)
|
116
|
+
bytes.unpack("H*")[0].downcase
|
117
|
+
end
|
118
|
+
|
119
|
+
def hex_to_bytes(hex)
|
120
|
+
[hex].pack("H*")
|
121
|
+
end
|
122
|
+
|
123
|
+
def hex_to_int(hex)
|
124
|
+
hex.to_i(16)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/money-tree.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "openssl_extensions"
|
2
|
+
require "money-tree/version"
|
3
|
+
require "money-tree/support"
|
4
|
+
require "money-tree/networks"
|
5
|
+
require "money-tree/key"
|
6
|
+
require "money-tree/address"
|
7
|
+
require "money-tree/networks"
|
8
|
+
require "money-tree/node"
|
9
|
+
|
10
|
+
module MoneyTree
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module MoneyTree
|
6
|
+
module OpenSSLExtensions
|
7
|
+
def self.add(point_0, point_1)
|
8
|
+
validate_points(point_0, point_1)
|
9
|
+
|
10
|
+
group = OpenSSL::PKey::EC::Group.new('secp256k1')
|
11
|
+
|
12
|
+
point_0_hex = point_0.to_bn.to_s(16)
|
13
|
+
point_0_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_0_hex, 16))
|
14
|
+
point_1_hex = point_1.to_bn.to_s(16)
|
15
|
+
point_1_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_1_hex, 16))
|
16
|
+
|
17
|
+
sum_point = point_0_pt.add(point_1_pt)
|
18
|
+
|
19
|
+
sum_point.to_bn.to_s(16)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.validate_points(*points)
|
23
|
+
points.each do |point|
|
24
|
+
if !point.is_a?(OpenSSL::PKey::EC::Point)
|
25
|
+
raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
|
26
|
+
elsif point.infinity?
|
27
|
+
raise ArgumentError, "point must not be infinity"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
data/money-tree.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'money-tree/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "money-tree-openssl"
|
8
|
+
spec.version = MoneyTree::VERSION
|
9
|
+
spec.authors = ["Micah Winkelspecht"]
|
10
|
+
spec.email = ["winkelspecht@gmail.com"]
|
11
|
+
spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets (forked to remove ffi)}
|
12
|
+
spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
|
13
|
+
spec.homepage = "https://github.com/cte/money-tree"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# used with gem install ... -P HighSecurity
|
22
|
+
# spec.cert_chain = ["certs/mattatgemco.pem"]
|
23
|
+
|
24
|
+
# # Sign gem when evaluating spec with `gem` command
|
25
|
+
# # unless ENV has set a SKIP_GEM_SIGNING
|
26
|
+
# if ($0 =~ /gem\z/) and not ENV.include?("SKIP_GEM_SIGNING")
|
27
|
+
# spec.signing_key = File.join(Gem.user_home, ".ssh", "gem-private_key.pem")
|
28
|
+
# end
|
29
|
+
|
30
|
+
spec.add_dependency 'openssl', '>= 2.2'
|
31
|
+
|
32
|
+
spec.add_development_dependency "bundler"
|
33
|
+
spec.add_development_dependency "rake"
|
34
|
+
spec.add_development_dependency "rspec"
|
35
|
+
spec.add_development_dependency "simplecov"
|
36
|
+
spec.add_development_dependency "coveralls"
|
37
|
+
spec.add_development_dependency "pry"
|
38
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MoneyTree::Address do
|
4
|
+
describe "initialize" do
|
5
|
+
it "generates a private key by default" do
|
6
|
+
address = MoneyTree::Address.new
|
7
|
+
expect(address.private_key.key.length).to eql(64)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "generates a public key by default" do
|
11
|
+
address = MoneyTree::Address.new
|
12
|
+
expect(address.public_key.key.length).to eql(66)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "imports a private key in hex form" do
|
16
|
+
address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
|
17
|
+
expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
|
18
|
+
expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
|
19
|
+
expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
20
|
+
expect(address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
|
21
|
+
expect(address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "imports a private key in compressed wif format" do
|
25
|
+
address = MoneyTree::Address.new private_key: "KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK"
|
26
|
+
expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
|
27
|
+
expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
|
28
|
+
expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "imports a private key in uncompressed wif format" do
|
32
|
+
address = MoneyTree::Address.new private_key: "5JXz5ZyFk31oHVTQxqce7yitCmTAPxBqeGQ4b7H3Aj3L45wUhoa"
|
33
|
+
expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
|
34
|
+
expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "to_s" do
|
39
|
+
before do
|
40
|
+
@address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns compressed base58 public key" do
|
44
|
+
expect(@address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
45
|
+
expect(@address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "returns compressed WIF private key" do
|
49
|
+
expect(@address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "testnet3" do
|
54
|
+
before do
|
55
|
+
@address = MoneyTree::Address.new network: :bitcoin_testnet
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns a testnet address" do
|
59
|
+
expect(%w(m n)).to include(@address.to_s(network: :bitcoin_testnet)[0])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|