money-tree-extended 0.11.0
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/.idea/$CACHE_FILE$ +6 -0
- data/.idea/.gitignore +2 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/misc.xml +7 -0
- data/.idea/modules.xml +8 -0
- data/.idea/money-tree.iml +31 -0
- data/.idea/vcs.xml +6 -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 +71 -0
- data/lib/money-tree/node.rb +297 -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 +73 -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 +23 -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 +184 -0
@@ -0,0 +1,265 @@
|
|
1
|
+
# encoding ascii-8bit
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module MoneyTree
|
6
|
+
class Key
|
7
|
+
include OpenSSL
|
8
|
+
include Support
|
9
|
+
extend Support
|
10
|
+
class KeyInvalid < StandardError; end
|
11
|
+
class KeyGenerationFailure < StandardError; end
|
12
|
+
class KeyImportFailure < StandardError; end
|
13
|
+
class KeyFormatNotFound < StandardError; end
|
14
|
+
class InvalidWIFFormat < StandardError; end
|
15
|
+
class InvalidBase64Format < StandardError; end
|
16
|
+
|
17
|
+
attr_reader :options, :key, :raw_key
|
18
|
+
attr_accessor :ec_key
|
19
|
+
|
20
|
+
GROUP_NAME = 'secp256k1'
|
21
|
+
ORDER = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141".to_i(16)
|
22
|
+
|
23
|
+
def valid?(eckey = nil)
|
24
|
+
eckey ||= ec_key
|
25
|
+
eckey.nil? ? false : eckey.check_key
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_bytes
|
29
|
+
hex_to_bytes to_hex
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_i
|
33
|
+
bytes_to_int to_bytes
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class PrivateKey < Key
|
38
|
+
|
39
|
+
def initialize(opts = {})
|
40
|
+
@options = opts
|
41
|
+
@ec_key = PKey::EC.new GROUP_NAME
|
42
|
+
if @options[:key]
|
43
|
+
@raw_key = @options[:key]
|
44
|
+
@key = parse_raw_key
|
45
|
+
import
|
46
|
+
else
|
47
|
+
generate
|
48
|
+
@key = to_hex
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate
|
53
|
+
ec_key.generate_key
|
54
|
+
end
|
55
|
+
|
56
|
+
def import
|
57
|
+
ec_key.private_key = BN.new(key, 16)
|
58
|
+
set_public_key
|
59
|
+
end
|
60
|
+
|
61
|
+
def calculate_public_key(opts = {})
|
62
|
+
opts[:compressed] = true unless opts[:compressed] == false
|
63
|
+
group = ec_key.group
|
64
|
+
group.point_conversion_form = opts[:compressed] ? :compressed : :uncompressed
|
65
|
+
point = group.generator.mul ec_key.private_key
|
66
|
+
end
|
67
|
+
|
68
|
+
def set_public_key(opts = {})
|
69
|
+
ec_key.public_key = calculate_public_key(opts)
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_raw_key
|
73
|
+
result = if raw_key.is_a?(Integer) then from_integer
|
74
|
+
elsif hex_format? then from_hex
|
75
|
+
elsif base64_format? then from_base64
|
76
|
+
elsif compressed_wif_format? then from_wif
|
77
|
+
elsif uncompressed_wif_format? then from_wif
|
78
|
+
else
|
79
|
+
raise KeyFormatNotFound
|
80
|
+
end
|
81
|
+
result.downcase
|
82
|
+
end
|
83
|
+
|
84
|
+
def from_integer(bignum = raw_key)
|
85
|
+
# TODO: does this need a byte size specification?
|
86
|
+
int_to_hex(bignum)
|
87
|
+
end
|
88
|
+
|
89
|
+
def from_hex(hex = raw_key)
|
90
|
+
hex
|
91
|
+
end
|
92
|
+
|
93
|
+
def from_wif(wif = raw_key)
|
94
|
+
compressed = wif.length == 52
|
95
|
+
validate_wif(wif)
|
96
|
+
hex = decode_base58(wif)
|
97
|
+
last_char = compressed ? -11 : -9
|
98
|
+
hex.slice(2..last_char)
|
99
|
+
end
|
100
|
+
|
101
|
+
def from_base64(base64_key = raw_key)
|
102
|
+
raise InvalidBase64Format unless base64_format?(base64_key)
|
103
|
+
decode_base64(base64_key)
|
104
|
+
end
|
105
|
+
|
106
|
+
def compressed_wif_format?
|
107
|
+
wif_format?(:compressed)
|
108
|
+
end
|
109
|
+
|
110
|
+
def uncompressed_wif_format?
|
111
|
+
wif_format?(:uncompressed)
|
112
|
+
end
|
113
|
+
|
114
|
+
def wif_format?(compression)
|
115
|
+
length = compression == :compressed ? 52 : 51
|
116
|
+
wif_prefixes = MoneyTree::NETWORKS.map {|k, v| v["#{compression}_wif_chars".to_sym]}.flatten
|
117
|
+
raw_key.length == length && wif_prefixes.include?(raw_key.slice(0))
|
118
|
+
end
|
119
|
+
|
120
|
+
def base64_format?(base64_key = raw_key)
|
121
|
+
base64_key.length == 44 && base64_key =~ /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
|
122
|
+
end
|
123
|
+
|
124
|
+
def hex_format?
|
125
|
+
raw_key.length == 64 && !raw_key[/\H/]
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_hex
|
129
|
+
int_to_hex @ec_key.private_key, 64
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_wif(compressed: true, network: :bitcoin)
|
133
|
+
source = NETWORKS[network][:privkey_version] + to_hex
|
134
|
+
source += NETWORKS[network][:privkey_compression_flag] if compressed
|
135
|
+
hash = sha256(source)
|
136
|
+
hash = sha256(hash)
|
137
|
+
checksum = hash.slice(0..7)
|
138
|
+
source_with_checksum = source + checksum
|
139
|
+
encode_base58(source_with_checksum)
|
140
|
+
end
|
141
|
+
|
142
|
+
def wif_valid?(wif)
|
143
|
+
hex = decode_base58(wif)
|
144
|
+
checksum = hex.chars.to_a.pop(8).join
|
145
|
+
source = hex.slice(0..-9)
|
146
|
+
hash = sha256(source)
|
147
|
+
hash = sha256(hash)
|
148
|
+
hash_checksum = hash.slice(0..7)
|
149
|
+
checksum == hash_checksum
|
150
|
+
end
|
151
|
+
|
152
|
+
def validate_wif(wif)
|
153
|
+
raise InvalidWIFFormat unless wif_valid?(wif)
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_base64
|
157
|
+
encode_base64(to_hex)
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_s(network: :bitcoin)
|
161
|
+
to_wif(network: network)
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
class PublicKey < Key
|
167
|
+
attr_reader :private_key, :point, :group, :key_int
|
168
|
+
|
169
|
+
def initialize(p_key, opts = {})
|
170
|
+
@options = opts
|
171
|
+
@options[:compressed] = true if @options[:compressed].nil?
|
172
|
+
if p_key.is_a?(PrivateKey)
|
173
|
+
@private_key = p_key
|
174
|
+
@point = @private_key.calculate_public_key(@options)
|
175
|
+
@group = @point.group
|
176
|
+
@key = @raw_key = to_hex
|
177
|
+
else
|
178
|
+
@raw_key = p_key
|
179
|
+
@group = PKey::EC::Group.new GROUP_NAME
|
180
|
+
@key = parse_raw_key
|
181
|
+
end
|
182
|
+
|
183
|
+
raise ArgumentError, "Must initialize with a MoneyTree::PrivateKey or a public key value" if @key.nil?
|
184
|
+
end
|
185
|
+
|
186
|
+
def compression
|
187
|
+
@group.point_conversion_form
|
188
|
+
end
|
189
|
+
|
190
|
+
def compression=(compression_type = :compressed)
|
191
|
+
@group.point_conversion_form = compression_type
|
192
|
+
end
|
193
|
+
|
194
|
+
def compressed
|
195
|
+
compressed_key = self.class.new raw_key, options # deep clone
|
196
|
+
compressed_key.set_point to_i, compressed: true
|
197
|
+
compressed_key
|
198
|
+
end
|
199
|
+
|
200
|
+
def uncompressed
|
201
|
+
uncompressed_key = self.class.new raw_key, options # deep clone
|
202
|
+
uncompressed_key.set_point to_i, compressed: false
|
203
|
+
uncompressed_key
|
204
|
+
end
|
205
|
+
|
206
|
+
def set_point(int = to_i, opts = {})
|
207
|
+
opts = options.merge(opts)
|
208
|
+
opts[:compressed] = true if opts[:compressed].nil?
|
209
|
+
self.compression = opts[:compressed] ? :compressed : :uncompressed
|
210
|
+
bn = BN.new int_to_hex(int), 16
|
211
|
+
@point = PKey::EC::Point.new group, bn
|
212
|
+
raise KeyInvalid, 'point is not on the curve' unless @point.on_curve?
|
213
|
+
end
|
214
|
+
|
215
|
+
def parse_raw_key
|
216
|
+
result = if raw_key.is_a?(Integer)
|
217
|
+
set_point raw_key
|
218
|
+
elsif hex_format?
|
219
|
+
set_point hex_to_int(raw_key), compressed: false
|
220
|
+
elsif compressed_hex_format?
|
221
|
+
set_point hex_to_int(raw_key), compressed: true
|
222
|
+
else
|
223
|
+
raise KeyFormatNotFound
|
224
|
+
end
|
225
|
+
to_hex
|
226
|
+
end
|
227
|
+
|
228
|
+
def hex_format?
|
229
|
+
raw_key.length == 130 && !raw_key[/\H/]
|
230
|
+
end
|
231
|
+
|
232
|
+
def compressed_hex_format?
|
233
|
+
raw_key.length == 66 && !raw_key[/\H/]
|
234
|
+
end
|
235
|
+
|
236
|
+
def to_hex
|
237
|
+
int_to_hex to_i, 66
|
238
|
+
end
|
239
|
+
|
240
|
+
def to_i
|
241
|
+
point.to_bn.to_i
|
242
|
+
end
|
243
|
+
|
244
|
+
def to_ripemd160
|
245
|
+
hash = sha256 to_hex
|
246
|
+
ripemd160 hash
|
247
|
+
end
|
248
|
+
|
249
|
+
def to_address(network: :bitcoin)
|
250
|
+
hash = to_ripemd160
|
251
|
+
address = NETWORKS[network][:address_version] + hash
|
252
|
+
to_serialized_base58 address
|
253
|
+
end
|
254
|
+
alias :to_s :to_address
|
255
|
+
|
256
|
+
def to_fingerprint
|
257
|
+
hash = to_ripemd160
|
258
|
+
hash.slice(0..7)
|
259
|
+
end
|
260
|
+
|
261
|
+
def to_bytes
|
262
|
+
int_to_bytes to_i
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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
|
+
zcoin: {
|
32
|
+
address_version: '6f',
|
33
|
+
p2sh_version: 'c4',
|
34
|
+
p2sh_char: 'a',
|
35
|
+
privkey_version: 'ef',
|
36
|
+
privkey_compression_flag: '01',
|
37
|
+
extended_privkey_version: "04358394",
|
38
|
+
extended_pubkey_version: "043587cf",
|
39
|
+
compressed_wif_chars: %w(K L),
|
40
|
+
uncompressed_wif_chars: %w(5),
|
41
|
+
protocol_version: 70001
|
42
|
+
},
|
43
|
+
litecoin: {
|
44
|
+
address_version: '30',
|
45
|
+
p2sh_version: '32',
|
46
|
+
p2sh_char: 'L',
|
47
|
+
privkey_version: '80',
|
48
|
+
privkey_compression_flag: '01',
|
49
|
+
extended_privkey_version: "0488ade4",
|
50
|
+
extended_pubkey_version: "0488b21e",
|
51
|
+
compressed_wif_chars: %w(K L),
|
52
|
+
uncompressed_wif_chars: %w(5),
|
53
|
+
protocol_version: 70001
|
54
|
+
},
|
55
|
+
bitcoin_cash: {
|
56
|
+
address_version: '00',
|
57
|
+
p2sh_version: '05',
|
58
|
+
p2sh_char: '3',
|
59
|
+
privkey_version: '80',
|
60
|
+
privkey_compression_flag: '01',
|
61
|
+
extended_privkey_version: "0488ade4",
|
62
|
+
extended_pubkey_version: "0488b21e",
|
63
|
+
compressed_wif_chars: %w(K L),
|
64
|
+
uncompressed_wif_chars: %w(5),
|
65
|
+
protocol_version: 70001
|
66
|
+
}
|
67
|
+
)
|
68
|
+
hsh[:testnet3] = hsh[:bitcoin_testnet]
|
69
|
+
hsh
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,297 @@
|
|
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
|
+
child_public_key = public_key.uncompressed.group.generator.mul(factor).add(public_key.uncompressed.point).to_bn.to_i
|
94
|
+
raise InvalidKeyForIndex, 'at infinity' if child_public_key == 1/0.0 # very low probability
|
95
|
+
child_chain_code = right_from_hash(hash)
|
96
|
+
return child_public_key, child_chain_code
|
97
|
+
end
|
98
|
+
|
99
|
+
def left_from_hash(hash)
|
100
|
+
bytes_to_int hash.bytes.to_a[0..31]
|
101
|
+
end
|
102
|
+
|
103
|
+
def right_from_hash(hash)
|
104
|
+
bytes_to_int hash.bytes.to_a[32..-1]
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_serialized_hex(type = :public, network: :bitcoin)
|
108
|
+
raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
|
109
|
+
version_key = type.to_sym == :private ? :extended_privkey_version : :extended_pubkey_version
|
110
|
+
hex = NETWORKS[network][version_key] # version (4 bytes)
|
111
|
+
hex += depth_hex(depth) # depth (1 byte)
|
112
|
+
hex += parent_fingerprint # fingerprint of key (4 bytes)
|
113
|
+
hex += index_hex(index) # child number i (4 bytes)
|
114
|
+
hex += chain_code_hex
|
115
|
+
hex += type.to_sym == :private ? "00#{private_key.to_hex}" : public_key.compressed.to_hex
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_bip32(type = :public, network: :bitcoin)
|
119
|
+
raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
|
120
|
+
to_serialized_base58 to_serialized_hex(type, network: network)
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_serialized_address(type = :public, network: :bitcoin)
|
124
|
+
puts 'Node.to_serialized_address is DEPRECATED. Please use .to_bip32.'
|
125
|
+
to_bip32(type, network: network)
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_identifier(compressed=true)
|
129
|
+
key = compressed ? public_key.compressed : public_key.uncompressed
|
130
|
+
key.to_ripemd160
|
131
|
+
end
|
132
|
+
|
133
|
+
def to_fingerprint
|
134
|
+
public_key.compressed.to_fingerprint
|
135
|
+
end
|
136
|
+
|
137
|
+
def parent_fingerprint
|
138
|
+
if @parent_fingerprint
|
139
|
+
@parent_fingerprint
|
140
|
+
else
|
141
|
+
depth.zero? ? '00000000' : parent.to_fingerprint
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def to_address(compressed=true, network: :bitcoin)
|
146
|
+
address = NETWORKS[network][:address_version] + to_identifier(compressed)
|
147
|
+
to_serialized_base58 address
|
148
|
+
end
|
149
|
+
|
150
|
+
def subnode(i = 0, opts = {})
|
151
|
+
if private_key.nil?
|
152
|
+
child_public_key, child_chain_code = derive_public_key(i)
|
153
|
+
child_public_key = MoneyTree::PublicKey.new child_public_key
|
154
|
+
else
|
155
|
+
child_private_key, child_chain_code = derive_private_key(i)
|
156
|
+
child_private_key = MoneyTree::PrivateKey.new key: child_private_key
|
157
|
+
child_public_key = MoneyTree::PublicKey.new child_private_key
|
158
|
+
end
|
159
|
+
|
160
|
+
MoneyTree::Node.new( depth: depth+1,
|
161
|
+
index: i,
|
162
|
+
private_key: private_key.nil? ? nil : child_private_key,
|
163
|
+
public_key: child_public_key,
|
164
|
+
chain_code: child_chain_code,
|
165
|
+
parent: self)
|
166
|
+
end
|
167
|
+
|
168
|
+
# path: a path of subkeys denoted by numbers and slashes. Use
|
169
|
+
# p or i<0 for private key derivation. End with .pub to force
|
170
|
+
# the key public.
|
171
|
+
#
|
172
|
+
# Examples:
|
173
|
+
# 1p/-5/2/1 would call subkey(i=1, is_prime=True).subkey(i=-5).
|
174
|
+
# subkey(i=2).subkey(i=1) and then yield the private key
|
175
|
+
# 0/0/458.pub would call subkey(i=0).subkey(i=0).subkey(i=458) and
|
176
|
+
# then yield the public key
|
177
|
+
#
|
178
|
+
# You should choose either the p or the negative number convention for private key derivation.
|
179
|
+
def node_for_path(path)
|
180
|
+
force_public = path[-4..-1] == '.pub'
|
181
|
+
path = path[0..-5] if force_public
|
182
|
+
parts = path.split('/')
|
183
|
+
nodes = []
|
184
|
+
parts.each_with_index do |part, depth|
|
185
|
+
if part =~ /m/i
|
186
|
+
nodes << self
|
187
|
+
else
|
188
|
+
i = parse_index(part)
|
189
|
+
node = nodes.last || self
|
190
|
+
nodes << node.subnode(i)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
if force_public or parts.first == 'M'
|
194
|
+
node = nodes.last
|
195
|
+
node.strip_private_info!
|
196
|
+
node
|
197
|
+
else
|
198
|
+
nodes.last
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def parse_index(path_part)
|
203
|
+
is_prime = %w(p ').include? path_part[-1]
|
204
|
+
i = path_part.to_i
|
205
|
+
|
206
|
+
i = if i < 0
|
207
|
+
i
|
208
|
+
elsif is_prime
|
209
|
+
i | 0x80000000
|
210
|
+
else
|
211
|
+
i & 0x7fffffff
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def strip_private_info!
|
216
|
+
@private_key = nil
|
217
|
+
end
|
218
|
+
|
219
|
+
def chain_code_hex
|
220
|
+
int_to_hex chain_code, 64
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class Master < Node
|
225
|
+
module SeedGeneration
|
226
|
+
class Failure < Exception; end
|
227
|
+
class RNGFailure < Failure; end
|
228
|
+
class LengthFailure < Failure; end
|
229
|
+
class ValidityError < Failure; end
|
230
|
+
class ImportError < Failure; end
|
231
|
+
class TooManyAttempts < Failure; end
|
232
|
+
end
|
233
|
+
|
234
|
+
HD_WALLET_BASE_KEY = "Bitcoin seed"
|
235
|
+
RANDOM_SEED_SIZE = 32
|
236
|
+
|
237
|
+
attr_reader :seed, :seed_hash
|
238
|
+
|
239
|
+
def initialize(opts = {})
|
240
|
+
@depth = 0
|
241
|
+
@index = 0
|
242
|
+
opts[:seed] = [opts[:seed_hex]].pack("H*") if opts[:seed_hex]
|
243
|
+
if opts[:seed]
|
244
|
+
@seed = opts[:seed]
|
245
|
+
@seed_hash = generate_seed_hash(@seed)
|
246
|
+
raise SeedGeneration::ImportError unless seed_valid?(@seed_hash)
|
247
|
+
set_seeded_keys
|
248
|
+
elsif opts[:private_key] || opts[:public_key]
|
249
|
+
raise ImportError, 'chain code required' unless opts[:chain_code]
|
250
|
+
@chain_code = opts[:chain_code]
|
251
|
+
if opts[:private_key]
|
252
|
+
@private_key = opts[:private_key]
|
253
|
+
@public_key = MoneyTree::PublicKey.new @private_key
|
254
|
+
else opts[:public_key]
|
255
|
+
@public_key = if opts[:public_key].is_a?(MoneyTree::PublicKey)
|
256
|
+
opts[:public_key]
|
257
|
+
else
|
258
|
+
MoneyTree::PublicKey.new(opts[:public_key])
|
259
|
+
end
|
260
|
+
end
|
261
|
+
else
|
262
|
+
generate_seed
|
263
|
+
set_seeded_keys
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def is_private?
|
268
|
+
true
|
269
|
+
end
|
270
|
+
|
271
|
+
def generate_seed
|
272
|
+
@seed = OpenSSL::Random.random_bytes(32)
|
273
|
+
@seed_hash = generate_seed_hash(@seed)
|
274
|
+
raise SeedGeneration::ValidityError unless seed_valid?(@seed_hash)
|
275
|
+
end
|
276
|
+
|
277
|
+
def generate_seed_hash(seed)
|
278
|
+
hmac_sha512 HD_WALLET_BASE_KEY, seed
|
279
|
+
end
|
280
|
+
|
281
|
+
def seed_valid?(seed_hash)
|
282
|
+
return false unless seed_hash.bytesize == 64
|
283
|
+
master_key = left_from_hash(seed_hash)
|
284
|
+
!master_key.zero? && master_key < MoneyTree::Key::ORDER
|
285
|
+
end
|
286
|
+
|
287
|
+
def set_seeded_keys
|
288
|
+
@private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash)
|
289
|
+
@chain_code = right_from_hash(seed_hash)
|
290
|
+
@public_key = MoneyTree::PublicKey.new @private_key
|
291
|
+
end
|
292
|
+
|
293
|
+
def seed_hex
|
294
|
+
bytes_to_hex(seed)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|