money-tree-extended 0.11.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.
- 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
|