money-tree 0.9.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/{donation_btc_qr_code.gif → .github/donation_btc_qr_code.gif} +0 -0
- data/.github/workflows/spec.yml +31 -0
- data/README.md +36 -21
- data/Rakefile +1 -1
- data/certs/mattatgemco.pem +24 -0
- data/checksum/money-tree-0.9.0.gem.sha512 +1 -0
- data/lib/money-tree/address.rb +2 -2
- data/lib/money-tree/key.rb +9 -9
- data/lib/money-tree/node.rb +10 -5
- data/lib/money-tree/support.rb +17 -17
- data/lib/money-tree/version.rb +1 -1
- data/lib/money-tree.rb +0 -1
- data/lib/openssl_extensions.rb +7 -52
- data/money-tree.gemspec +9 -20
- data/spec/lib/money-tree/address_spec.rb +23 -7
- data/spec/lib/money-tree/node_spec.rb +190 -190
- data/spec/lib/money-tree/openssl_extensions_spec.rb +49 -1
- data/spec/lib/money-tree/private_key_spec.rb +31 -21
- data/spec/lib/money-tree/public_key_spec.rb +43 -31
- data/spec/lib/money-tree/support_spec.rb +3 -3
- data/spec/spec_helper.rb +0 -1
- metadata +31 -78
- checksums.yaml.gz.sig +0 -0
- data/.simplecov +0 -7
- data/.travis.yml +0 -3
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 040b86e807e6dea0ebf4dfc4b5e508e3f9ef673c94b95a210a18c125d8857974
|
4
|
+
data.tar.gz: '01800e0fb32058488fd01f1a8f2be3539788c7d0bebc0021fd49776fdde947a4'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 70a6465799b1f7140a2e45c2ded416b41e4b64a7801b57325ed20cea470c92cad96e7644596d523a192254e7284eb772406e90af0a797f5b367304cbe2e9b725
|
7
|
+
data.tar.gz: 2d0c6bea3a33f161cf30a3400d3f75b6119098f035eca6cdac076c11c03596a2e0e205456b1e3ea2453f2ad1e58fd7edec91ff21ec1a8fd515c2d7836169762c
|
File without changes
|
@@ -0,0 +1,31 @@
|
|
1
|
+
---
|
2
|
+
name: Spec
|
3
|
+
|
4
|
+
on:
|
5
|
+
pull_request:
|
6
|
+
branches:
|
7
|
+
- master
|
8
|
+
push:
|
9
|
+
branches:
|
10
|
+
- master
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
test:
|
14
|
+
runs-on: ${{ matrix.os }}
|
15
|
+
strategy:
|
16
|
+
fail-fast: false
|
17
|
+
matrix:
|
18
|
+
os: [ubuntu-latest, macos-latest]
|
19
|
+
ruby: ['2.7', '3.0']
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v2
|
22
|
+
- uses: ruby/setup-ruby@v1
|
23
|
+
with:
|
24
|
+
ruby-version: ${{ matrix.ruby }}
|
25
|
+
bundler-cache: false
|
26
|
+
- name: Install Dependencies
|
27
|
+
run: |
|
28
|
+
bundle install
|
29
|
+
- name: Run Tests
|
30
|
+
run: |
|
31
|
+
bundle exec rspec
|
data/README.md
CHANGED
@@ -1,13 +1,23 @@
|
|
1
|
-
[![Build Status](https://travis-ci.org/GemHQ/money-tree.png)](https://travis-ci.org/GemHQ/money-tree) [![Coverage Status](https://img.shields.io/coveralls/GemHQ/money-tree.svg)](https://coveralls.io/r/GemHQ/money-tree?branch=master) [![Code Climate](https://codeclimate.com/github/GemHQ/money-tree.png)](https://codeclimate.com/github/GemHQ/money-tree) [![Gem Version](https://badge.fury.io/rb/money-tree.png)](http://badge.fury.io/rb/money-tree)
|
2
1
|
# MoneyTree
|
2
|
+
|
3
|
+
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/GemHQ/money-tree)](https://github.com/GemHQ/money-tree/releases)
|
4
|
+
[![Gem](https://img.shields.io/gem/v/money-tree)](https://rubygems.org/gems/money-tree)
|
5
|
+
[![Gem](https://img.shields.io/gem/dt/money-tree)](https://rubygems.org/gems/money-tree)
|
6
|
+
[![GitHub top language](https://img.shields.io/github/languages/top/GemHQ/money-tree?color=red)](https://github.com/GemHQ/money-tree/pulse)
|
7
|
+
|
8
|
+
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/GemHQ/money-tree/Spec)](https://github.com/GemHQ/money-tree/actions)
|
9
|
+
[![Coverage Status](https://img.shields.io/coveralls/GemHQ/money-tree.svg)](https://coveralls.io/r/GemHQ/money-tree?branch=master)
|
10
|
+
[![Code Climate](https://codeclimate.com/github/GemHQ/money-tree.png)](https://codeclimate.com/github/GemHQ/money-tree)
|
11
|
+
[![GitHub](https://img.shields.io/github/license/GemHQ/money-tree)](LICENSE)
|
12
|
+
|
3
13
|
### RSpec tested. Big Brother removed.
|
4
14
|
|
5
15
|
MoneyTree is a Ruby implementation of Bitcoin Wallets. Specifically, it supports [Hierachical Deterministic wallets](https://en.bitcoin.it/wiki/Deterministic_Wallet) according to the protocol specified in [BIP0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki).
|
6
16
|
|
7
17
|
___
|
8
|
-
If you find this helpful, please consider a small Bitcoin donation to 1nj2kie1hATcFbAaD7dEY53QaxNgt4KBp
|
18
|
+
If you find this helpful, please consider a small Bitcoin donation to `1nj2kie1hATcFbAaD7dEY53QaxNgt4KBp`.
|
9
19
|
|
10
|
-
![Donate BTC](
|
20
|
+
![Donate BTC](./.github/donation_btc_qr_code.gif)
|
11
21
|
___
|
12
22
|
|
13
23
|
## Why would I want an HD Wallet?
|
@@ -60,7 +70,7 @@ These instructions assume you have a decent understanding of how Bitcoin wallets
|
|
60
70
|
|
61
71
|
### Create a Master Node (seed)
|
62
72
|
|
63
|
-
To create a new HD Wallet, we're going to create a tree structure of private/public keypairs (nodes). You'll first want to start with a master node. This master node should be seeded with at least 16 random bytes but preferably 32 random bytes from a cryptographically secure PRNG (pseudo-random number generator).
|
73
|
+
To create a new HD Wallet, we're going to create a tree structure of private/public keypairs (nodes). You'll first want to start with a master node. This master node should be seeded with at least 16 random bytes but preferably 32 random bytes from a cryptographically secure PRNG (pseudo-random number generator).
|
64
74
|
|
65
75
|
DO NOT use a user generated password. Keep in mind that whoever controls the seed controls ALL coins in the entire tree, so it should not be left up to a human brain, because humans tend to follow patterns and patterns are subject to brute force attacks. Luckily, MoneyTree includes the seed generation by default so you don't need to create this on your own.
|
66
76
|
|
@@ -109,19 +119,19 @@ DO NOT use a user generated password. Keep in mind that whoever controls the see
|
|
109
119
|
@master.public_key.to_hex
|
110
120
|
=> "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"
|
111
121
|
|
112
|
-
@master.chain_code_hex
|
122
|
+
@master.chain_code_hex
|
113
123
|
=> "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" # Look up chain codes in the BIP0032 spec
|
114
124
|
|
115
125
|
@master.to_serialized_hex(:private)
|
116
126
|
=> "0488ade4000000000000000000873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d50800e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"
|
117
127
|
|
118
|
-
@master.
|
128
|
+
@master.to_bip32(:private)
|
119
129
|
=> "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
|
120
130
|
|
121
131
|
@master.to_serialized_hex
|
122
132
|
=> "0488b21e000000000000000000873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d5080339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"
|
123
133
|
|
124
|
-
@master.
|
134
|
+
@master.to_bip32
|
125
135
|
=> "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
|
126
136
|
```
|
127
137
|
|
@@ -140,10 +150,10 @@ To generate a child node from a given path:
|
|
140
150
|
@node.depth
|
141
151
|
=> 2
|
142
152
|
|
143
|
-
@node.
|
153
|
+
@node.to_bip32(:private)
|
144
154
|
=> "xprv9ww7sMFLzJN15m7zX5JEBXQrQq8h4fU8PVqd929Hjy3xNSMzeBf163idMNBSq47DdCakyZTK7KcC2nbz3jqUkpJj8ZR4FqrijcFcFmcoBAe"
|
145
155
|
|
146
|
-
@node.
|
156
|
+
@node.to_bip32
|
147
157
|
=> "xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr"
|
148
158
|
```
|
149
159
|
|
@@ -163,23 +173,23 @@ Because we need multiple pieces of info to reconstruct nodes in a tree, when we'
|
|
163
173
|
"xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
|
164
174
|
```
|
165
175
|
|
166
|
-
In addition to the key and the chain code, this encoding also includes info about the depth and index of the key, along with a fingerprint of its parent key (which I presume is for quickly sorting a big pile of keys into a tree).
|
167
|
-
|
176
|
+
In addition to the key and the chain code, this encoding also includes info about the depth and index of the key, along with a fingerprint of its parent key (which I presume is for quickly sorting a big pile of keys into a tree).
|
177
|
+
|
168
178
|
These are the addresses that you should use to represent each node in the tree structure, however these are NOT the bitcoin addresses you should pass around for receiving money. These are more for storing inside a wallet file so that you can reconstruct the tree.
|
169
179
|
|
170
180
|
To export a node to a serialized address, you can do:
|
171
181
|
|
172
182
|
```ruby
|
173
|
-
@node.
|
183
|
+
@node.to_bip32(:private) # for private keys
|
174
184
|
=> "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
|
175
185
|
|
176
|
-
@node.
|
186
|
+
@node.to_bip32
|
177
187
|
=> "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
|
178
188
|
```
|
179
|
-
|
189
|
+
|
180
190
|
To import from a serialized address: (either public or private)
|
181
191
|
```ruby
|
182
|
-
@node = MoneyTree::Node.
|
192
|
+
@node = MoneyTree::Node.from_bip32 "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
|
183
193
|
=> MoneyTree::Node instance
|
184
194
|
```
|
185
195
|
|
@@ -198,10 +208,10 @@ For example:
|
|
198
208
|
```ruby
|
199
209
|
@node = @master.node_for_path("M/0/3") # or "m/0/3.pub" or "M/0/3.pub"...these are equivalent
|
200
210
|
|
201
|
-
@node.
|
211
|
+
@node.to_bip32
|
202
212
|
=> "xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr"
|
203
213
|
|
204
|
-
@node.
|
214
|
+
@node.to_bip32(:private)
|
205
215
|
-> raises MoneyTree::Node::PrivatePublicMismatch error
|
206
216
|
```
|
207
217
|
|
@@ -209,13 +219,13 @@ For example:
|
|
209
219
|
You can also import a node using only a public key. Keep in mind that this node will only be able to generate other public-key only nodes. You will not be able to derive child private keys using this node.
|
210
220
|
|
211
221
|
```ruby
|
212
|
-
@node = MoneyTree::Node.
|
222
|
+
@node = MoneyTree::Node.from_bip32("xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr")
|
213
223
|
=> MoneyTree::Node instance
|
214
224
|
|
215
|
-
@node.
|
225
|
+
@node.to_bip32
|
216
226
|
=> "xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr"
|
217
227
|
|
218
|
-
@node.
|
228
|
+
@node.to_bip32(:private)
|
219
229
|
-> raises MoneyTree::Node::PrivatePublicMismatch error
|
220
230
|
```
|
221
231
|
|
@@ -241,7 +251,7 @@ For instance:
|
|
241
251
|
|
242
252
|
They are all equivalent ways of saying the same thing, but the first two are just a more human readable shorthand notation. You are free to use whichever notation you prefer. This gem will parse it.
|
243
253
|
|
244
|
-
To add just a little more confusion to the mix, some wallets will use negative `i` values to also denote private derivation. Any `i` value that is negative will be processed using private derivation by this library. e.g. `"m/-1/1"`.
|
254
|
+
To add just a little more confusion to the mix, some wallets will use negative `i` values to also denote private derivation. Any `i` value that is negative will be processed using private derivation by this library. e.g. `"m/-1/1"`. (NOTE: known issue, see below)
|
245
255
|
|
246
256
|
|
247
257
|
## Contributing
|
@@ -251,3 +261,8 @@ To add just a little more confusion to the mix, some wallets will use negative `
|
|
251
261
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
252
262
|
4. Push to the branch (`git push origin my-new-feature`)
|
253
263
|
5. Create new Pull Request
|
264
|
+
|
265
|
+
## Known Issues (PRs welcome)
|
266
|
+
|
267
|
+
- Segwit ([BIP49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki)) address generation is not supported
|
268
|
+
- Use of negative integers in paths does _not_ produce the correct hardened derivation.
|
data/Rakefile
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIEHDCCAoSgAwIBAgIBATANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFtYXR0
|
3
|
+
L0RDPWdlbS9EQz1jbzAeFw0xODA1MzAyMTQwNDZaFw0xOTA1MzAyMTQwNDZaMBwx
|
4
|
+
GjAYBgNVBAMMEW1hdHQvREM9Z2VtL0RDPWNvMIIBojANBgkqhkiG9w0BAQEFAAOC
|
5
|
+
AY8AMIIBigKCAYEAxfbjMHFlxA2P+4YWPagKoGAMi4078imgXdFbD3Rloe6cGfYp
|
6
|
+
IMUQitiHrKi6fhSE0UjXmoP3qnYFddm1enN9zUAFRhHWv7xpINqSqss4PYAb5Anl
|
7
|
+
RYZu3jromop5aVodi15HUfu5z27MvBm4rAaN/dDRfh/rT2hDbTTh0HmvEaPUDfX6
|
8
|
+
TyflAttfabFvtY4qsD+ao8tks0DytqyuEWZ0tvQ6upOgHRNNuYDwDZB1T9v2dq2w
|
9
|
+
3goJFmOKBMMn7UH8WMjD3HiOuRD4tWhq5xWLjBqjzFlVPlZPgdCNyXeMMnLXER98
|
10
|
+
NY35cVWFFuqG+kZwy4MFKdE9WFTocLZxLFo0VVTNSpPara9HirbHtIo9jZNuop4S
|
11
|
+
g4JTf1F8dIWYii3sXoAYZfkl6rHVRP0G/OV5LcTfSS3QkmI5hNltz5FZzc+qI6S1
|
12
|
+
rTR1ZwTy1rRI3coFY7vDRaFWBoMbbo/DytgCE3+rfbVDxQrJa4aZ0iYDhu8LXEA1
|
13
|
+
VTtpf1EWYCOsYE1TAgMBAAGjaTBnMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0G
|
14
|
+
A1UdDgQWBBQ6QoDNre7LFgOukH2Cv+RqZyfUzjAWBgNVHREEDzANgQttYXR0QGdl
|
15
|
+
bS5jbzAWBgNVHRIEDzANgQttYXR0QGdlbS5jbzANBgkqhkiG9w0BAQsFAAOCAYEA
|
16
|
+
kOxYnOsB+NwHwLc2lHEZ8ubxanq2qIZDhvVQ4M31gwmba43xO7vq0ktFxYRvozs4
|
17
|
+
74dQ6bmY2e7njoFgeutyJwxulA+BC71mDQA1s4WsZo7Z2TRgB0GViVqHrzq+jY+M
|
18
|
+
p9mTHQqKH+2j0P9T4DXSzq4qOaBA3YROAwAzYI9N8MObeWkRt2pZ4zYQrAniP2nd
|
19
|
+
wzXs/G5lWbbntVcvQOfAAXBipSJ3X5P2EGpUytP9ZpGdezY5HZzuiJFcmCf1CM3t
|
20
|
+
VX4NZjbJak9gOY0AFD0Aw497sYenm0VBExclOmeRuZLffpWteTTL//utpG3bbFPl
|
21
|
+
jQ78uzsrexYTYW5IshjfSIf3TZxm50Z45pyOTow5EOP1Nd7OmKOcI8hrLGv5+AlD
|
22
|
+
hCnomUTUNsM4Rjwl5rzQiIn3ezv6+0tlg4rWJmVTuOGwcHk/oj1In2sPjCqm0pgx
|
23
|
+
TLnMa8gr6aUpuHR5s2N4ZH0Q2YIsaD6cv7DYXt+G4MRut3njOYHfkqsSVykO6hvr
|
24
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1 @@
|
|
1
|
+
ce3c7dc0fe6817aee65f990c7a97f89fd36c94380ac804c7579554d665a934df99d4e72ee9b2e467efcc76799a20c4d1de4c950bbba3512d42260c38a46e54b9
|
data/lib/money-tree/address.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
module MoneyTree
|
2
2
|
class Address
|
3
3
|
attr_reader :private_key, :public_key
|
4
|
-
|
4
|
+
|
5
5
|
def initialize(opts = {})
|
6
6
|
private_key = opts.delete(:private_key)
|
7
7
|
@private_key = MoneyTree::PrivateKey.new({ key: private_key }.merge(opts))
|
8
8
|
@public_key = MoneyTree::PublicKey.new(@private_key, opts)
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
def to_s(network: :bitcoin)
|
12
12
|
public_key.to_s(network: network)
|
13
13
|
end
|
data/lib/money-tree/key.rb
CHANGED
@@ -7,12 +7,12 @@ module MoneyTree
|
|
7
7
|
include OpenSSL
|
8
8
|
include Support
|
9
9
|
extend Support
|
10
|
-
class KeyInvalid <
|
11
|
-
class KeyGenerationFailure <
|
12
|
-
class KeyImportFailure <
|
13
|
-
class KeyFormatNotFound <
|
14
|
-
class InvalidWIFFormat <
|
15
|
-
class InvalidBase64Format <
|
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
16
|
|
17
17
|
attr_reader :options, :key, :raw_key
|
18
18
|
attr_accessor :ec_key
|
@@ -70,7 +70,7 @@ module MoneyTree
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def parse_raw_key
|
73
|
-
result = if raw_key.is_a?(
|
73
|
+
result = if raw_key.is_a?(Integer) then from_integer
|
74
74
|
elsif hex_format? then from_hex
|
75
75
|
elsif base64_format? then from_base64
|
76
76
|
elsif compressed_wif_format? then from_wif
|
@@ -81,7 +81,7 @@ module MoneyTree
|
|
81
81
|
result.downcase
|
82
82
|
end
|
83
83
|
|
84
|
-
def
|
84
|
+
def from_integer(bignum = raw_key)
|
85
85
|
# TODO: does this need a byte size specification?
|
86
86
|
int_to_hex(bignum)
|
87
87
|
end
|
@@ -213,7 +213,7 @@ module MoneyTree
|
|
213
213
|
end
|
214
214
|
|
215
215
|
def parse_raw_key
|
216
|
-
result = if raw_key.is_a?(
|
216
|
+
result = if raw_key.is_a?(Integer)
|
217
217
|
set_point raw_key
|
218
218
|
elsif hex_format?
|
219
219
|
set_point hex_to_int(raw_key), compressed: false
|
data/lib/money-tree/node.rb
CHANGED
@@ -5,10 +5,10 @@ module MoneyTree
|
|
5
5
|
attr_reader :private_key, :public_key, :chain_code,
|
6
6
|
:is_private, :depth, :index, :parent
|
7
7
|
|
8
|
-
class PublicDerivationFailure <
|
9
|
-
class InvalidKeyForIndex <
|
10
|
-
class ImportError <
|
11
|
-
class PrivatePublicMismatch <
|
8
|
+
class PublicDerivationFailure < StandardError; end
|
9
|
+
class InvalidKeyForIndex < StandardError; end
|
10
|
+
class ImportError < StandardError; end
|
11
|
+
class PrivatePublicMismatch < StandardError; end
|
12
12
|
|
13
13
|
def initialize(opts = {})
|
14
14
|
opts.each { |k, v| instance_variable_set "@#{k}", v }
|
@@ -90,7 +90,12 @@ module MoneyTree
|
|
90
90
|
left_int = left_from_hash(hash)
|
91
91
|
raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability
|
92
92
|
factor = BN.new left_int.to_s
|
93
|
-
|
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
|
+
|
94
99
|
raise InvalidKeyForIndex, 'at infinity' if child_public_key == 1/0.0 # very low probability
|
95
100
|
child_chain_code = right_from_hash(hash)
|
96
101
|
return child_public_key, child_chain_code
|
data/lib/money-tree/support.rb
CHANGED
@@ -4,11 +4,11 @@ require 'base64'
|
|
4
4
|
module MoneyTree
|
5
5
|
module Support
|
6
6
|
include OpenSSL
|
7
|
-
|
7
|
+
|
8
8
|
INT32_MAX = 256 ** [1].pack("L*").size
|
9
9
|
INT64_MAX = 256 ** [1].pack("Q*").size
|
10
10
|
BASE58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
11
|
-
|
11
|
+
|
12
12
|
def int_to_base58(int_val, leading_zero_bytes=0)
|
13
13
|
base58_val, base = '', BASE58_CHARS.size
|
14
14
|
while int_val > 0
|
@@ -40,7 +40,7 @@ module MoneyTree
|
|
40
40
|
s
|
41
41
|
end
|
42
42
|
alias_method :base58_to_hex, :decode_base58
|
43
|
-
|
43
|
+
|
44
44
|
def to_serialized_base58(hex)
|
45
45
|
hash = sha256 hex
|
46
46
|
hash = sha256 hash
|
@@ -48,7 +48,7 @@ module MoneyTree
|
|
48
48
|
address = hex + checksum
|
49
49
|
encode_base58 address
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
def from_serialized_base58(base58)
|
53
53
|
hex = decode_base58 base58
|
54
54
|
checksum = hex.slice!(-8..-1)
|
@@ -56,45 +56,45 @@ module MoneyTree
|
|
56
56
|
raise EncodingError unless checksum == compare_checksum
|
57
57
|
hex
|
58
58
|
end
|
59
|
-
|
59
|
+
|
60
60
|
def digestify(digest_type, source, opts = {})
|
61
61
|
source = [source].pack("H*") unless opts[:ascii]
|
62
62
|
bytes_to_hex Digest.digest(digest_type, source)
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
def sha256(source, opts = {})
|
66
66
|
digestify('SHA256', source, opts)
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
def ripemd160(source, opts = {})
|
70
70
|
digestify('RIPEMD160', source, opts)
|
71
71
|
end
|
72
|
-
|
72
|
+
|
73
73
|
def encode_base64(hex)
|
74
74
|
Base64.encode64([hex].pack("H*")).chomp
|
75
75
|
end
|
76
|
-
|
76
|
+
|
77
77
|
def decode_base64(base64)
|
78
78
|
Base64.decode64(base64).unpack("H*")[0]
|
79
79
|
end
|
80
|
-
|
80
|
+
|
81
81
|
def hmac_sha512(key, message)
|
82
82
|
digest = Digest::SHA512.new
|
83
83
|
HMAC.digest digest, key, message
|
84
84
|
end
|
85
|
-
|
85
|
+
|
86
86
|
def hmac_sha512_hex(key, message)
|
87
87
|
md = hmac_sha512(key, message)
|
88
88
|
md.unpack("H*").first.rjust(64, '0')
|
89
89
|
end
|
90
|
-
|
90
|
+
|
91
91
|
def bytes_to_int(bytes, base = 16)
|
92
92
|
if bytes.is_a?(Array)
|
93
93
|
bytes = bytes.pack("C*")
|
94
94
|
end
|
95
95
|
bytes.unpack("H*")[0].to_i(16)
|
96
96
|
end
|
97
|
-
|
97
|
+
|
98
98
|
def int_to_hex(i, size=nil)
|
99
99
|
hex = i.to_s(16).downcase
|
100
100
|
if (hex.size % 2) != 0
|
@@ -107,19 +107,19 @@ module MoneyTree
|
|
107
107
|
hex
|
108
108
|
end
|
109
109
|
end
|
110
|
-
|
110
|
+
|
111
111
|
def int_to_bytes(i)
|
112
112
|
[int_to_hex(i)].pack("H*")
|
113
113
|
end
|
114
|
-
|
114
|
+
|
115
115
|
def bytes_to_hex(bytes)
|
116
116
|
bytes.unpack("H*")[0].downcase
|
117
117
|
end
|
118
|
-
|
118
|
+
|
119
119
|
def hex_to_bytes(hex)
|
120
120
|
[hex].pack("H*")
|
121
121
|
end
|
122
|
-
|
122
|
+
|
123
123
|
def hex_to_int(hex)
|
124
124
|
hex.to_i(16)
|
125
125
|
end
|
data/lib/money-tree/version.rb
CHANGED
data/lib/money-tree.rb
CHANGED
data/lib/openssl_extensions.rb
CHANGED
@@ -1,73 +1,28 @@
|
|
1
1
|
# encoding: ascii-8bit
|
2
2
|
|
3
3
|
require 'openssl'
|
4
|
-
require 'ffi'
|
5
4
|
|
6
5
|
module MoneyTree
|
7
6
|
module OpenSSLExtensions
|
8
|
-
extend FFI::Library
|
9
|
-
ffi_lib 'ssl'
|
10
|
-
|
11
|
-
NID_secp256k1 = 714
|
12
|
-
POINT_CONVERSION_COMPRESSED = 2
|
13
|
-
POINT_CONVERSION_UNCOMPRESSED = 4
|
14
|
-
|
15
|
-
attach_function :EC_KEY_free, [:pointer], :int
|
16
|
-
attach_function :EC_KEY_get0_group, [:pointer], :pointer
|
17
|
-
attach_function :EC_KEY_new_by_curve_name, [:int], :pointer
|
18
|
-
attach_function :EC_POINT_clear_free, [:pointer], :int
|
19
|
-
attach_function :EC_POINT_add, [:pointer, :pointer, :pointer, :pointer, :pointer], :int
|
20
|
-
attach_function :EC_POINT_point2hex, [:pointer, :pointer, :int, :pointer], :string
|
21
|
-
attach_function :EC_POINT_hex2point, [:pointer, :string, :pointer, :pointer], :pointer
|
22
|
-
attach_function :EC_POINT_new, [:pointer], :pointer
|
23
|
-
|
24
7
|
def self.add(point_0, point_1)
|
25
8
|
validate_points(point_0, point_1)
|
26
|
-
|
27
|
-
group = EC_KEY_get0_group(eckey)
|
28
|
-
|
9
|
+
group = OpenSSL::PKey::EC::Group.new('secp256k1')
|
29
10
|
point_0_hex = point_0.to_bn.to_s(16)
|
30
|
-
point_0_pt =
|
11
|
+
point_0_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_0_hex, 16))
|
31
12
|
point_1_hex = point_1.to_bn.to_s(16)
|
32
|
-
point_1_pt =
|
33
|
-
|
34
|
-
sum_point
|
35
|
-
success = EC_POINT_add(group, sum_point, point_0_pt, point_1_pt, nil)
|
36
|
-
hex = EC_POINT_point2hex(group, sum_point, POINT_CONVERSION_UNCOMPRESSED, nil)
|
37
|
-
|
38
|
-
EC_KEY_free(eckey)
|
39
|
-
EC_POINT_clear_free(sum_point)
|
40
|
-
EC_POINT_clear_free(point_0_pt)
|
41
|
-
EC_POINT_clear_free(point_1_pt)
|
42
|
-
|
43
|
-
eckey = nil
|
44
|
-
group = nil
|
45
|
-
sum_point = nil
|
46
|
-
point_0_pt = nil
|
47
|
-
point_1_pt = nil
|
48
|
-
|
49
|
-
hex
|
13
|
+
point_1_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_1_hex, 16))
|
14
|
+
sum_point = point_0_pt.add(point_1_pt)
|
15
|
+
sum_point.to_bn.to_s(16)
|
50
16
|
end
|
51
17
|
|
52
18
|
def self.validate_points(*points)
|
53
19
|
points.each do |point|
|
54
20
|
if !point.is_a?(OpenSSL::PKey::EC::Point)
|
55
|
-
raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
|
21
|
+
raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
|
56
22
|
elsif point.infinity?
|
57
|
-
raise ArgumentError, "point must not be infinity"
|
23
|
+
raise ArgumentError, "point must not be infinity"
|
58
24
|
end
|
59
25
|
end
|
60
26
|
end
|
61
27
|
end
|
62
28
|
end
|
63
|
-
|
64
|
-
|
65
|
-
class OpenSSL::PKey::EC::Point
|
66
|
-
include MoneyTree::OpenSSLExtensions
|
67
|
-
|
68
|
-
def add(point)
|
69
|
-
sum_point_hex = MoneyTree::OpenSSLExtensions.add(self, point)
|
70
|
-
self.class.new group, OpenSSL::BN.new(sum_point_hex, 16)
|
71
|
-
end
|
72
|
-
|
73
|
-
end
|
data/money-tree.gemspec
CHANGED
@@ -6,33 +6,22 @@ require 'money-tree/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "money-tree"
|
8
8
|
spec.version = MoneyTree::VERSION
|
9
|
-
spec.authors = ["Micah Winkelspecht"]
|
10
|
-
spec.email = ["winkelspecht@gmail.com"]
|
9
|
+
spec.authors = ["Micah Winkelspecht", "Afri Schoedon"]
|
10
|
+
spec.email = ["winkelspecht@gmail.com", "gems@q9f.cc"]
|
11
11
|
spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets}
|
12
12
|
spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
|
13
|
-
spec.homepage = ""
|
13
|
+
spec.homepage = "https://github.com/gemhq/money-tree"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
|
-
|
21
|
-
# used with gem i coin-op -P HighSecurity
|
22
|
-
spec.cert_chain = ["certs/jvergeldedios.pem"]
|
23
|
-
# Sign gem when evaluating spec with `gem` command
|
24
|
-
# unless ENV has set a SKIP_GEM_SIGNING
|
25
|
-
if ($0 =~ /gem\z/) and not ENV.include?("SKIP_GEM_SIGNING")
|
26
|
-
spec.signing_key = File.join(Gem.user_home, ".ssh", "gem-private_key.pem")
|
27
|
-
end
|
28
20
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
spec.add_development_dependency "
|
33
|
-
spec.add_development_dependency "
|
34
|
-
spec.add_development_dependency "
|
35
|
-
spec.add_development_dependency "simplecov"
|
36
|
-
spec.add_development_dependency "coveralls"
|
37
|
-
spec.add_development_dependency "pry"
|
21
|
+
spec.add_dependency 'openssl', '>= 2.2'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 2.2"
|
24
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.10"
|
26
|
+
spec.add_development_dependency "pry", "~> 0.4"
|
38
27
|
end
|
@@ -4,14 +4,20 @@ describe MoneyTree::Address do
|
|
4
4
|
describe "initialize" do
|
5
5
|
it "generates a private key by default" do
|
6
6
|
address = MoneyTree::Address.new
|
7
|
+
expect(address).to be
|
8
|
+
expect(address).to be_instance_of MoneyTree::Address
|
9
|
+
expect(address.private_key).to be_instance_of MoneyTree::PrivateKey
|
7
10
|
expect(address.private_key.key.length).to eql(64)
|
8
11
|
end
|
9
|
-
|
12
|
+
|
10
13
|
it "generates a public key by default" do
|
11
14
|
address = MoneyTree::Address.new
|
15
|
+
expect(address).to be
|
16
|
+
expect(address).to be_instance_of MoneyTree::Address
|
17
|
+
expect(address.public_key).to be_instance_of MoneyTree::PublicKey
|
12
18
|
expect(address.public_key.key.length).to eql(66)
|
13
19
|
end
|
14
|
-
|
20
|
+
|
15
21
|
it "imports a private key in hex form" do
|
16
22
|
address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
|
17
23
|
expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
|
@@ -20,36 +26,46 @@ describe MoneyTree::Address do
|
|
20
26
|
expect(address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
|
21
27
|
expect(address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
22
28
|
end
|
23
|
-
|
29
|
+
|
24
30
|
it "imports a private key in compressed wif format" do
|
25
31
|
address = MoneyTree::Address.new private_key: "KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK"
|
26
32
|
expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
|
27
33
|
expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
|
28
34
|
expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
29
35
|
end
|
30
|
-
|
36
|
+
|
31
37
|
it "imports a private key in uncompressed wif format" do
|
32
38
|
address = MoneyTree::Address.new private_key: "5JXz5ZyFk31oHVTQxqce7yitCmTAPxBqeGQ4b7H3Aj3L45wUhoa"
|
33
39
|
expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
|
34
40
|
expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
|
35
41
|
end
|
36
42
|
end
|
37
|
-
|
43
|
+
|
38
44
|
describe "to_s" do
|
39
45
|
before do
|
40
46
|
@address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
|
41
47
|
end
|
42
|
-
|
48
|
+
|
43
49
|
it "returns compressed base58 public key" do
|
44
50
|
expect(@address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
45
51
|
expect(@address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
|
46
52
|
end
|
47
|
-
|
53
|
+
|
48
54
|
it "returns compressed WIF private key" do
|
49
55
|
expect(@address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
|
50
56
|
end
|
51
57
|
end
|
52
58
|
|
59
|
+
context "bitcoin wiki" do
|
60
|
+
# ref https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses
|
61
|
+
subject(:wiki) { MoneyTree::Address.new private_key: '18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725' }
|
62
|
+
|
63
|
+
it "always regenerates the bitcoin wiki example" do
|
64
|
+
expect(wiki.public_key.key).to eq "0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352"
|
65
|
+
expect(wiki.to_s).to eq "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
53
69
|
context "testnet3" do
|
54
70
|
before do
|
55
71
|
@address = MoneyTree::Address.new network: :bitcoin_testnet
|