money-tree-openssl 1.1
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.
- 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
|