diff --git a/.simplecov b/.simplecov index 3ce85da..af7f103 100644 --- a/.simplecov +++ b/.simplecov @@ -1,7 +1,7 @@ require 'coveralls' -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ +SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter -] +]) SimpleCov.start diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..5cc4965 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,62 @@ +PATH + remote: . + specs: + money-tree (0.10.0) + ffi + +GEM + remote: https://rubygems.org/ + specs: + coderay (1.1.2) + coveralls (0.8.22) + json (>= 1.8, < 3) + simplecov (~> 0.16.1) + term-ansicolor (~> 1.3) + thor (~> 0.19.4) + tins (~> 1.6) + diff-lcs (1.3) + docile (1.3.1) + ffi (1.9.25) + json (2.1.0) + method_source (0.9.0) + pry (0.11.3) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + rake (12.3.1) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + simplecov (0.16.1) + docile (~> 1.1) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) + term-ansicolor (1.6.0) + tins (~> 1.0) + thor (0.19.4) + tins (1.16.3) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.3) + coveralls + money-tree! + pry + rake + rspec + simplecov + +BUNDLED WITH + 1.16.1 diff --git a/README.md b/README.md index aa425bb..70a85db 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ These instructions assume you have a decent understanding of how Bitcoin wallets ### Create a Master Node (seed) -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). +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). 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. @@ -109,19 +109,19 @@ DO NOT use a user generated password. Keep in mind that whoever controls the see @master.public_key.to_hex => "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2" -@master.chain_code_hex +@master.chain_code_hex => "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" # Look up chain codes in the BIP0032 spec @master.to_serialized_hex(:private) => "0488ade4000000000000000000873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d50800e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" -@master.to_serialized_address(:private) +@master.to_bip32(:private) => "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" @master.to_serialized_hex => "0488b21e000000000000000000873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d5080339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2" -@master.to_serialized_address +@master.to_bip32 => "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" ``` @@ -140,10 +140,10 @@ To generate a child node from a given path: @node.depth => 2 -@node.to_serialized_address(:private) +@node.to_bip32(:private) => "xprv9ww7sMFLzJN15m7zX5JEBXQrQq8h4fU8PVqd929Hjy3xNSMzeBf163idMNBSq47DdCakyZTK7KcC2nbz3jqUkpJj8ZR4FqrijcFcFmcoBAe" -@node.to_serialized_address +@node.to_bip32 => "xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr" ``` @@ -153,7 +153,7 @@ In HD wallets, chain codes are the mathematical glue that binds a parent node to You don't need to worry about chain codes if you are creating or importing from a Master key (it's always the same for all HD wallet master keys), however if you are trying to import a derived child key at some lower depth in the tree, you'll need the chain code. Luckily, whenever we export a node to a wallet file, we encode it in a special format that includes all of the relevant info (including chain code) that we need to reconstruct the node in a single convenient serialized address. #### Serialized Addresses -Beacause we need multiple pieces of info to reconstruct nodes in a tree, when we're dealing with HD wallets, we pass around a serialized address format that encodes both the key and the chain code. It looks like this: +Because we need multiple pieces of info to reconstruct nodes in a tree, when we're dealing with HD wallets, we pass around a serialized address format that encodes both the key and the chain code. It looks like this: ```ruby # private key @@ -164,22 +164,22 @@ Beacause we need multiple pieces of info to reconstruct nodes in a tree, when we ``` 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). - + 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. To export a node to a serialized address, you can do: ```ruby -@node.to_serialized_address(:private) # for private keys +@node.to_bip32(:private) # for private keys => "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" -@node.to_serialized_address +@node.to_bip32 => "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" ``` - + To import from a serialized address: (either public or private) ```ruby -@node = MoneyTree::Node.from_serialized_address "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" +@node = MoneyTree::Node.from_bip32 "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" => MoneyTree::Node instance ``` @@ -198,10 +198,10 @@ For example: ```ruby @node = @master.node_for_path("M/0/3") # or "m/0/3.pub" or "M/0/3.pub"...these are equivalent -@node.to_serialized_address +@node.to_bip32 => "xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr" -@node.to_serialized_address(:private) +@node.to_bip32(:private) -> raises MoneyTree::Node::PrivatePublicMismatch error ``` @@ -209,13 +209,13 @@ For example: 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. ```ruby -@node = MoneyTree::Node.from_serialized_address("xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr") +@node = MoneyTree::Node.from_bip32("xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr") => MoneyTree::Node instance -@node.to_serialized_address +@node.to_bip32 => "xpub6AvUGrnEpfvJJFCTd6qEYfMaxryBU8BykimDwQYuJJawFEh9BiyFdr37Cc4wEKCWWv7TsFQRUMdezXVqV9cfBUbeUEgNYCCP4omxULbNaRr" -@node.to_serialized_address(:private) +@node.to_bip32(:private) -> raises MoneyTree::Node::PrivatePublicMismatch error ``` @@ -241,7 +241,7 @@ For instance: 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. -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"`. +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) ## Contributing @@ -251,3 +251,8 @@ To add just a little more confusion to the mix, some wallets will use negative ` 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request + +## Known Issues (PRs welcome) + +- Segwit ([BIP49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki)) address generation is not supported +- Use of negative integers in paths does _not_ produce the correct hardened derivation. diff --git a/certs/mattatgemco.pem b/certs/mattatgemco.pem new file mode 100644 index 0000000..afcbcd5 --- /dev/null +++ b/certs/mattatgemco.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEHDCCAoSgAwIBAgIBATANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFtYXR0 +L0RDPWdlbS9EQz1jbzAeFw0xODA1MzAyMTQwNDZaFw0xOTA1MzAyMTQwNDZaMBwx +GjAYBgNVBAMMEW1hdHQvREM9Z2VtL0RDPWNvMIIBojANBgkqhkiG9w0BAQEFAAOC +AY8AMIIBigKCAYEAxfbjMHFlxA2P+4YWPagKoGAMi4078imgXdFbD3Rloe6cGfYp +IMUQitiHrKi6fhSE0UjXmoP3qnYFddm1enN9zUAFRhHWv7xpINqSqss4PYAb5Anl +RYZu3jromop5aVodi15HUfu5z27MvBm4rAaN/dDRfh/rT2hDbTTh0HmvEaPUDfX6 +TyflAttfabFvtY4qsD+ao8tks0DytqyuEWZ0tvQ6upOgHRNNuYDwDZB1T9v2dq2w +3goJFmOKBMMn7UH8WMjD3HiOuRD4tWhq5xWLjBqjzFlVPlZPgdCNyXeMMnLXER98 +NY35cVWFFuqG+kZwy4MFKdE9WFTocLZxLFo0VVTNSpPara9HirbHtIo9jZNuop4S +g4JTf1F8dIWYii3sXoAYZfkl6rHVRP0G/OV5LcTfSS3QkmI5hNltz5FZzc+qI6S1 +rTR1ZwTy1rRI3coFY7vDRaFWBoMbbo/DytgCE3+rfbVDxQrJa4aZ0iYDhu8LXEA1 +VTtpf1EWYCOsYE1TAgMBAAGjaTBnMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0G +A1UdDgQWBBQ6QoDNre7LFgOukH2Cv+RqZyfUzjAWBgNVHREEDzANgQttYXR0QGdl +bS5jbzAWBgNVHRIEDzANgQttYXR0QGdlbS5jbzANBgkqhkiG9w0BAQsFAAOCAYEA +kOxYnOsB+NwHwLc2lHEZ8ubxanq2qIZDhvVQ4M31gwmba43xO7vq0ktFxYRvozs4 +74dQ6bmY2e7njoFgeutyJwxulA+BC71mDQA1s4WsZo7Z2TRgB0GViVqHrzq+jY+M +p9mTHQqKH+2j0P9T4DXSzq4qOaBA3YROAwAzYI9N8MObeWkRt2pZ4zYQrAniP2nd +wzXs/G5lWbbntVcvQOfAAXBipSJ3X5P2EGpUytP9ZpGdezY5HZzuiJFcmCf1CM3t +VX4NZjbJak9gOY0AFD0Aw497sYenm0VBExclOmeRuZLffpWteTTL//utpG3bbFPl +jQ78uzsrexYTYW5IshjfSIf3TZxm50Z45pyOTow5EOP1Nd7OmKOcI8hrLGv5+AlD +hCnomUTUNsM4Rjwl5rzQiIn3ezv6+0tlg4rWJmVTuOGwcHk/oj1In2sPjCqm0pgx +TLnMa8gr6aUpuHR5s2N4ZH0Q2YIsaD6cv7DYXt+G4MRut3njOYHfkqsSVykO6hvr +-----END CERTIFICATE----- diff --git a/checksum/money-tree-0.9.0.gem.sha512 b/checksum/money-tree-0.9.0.gem.sha512 new file mode 100644 index 0000000..7d2a8af --- /dev/null +++ b/checksum/money-tree-0.9.0.gem.sha512 @@ -0,0 +1 @@ +ce3c7dc0fe6817aee65f990c7a97f89fd36c94380ac804c7579554d665a934df99d4e72ee9b2e467efcc76799a20c4d1de4c950bbba3512d42260c38a46e54b9 \ No newline at end of file diff --git a/lib/money-tree/address.rb b/lib/money-tree/address.rb index cf7ad53..d0de722 100644 --- a/lib/money-tree/address.rb +++ b/lib/money-tree/address.rb @@ -8,8 +8,8 @@ def initialize(opts = {}) @public_key = MoneyTree::PublicKey.new(@private_key, opts) end - def to_s - public_key.to_s + def to_s(network: :bitcoin) + public_key.to_s(network: network) end end diff --git a/lib/money-tree/key.rb b/lib/money-tree/key.rb index 630b6ec..b8a122f 100644 --- a/lib/money-tree/key.rb +++ b/lib/money-tree/key.rb @@ -7,16 +7,16 @@ class Key include OpenSSL include Support extend Support - class KeyInvalid < Exception; end - class KeyGenerationFailure < Exception; end - class KeyImportFailure < Exception; end - class KeyFormatNotFound < Exception; end - class InvalidWIFFormat < Exception; end - class InvalidBase64Format < Exception; end - - attr_reader :options, :key, :raw_key, :network, :network_key + class KeyInvalid < StandardError; end + class KeyGenerationFailure < StandardError; end + class KeyImportFailure < StandardError; end + class KeyFormatNotFound < StandardError; end + class InvalidWIFFormat < StandardError; end + class InvalidBase64Format < StandardError; end + + attr_reader :options, :key, :raw_key attr_accessor :ec_key - + GROUP_NAME = 'secp256k1' ORDER = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141".to_i(16) @@ -24,23 +24,21 @@ def valid?(eckey = nil) eckey ||= ec_key eckey.nil? ? false : eckey.check_key end - + def to_bytes hex_to_bytes to_hex end - + def to_i bytes_to_int to_bytes end end - + class PrivateKey < Key - + def initialize(opts = {}) @options = opts @ec_key = PKey::EC.new GROUP_NAME - @network_key = options[:network] || :bitcoin - @network = MoneyTree::NETWORKS[network_key] if @options[:key] @raw_key = @options[:key] @key = parse_raw_key @@ -50,40 +48,40 @@ def initialize(opts = {}) @key = to_hex end end - + def generate ec_key.generate_key end - + def import ec_key.private_key = BN.new(key, 16) set_public_key end - + def calculate_public_key(opts = {}) opts[:compressed] = true unless opts[:compressed] == false group = ec_key.group group.point_conversion_form = opts[:compressed] ? :compressed : :uncompressed point = group.generator.mul ec_key.private_key end - + def set_public_key(opts = {}) ec_key.public_key = calculate_public_key(opts) end - + def parse_raw_key - result = if raw_key.is_a?(Bignum) then from_bignum + result = if raw_key.is_a?(Integer) then from_integer elsif hex_format? then from_hex elsif base64_format? then from_base64 elsif compressed_wif_format? then from_wif elsif uncompressed_wif_format? then from_wif - else + else raise KeyFormatNotFound end result.downcase end - def from_bignum(bignum = raw_key) + def from_integer(bignum = raw_key) # TODO: does this need a byte size specification? int_to_hex(bignum) end @@ -91,25 +89,15 @@ def from_bignum(bignum = raw_key) def from_hex(hex = raw_key) hex end - + def from_wif(wif = raw_key) compressed = wif.length == 52 - parse_network_from_wif(wif, compressed: compressed) validate_wif(wif) hex = decode_base58(wif) last_char = compressed ? -11 : -9 hex.slice(2..last_char) end - def parse_network_from_wif(wif, opts = {}) - networks = MoneyTree::NETWORKS - chars_key = opts[:compressed] ? :compressed_wif_chars : :uncompressed_wif_chars - @network_key = networks.keys.select do |k| - networks[k][chars_key].include?(wif.slice(0)) - end.first - @network = networks[network_key] - end - def from_base64(base64_key = raw_key) raise InvalidBase64Format unless base64_format?(base64_key) decode_base64(base64_key) @@ -118,7 +106,7 @@ def from_base64(base64_key = raw_key) def compressed_wif_format? wif_format?(:compressed) end - + def uncompressed_wif_format? wif_format?(:uncompressed) end @@ -132,19 +120,18 @@ def wif_format?(compression) def base64_format?(base64_key = raw_key) base64_key.length == 44 && base64_key =~ /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/ end - + def hex_format? raw_key.length == 64 && !raw_key[/\H/] end - + def to_hex int_to_hex @ec_key.private_key, 64 end - - def to_wif(opts = {}) - opts[:compressed] = true unless opts[:compressed] == false - source = network[:privkey_version] + to_hex - source += network[:privkey_compression_flag] if opts[:compressed] + + def to_wif(compressed: true, network: :bitcoin) + source = NETWORKS[network][:privkey_version] + to_hex + source += NETWORKS[network][:privkey_compression_flag] if compressed hash = sha256(source) hash = sha256(hash) checksum = hash.slice(0..7) @@ -154,7 +141,6 @@ def to_wif(opts = {}) def wif_valid?(wif) hex = decode_base58(wif) - return false unless hex.slice(0..1) == network[:privkey_version] checksum = hex.chars.to_a.pop(8).join source = hex.slice(0..-9) hash = sha256(source) @@ -162,65 +148,61 @@ def wif_valid?(wif) hash_checksum = hash.slice(0..7) checksum == hash_checksum end - + def validate_wif(wif) raise InvalidWIFFormat unless wif_valid?(wif) end - + def to_base64 encode_base64(to_hex) end - - def to_s - to_wif + + def to_s(network: :bitcoin) + to_wif(network: network) end - + end - + class PublicKey < Key attr_reader :private_key, :point, :group, :key_int - + def initialize(p_key, opts = {}) @options = opts @options[:compressed] = true if @options[:compressed].nil? - if p_key.is_a?(PrivateKey) @private_key = p_key - @network_key = private_key.network_key - @network = MoneyTree::NETWORKS[network_key] @point = @private_key.calculate_public_key(@options) @group = @point.group @key = @raw_key = to_hex else - @network_key = @options[:network] || :bitcoin - @network = MoneyTree::NETWORKS[network_key] @raw_key = p_key @group = PKey::EC::Group.new GROUP_NAME @key = parse_raw_key end + raise ArgumentError, "Must initialize with a MoneyTree::PrivateKey or a public key value" if @key.nil? end - + def compression @group.point_conversion_form end - + def compression=(compression_type = :compressed) @group.point_conversion_form = compression_type end - + def compressed compressed_key = self.class.new raw_key, options # deep clone compressed_key.set_point to_i, compressed: true compressed_key end - + def uncompressed uncompressed_key = self.class.new raw_key, options # deep clone uncompressed_key.set_point to_i, compressed: false uncompressed_key end - + def set_point(int = to_i, opts = {}) opts = options.merge(opts) opts[:compressed] = true if opts[:compressed].nil? @@ -229,53 +211,53 @@ def set_point(int = to_i, opts = {}) @point = PKey::EC::Point.new group, bn raise KeyInvalid, 'point is not on the curve' unless @point.on_curve? end - + def parse_raw_key - result = if raw_key.is_a?(Bignum) + result = if raw_key.is_a?(Integer) set_point raw_key elsif hex_format? set_point hex_to_int(raw_key), compressed: false elsif compressed_hex_format? set_point hex_to_int(raw_key), compressed: true - else + else raise KeyFormatNotFound end to_hex end - + def hex_format? raw_key.length == 130 && !raw_key[/\H/] end - + def compressed_hex_format? raw_key.length == 66 && !raw_key[/\H/] end - + def to_hex int_to_hex to_i, 66 end - + def to_i point.to_bn.to_i end - + def to_ripemd160 hash = sha256 to_hex ripemd160 hash end - - def to_address + + def to_address(network: :bitcoin) hash = to_ripemd160 - address = network[:address_version] + hash + address = NETWORKS[network][:address_version] + hash to_serialized_base58 address end alias :to_s :to_address - + def to_fingerprint hash = to_ripemd160 hash.slice(0..7) end - + def to_bytes int_to_bytes to_i end diff --git a/lib/money-tree/networks.rb b/lib/money-tree/networks.rb index 0df0410..291d613 100644 --- a/lib/money-tree/networks.rb +++ b/lib/money-tree/networks.rb @@ -1,28 +1,35 @@ module MoneyTree - NETWORKS = { - bitcoin: { - address_version: '00', - p2sh_version: '05', - p2sh_char: '3', - privkey_version: '80', - privkey_compression_flag: '01', - extended_privkey_version: "0488ade4", - extended_pubkey_version: "0488b21e", - compressed_wif_chars: %w(K L), - uncompressed_wif_chars: %w(5), - protocol_version: 70001 - }, - bitcoin_testnet: { - address_version: '6f', - p2sh_version: 'c4', - p2sh_char: '2', - privkey_version: 'ef', - privkey_compression_flag: '01', - extended_privkey_version: "04358394", - extended_pubkey_version: "043587cf", - compressed_wif_chars: %w(c), - uncompressed_wif_chars: %w(9), - protocol_version: 70001 - } - } + NETWORKS = + begin + hsh = Hash.new do |_, key| + raise "#{key} is not a valid network!" + end.merge( + bitcoin: { + address_version: '00', + p2sh_version: '05', + p2sh_char: '3', + privkey_version: '80', + privkey_compression_flag: '01', + extended_privkey_version: "0488ade4", + extended_pubkey_version: "0488b21e", + compressed_wif_chars: %w(K L), + uncompressed_wif_chars: %w(5), + protocol_version: 70001 + }, + bitcoin_testnet: { + address_version: '6f', + p2sh_version: 'c4', + p2sh_char: '2', + privkey_version: 'ef', + privkey_compression_flag: '01', + extended_privkey_version: "04358394", + extended_pubkey_version: "043587cf", + compressed_wif_chars: %w(c), + uncompressed_wif_chars: %w(9), + protocol_version: 70001 + } + ) + hsh[:testnet3] = hsh[:bitcoin_testnet] + hsh + end end diff --git a/lib/money-tree/node.rb b/lib/money-tree/node.rb index 78d5748..9def6bf 100644 --- a/lib/money-tree/node.rb +++ b/lib/money-tree/node.rb @@ -2,62 +2,52 @@ module MoneyTree class Node include Support extend Support - attr_reader :private_key, :public_key, :chain_code, - :is_private, :depth, :index, :parent, :network, :network_key - - class PublicDerivationFailure < Exception; end - class InvalidKeyForIndex < Exception; end - class ImportError < Exception; end - class PrivatePublicMismatch < Exception; end - + attr_reader :private_key, :public_key, :chain_code, + :is_private, :depth, :index, :parent + + class PublicDerivationFailure < StandardError; end + class InvalidKeyForIndex < StandardError; end + class ImportError < StandardError; end + class PrivatePublicMismatch < StandardError; end + def initialize(opts = {}) - @network_key = opts.delete(:network) || :bitcoin - @network = MoneyTree::NETWORKS[network_key] opts.each { |k, v| instance_variable_set "@#{k}", v } end - - def self.from_serialized_address(address) + + def self.from_bip32(address, has_version: true) hex = from_serialized_base58 address - version = from_version_hex hex.slice!(0..7) + hex.slice!(0..7) if has_version self.new({ depth: hex.slice!(0..1).to_i(16), parent_fingerprint: hex.slice!(0..7), index: hex.slice!(0..7).to_i(16), chain_code: hex.slice!(0..63).to_i(16) - }.merge(key_options(hex, version))) - end - - def self.key_options(hex, version) - k_opts = { network: version[:network] } - if version[:private_key] && hex.slice(0..1) == '00' - private_key = MoneyTree::PrivateKey.new({ key: hex.slice(2..-1) }.merge(k_opts)) - k_opts.merge private_key: private_key, public_key: MoneyTree::PublicKey.new(private_key) + }.merge(parse_out_key(hex))) + end + + def self.from_serialized_address(address) + puts 'Node.from_serialized_address is DEPRECATED. Please use .from_bip32 instead.' + from_bip32(address) + end + + def self.parse_out_key(hex) + if hex.slice(0..1) == '00' + private_key = MoneyTree::PrivateKey.new(key: hex.slice(2..-1)) + { + private_key: private_key, + public_key: MoneyTree::PublicKey.new(private_key) + } elsif %w(02 03).include? hex.slice(0..1) - k_opts.merge public_key: MoneyTree::PublicKey.new(hex, k_opts) + { public_key: MoneyTree::PublicKey.new(hex) } else raise ImportError, 'Public or private key data does not match version type' end end - - def self.from_version_hex(hex) - case hex - when MoneyTree::NETWORKS[:bitcoin][:extended_privkey_version] - { private_key: true, network: :bitcoin } - when MoneyTree::NETWORKS[:bitcoin][:extended_pubkey_version] - { private_key: false, network: :bitcoin } - when MoneyTree::NETWORKS[:bitcoin_testnet][:extended_privkey_version] - { private_key: true, network: :bitcoin_testnet } - when MoneyTree::NETWORKS[:bitcoin_testnet][:extended_pubkey_version] - { private_key: false, network: :bitcoin_testnet } - else - raise ImportError, 'invalid version bytes' - end - end - + def is_private? index >= 0x80000000 || index < 0 end - + def index_hex(i = index) if i < 0 [i].pack('l>').unpack('H*').first @@ -65,15 +55,15 @@ def index_hex(i = index) i.to_s(16).rjust(8, "0") end end - + def depth_hex(depth) depth.to_s(16).rjust(2, "0") end - + def private_derivation_message(i) "\x00" + private_key.to_bytes + i_as_bytes(i) end - + def public_derivation_message(i) public_key.to_bytes << i_as_bytes(i) end @@ -81,10 +71,10 @@ def public_derivation_message(i) def i_as_bytes(i) [i].pack('N') end - + def derive_private_key(i = 0) message = i >= 0x80000000 || i < 0 ? private_derivation_message(i) : public_derivation_message(i) - hash = hmac_sha512 int_to_bytes(chain_code), message + hash = hmac_sha512 hex_to_bytes(chain_code_hex), message left_int = left_from_hash(hash) raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability child_private_key = (left_int + private_key.to_i) % MoneyTree::Key::ORDER @@ -92,11 +82,11 @@ def derive_private_key(i = 0) child_chain_code = right_from_hash(hash) return child_private_key, child_chain_code end - + def derive_public_key(i = 0) raise PrivatePublicMismatch if i >= 0x80000000 message = public_derivation_message(i) - hash = hmac_sha512 int_to_bytes(chain_code), message + hash = hmac_sha512 hex_to_bytes(chain_code_hex), message left_int = left_from_hash(hash) raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability factor = BN.new left_int.to_s @@ -105,39 +95,45 @@ def derive_public_key(i = 0) child_chain_code = right_from_hash(hash) return child_public_key, child_chain_code end - + def left_from_hash(hash) bytes_to_int hash.bytes.to_a[0..31] end - + def right_from_hash(hash) bytes_to_int hash.bytes.to_a[32..-1] end - def to_serialized_hex(type = :public) + def to_serialized_hex(type = :public, network: :bitcoin) raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil? version_key = type.to_sym == :private ? :extended_privkey_version : :extended_pubkey_version - hex = network[version_key] # version (4 bytes) + hex = NETWORKS[network][version_key] # version (4 bytes) hex += depth_hex(depth) # depth (1 byte) hex += parent_fingerprint # fingerprint of key (4 bytes) hex += index_hex(index) # child number i (4 bytes) hex += chain_code_hex hex += type.to_sym == :private ? "00#{private_key.to_hex}" : public_key.compressed.to_hex end - - def to_serialized_address(type = :public) + + def to_bip32(type = :public, network: :bitcoin) raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil? - to_serialized_base58 to_serialized_hex(type) + to_serialized_base58 to_serialized_hex(type, network: network) end - - def to_identifier - public_key.compressed.to_ripemd160 + + def to_serialized_address(type = :public, network: :bitcoin) + puts 'Node.to_serialized_address is DEPRECATED. Please use .to_bip32.' + to_bip32(type, network: network) + end + + def to_identifier(compressed=true) + key = compressed ? public_key.compressed : public_key.uncompressed + key.to_ripemd160 end - + def to_fingerprint public_key.compressed.to_fingerprint end - + def parent_fingerprint if @parent_fingerprint @parent_fingerprint @@ -146,40 +142,39 @@ def parent_fingerprint end end - def to_address - address = network[:address_version] + to_identifier + def to_address(compressed=true, network: :bitcoin) + address = NETWORKS[network][:address_version] + to_identifier(compressed) to_serialized_base58 address end - + def subnode(i = 0, opts = {}) if private_key.nil? child_public_key, child_chain_code = derive_public_key(i) - child_public_key = MoneyTree::PublicKey.new child_public_key, network: network_key + child_public_key = MoneyTree::PublicKey.new child_public_key else child_private_key, child_chain_code = derive_private_key(i) - child_private_key = MoneyTree::PrivateKey.new key: child_private_key, network: network_key + child_private_key = MoneyTree::PrivateKey.new key: child_private_key child_public_key = MoneyTree::PublicKey.new child_private_key end - - MoneyTree::Node.new network: network_key, - depth: depth+1, - index: i, + + MoneyTree::Node.new( depth: depth+1, + index: i, private_key: private_key.nil? ? nil : child_private_key, public_key: child_public_key, chain_code: child_chain_code, - parent: self + parent: self) end - + # path: a path of subkeys denoted by numbers and slashes. Use # p or i<0 for private key derivation. End with .pub to force # the key public. - # + # # Examples: # 1p/-5/2/1 would call subkey(i=1, is_prime=True).subkey(i=-5). # subkey(i=2).subkey(i=1) and then yield the private key # 0/0/458.pub would call subkey(i=0).subkey(i=0).subkey(i=458) and # then yield the public key - # + # # You should choose either the p or the negative number convention for private key derivation. def node_for_path(path) force_public = path[-4..-1] == '.pub' @@ -203,11 +198,11 @@ def node_for_path(path) nodes.last end end - + def parse_index(path_part) is_prime = %w(p ').include? path_part[-1] i = path_part.to_i - + i = if i < 0 i elsif is_prime @@ -216,16 +211,16 @@ def parse_index(path_part) i & 0x7fffffff end end - + def strip_private_info! @private_key = nil end - + def chain_code_hex int_to_hex chain_code, 64 end end - + class Master < Node module SeedGeneration class Failure < Exception; end @@ -235,18 +230,16 @@ class ValidityError < Failure; end class ImportError < Failure; end class TooManyAttempts < Failure; end end - + HD_WALLET_BASE_KEY = "Bitcoin seed" RANDOM_SEED_SIZE = 32 - + attr_reader :seed, :seed_hash - + def initialize(opts = {}) @depth = 0 @index = 0 opts[:seed] = [opts[:seed_hex]].pack("H*") if opts[:seed_hex] - @network_key = opts[:network] || :bitcoin - @network = MoneyTree::NETWORKS[network_key] if opts[:seed] @seed = opts[:seed] @seed_hash = generate_seed_hash(@seed) @@ -257,14 +250,12 @@ def initialize(opts = {}) @chain_code = opts[:chain_code] if opts[:private_key] @private_key = opts[:private_key] - @network_key = @private_key.network_key - @network = MoneyTree::NETWORKS[network_key] @public_key = MoneyTree::PublicKey.new @private_key else opts[:public_key] @public_key = if opts[:public_key].is_a?(MoneyTree::PublicKey) opts[:public_key] else - MoneyTree::PublicKey.new(opts[:public_key], network: network_key) + MoneyTree::PublicKey.new(opts[:public_key]) end end else @@ -272,29 +263,29 @@ def initialize(opts = {}) set_seeded_keys end end - + def is_private? true end - + def generate_seed @seed = OpenSSL::Random.random_bytes(32) @seed_hash = generate_seed_hash(@seed) raise SeedGeneration::ValidityError unless seed_valid?(@seed_hash) end - + def generate_seed_hash(seed) hmac_sha512 HD_WALLET_BASE_KEY, seed end - + def seed_valid?(seed_hash) return false unless seed_hash.bytesize == 64 master_key = left_from_hash(seed_hash) !master_key.zero? && master_key < MoneyTree::Key::ORDER end - + def set_seeded_keys - @private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash), network: network_key + @private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash) @chain_code = right_from_hash(seed_hash) @public_key = MoneyTree::PublicKey.new @private_key end diff --git a/lib/money-tree/version.rb b/lib/money-tree/version.rb index 12e7322..612eaa6 100644 --- a/lib/money-tree/version.rb +++ b/lib/money-tree/version.rb @@ -1,3 +1,3 @@ module MoneyTree - VERSION = "0.8.8" + VERSION = "0.10.0" end diff --git a/lib/openssl_extensions.rb b/lib/openssl_extensions.rb index b942e39..44b7f5e 100644 --- a/lib/openssl_extensions.rb +++ b/lib/openssl_extensions.rb @@ -6,7 +6,7 @@ module MoneyTree module OpenSSLExtensions extend FFI::Library - ffi_lib 'ssl' + ffi_lib ['libssl.so.1.0.0', 'libssl.so.10', 'libssl1.0.0', 'ssl'] NID_secp256k1 = 714 POINT_CONVERSION_COMPRESSED = 2 @@ -40,6 +40,12 @@ def self.add(point_0, point_1) EC_POINT_clear_free(point_0_pt) EC_POINT_clear_free(point_1_pt) + eckey = nil + group = nil + sum_point = nil + point_0_pt = nil + point_1_pt = nil + hex end @@ -64,4 +70,4 @@ def add(point) self.class.new group, OpenSSL::BN.new(sum_point_hex, 16) end -end \ No newline at end of file +end diff --git a/money-tree.gemspec b/money-tree.gemspec index f174e3b..43c1f72 100644 --- a/money-tree.gemspec +++ b/money-tree.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.email = ["winkelspecht@gmail.com"] spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets} spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)} - spec.homepage = "" + spec.homepage = "https://github.com/gemhq/money-tree" spec.license = "MIT" spec.files = `git ls-files`.split($/) @@ -18,6 +18,15 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] + # used with gem install ... -P HighSecurity + spec.cert_chain = ["certs/mattatgemco.pem"] + # Sign gem when evaluating spec with `gem` command + # unless ENV has set a SKIP_GEM_SIGNING + if ($0 =~ /gem\z/) and not ENV.include?("SKIP_GEM_SIGNING") + spec.signing_key = File.join(Gem.user_home, ".ssh", "gem-private_key.pem") + end + + spec.add_dependency "ffi" spec.add_development_dependency "bundler", "~> 1.3" diff --git a/spec/lib/money-tree/address_spec.rb b/spec/lib/money-tree/address_spec.rb index 7d42bc8..e48fc60 100644 --- a/spec/lib/money-tree/address_spec.rb +++ b/spec/lib/money-tree/address_spec.rb @@ -56,7 +56,7 @@ end it "returns a testnet address" do - expect(%w(m n)).to include(@address.to_s[0]) + expect(%w(m n)).to include(@address.to_s(network: :bitcoin_testnet)[0]) end end end diff --git a/spec/lib/money-tree/node_spec.rb b/spec/lib/money-tree/node_spec.rb index 715fb46..24a2928 100644 --- a/spec/lib/money-tree/node_spec.rb +++ b/spec/lib/money-tree/node_spec.rb @@ -7,7 +7,7 @@ before do @master = MoneyTree::Master.new end - + it "generates a random seed 32 bytes long" do expect(@master.seed.bytesize).to eql(32) end @@ -24,49 +24,49 @@ end it "generates testnet address" do - expect(%w(m n)).to include(@master.to_address[0]) + expect(%w(m n)).to include(@master.to_address(network: :bitcoin_testnet)[0]) end it "generates testnet compressed wif" do - expect(@master.private_key.to_wif[0]).to eql('c') + expect(@master.private_key.to_wif(network: :bitcoin_testnet)[0]).to eql('c') end it "generates testnet uncompressed wif" do - expect(@master.private_key.to_wif(compressed: false)[0]).to eql('9') + expect(@master.private_key.to_wif(compressed: false, network: :bitcoin_testnet)[0]).to eql('9') end it "generates testnet serialized private address" do - expect(@master.to_serialized_address(:private).slice(0, 4)).to eql("tprv") + expect(@master.to_bip32(:private, network: :bitcoin_testnet).slice(0, 4)).to eql("tprv") end it "generates testnet serialized public address" do - expect(@master.to_serialized_address.slice(0, 4)).to eql("tpub") + expect(@master.to_bip32(network: :bitcoin_testnet).slice(0, 4)).to eql("tpub") end it "imports from testnet serialized private address" do - node = MoneyTree::Node.from_serialized_address 'tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE' - expect(node.to_serialized_address(:private)).to eql('tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE') + node = MoneyTree::Node.from_bip32 'tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE' + expect(node.to_bip32(:private, network: :bitcoin_testnet)).to eql('tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE') end it "imports from testnet serialized public address" do - node = MoneyTree::Node.from_serialized_address 'tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT' - expect(%w(m n)).to include(node.public_key.to_s[0]) - expect(node.to_serialized_address).to eql('tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT') + node = MoneyTree::Node.from_bip32 'tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT' + expect(%w(m n)).to include(node.public_key.to_s(network: :bitcoin_testnet)[0]) + expect(node.to_bip32(network: :bitcoin_testnet)).to eql('tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT') end it "generates testnet subnodes from serialized private address" do - node = MoneyTree::Node.from_serialized_address 'tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE' + node = MoneyTree::Node.from_bip32 'tprv8ZgxMBicQKsPcuN7bfUZqq78UEYapr3Tzmc9NcDXw8BnBJ47dZYr6SusnfYj7vbAYP9CP8ZiD5aVBTUo1yU5QP56mepKVvuEbu8KZQXMKNE' subnode = node.node_for_path('1/1/1') - expect(%w(m n)).to include(subnode.public_key.to_s[0]) - expect(subnode.to_serialized_address(:private).slice(0,4)).to eql('tprv') - expect(subnode.to_serialized_address.slice(0,4)).to eql('tpub') + expect(%w(m n)).to include(subnode.public_key.to_s(network: :bitcoin_testnet)[0]) + expect(subnode.to_bip32(:private, network: :bitcoin_testnet).slice(0,4)).to eql('tprv') + expect(subnode.to_bip32(network: :bitcoin_testnet).slice(0,4)).to eql('tpub') end it "generates testnet subnodes from serialized public address" do - node = MoneyTree::Node.from_serialized_address 'tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT' + node = MoneyTree::Node.from_bip32 'tpubD6NzVbkrYhZ4YA8aUE9bBZTSyHJibBqwDny5urfwDdJc4W8od3y3Ebzy6CqsYn9CCC5P5VQ7CeZYpnT1kX3RPVPysU2rFRvYSj8BCoYYNqT' subnode = node.node_for_path('1/1/1') - expect(%w(m n)).to include(subnode.public_key.to_s[0]) - expect(subnode.to_serialized_address.slice(0,4)).to eql('tpub') + expect(%w(m n)).to include(subnode.public_key.to_s(network: :bitcoin_testnet)[0]) + expect(subnode.to_bip32(network: :bitcoin_testnet).slice(0,4)).to eql('tpub') end end @@ -75,424 +75,430 @@ before do @master = MoneyTree::Master.new seed_hex: "000102030405060708090a0b0c0d0e0f" end - + describe "m" do it "has an index of 0" do expect(@master.index).to eql(0) end - + it "is private" do expect(@master.is_private?).to eql(true) end - + it "has a depth of 0" do expect(@master.depth).to eql(0) end - + it "generates master node (Master)" do expect(@master.to_identifier).to eql("3442193e1bb70916e914552172cd4e2dbc9df811") expect(@master.to_fingerprint).to eql("3442193e") expect(@master.to_address).to eql("15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma") end - + it "generates a secret key" do expect(@master.private_key.to_hex).to eql("e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35") expect(@master.private_key.to_wif).to eql("L52XzL2cMkHxqxBXRyEpnPQZGUs3uKiL3R11XbAdHigRzDozKZeW") end - + it "generates a public key" do expect(@master.public_key.to_hex).to eql("0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2") end - + it "generates a chain code" do expect(@master.chain_code_hex).to eql("873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508") end - + it "generates a serialized private key" do expect(@master.to_serialized_hex(:private)).to eql("0488ade4000000000000000000873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d50800e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35") - expect(@master.to_serialized_address(:private)).to eql("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") + expect(@master.to_bip32(:private)).to eql("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") end - + it "generates a serialized public_key" do expect(@master.to_serialized_hex).to eql("0488b21e000000000000000000873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d5080339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2") - expect(@master.to_serialized_address).to eql("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8") + expect(@master.to_bip32).to eql("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8") end end - + describe "m/0p" do before do @node = @master.node_for_path "m/0p" end - + it "has an index of 2147483648" do expect(@node.index).to eql(2147483648) end - + it "is private" do expect(@node.is_private?).to eql(true) end - + it "has a depth of 1" do expect(@node.depth).to eql(1) end - + it "generates subnode" do expect(@node.to_identifier).to eql("5c1bd648ed23aa5fd50ba52b2457c11e9e80a6a7") expect(@node.to_fingerprint).to eql("5c1bd648") expect(@node.to_address).to eql("19Q2WoS5hSS6T8GjhK8KZLMgmWaq4neXrh") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea") expect(@node.private_key.to_wif).to eql("L5BmPijJjrKbiUfG4zbiFKNqkvuJ8usooJmzuD7Z8dkRoTThYnAT") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade4013442193e8000000047fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae623614100edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea") - expect(@node.to_serialized_address(:private)).to eql("xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7") + expect(@node.to_bip32(:private)).to eql("xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e013442193e8000000047fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56") - expect(@node.to_serialized_address).to eql("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw") + expect(@node.to_bip32).to eql("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw") end end - + describe "m/0p.pub" do before do @node = @master.node_for_path "m/0p.pub" end - + it "has an index of 2147483648" do expect(@node.index).to eql(2147483648) end - + it "is private" do expect(@node.is_private?).to eql(true) end - + it "has a depth of 1" do expect(@node.depth).to eql(1) end - + it "generates subnode" do expect(@node.to_identifier).to eql("5c1bd648ed23aa5fd50ba52b2457c11e9e80a6a7") expect(@node.to_fingerprint).to eql("5c1bd648") expect(@node.to_address).to eql("19Q2WoS5hSS6T8GjhK8KZLMgmWaq4neXrh") end - + it "does not generate a private key" do expect(@node.private_key).to be_nil end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141") end - + it "does not generate a serialized private key" do expect { @node.to_serialized_hex(:private) }.to raise_error(MoneyTree::Node::PrivatePublicMismatch) end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e013442193e8000000047fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56") - expect(@node.to_serialized_address).to eql("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw") + expect(@node.to_bip32).to eql("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw") end end - + describe "m/0'/1" do before do @node = @master.node_for_path "m/0'/1" end - + it "has an index of 1" do expect(@node.index).to eql(1) end - + it "is public" do expect(@node.is_private?).to eql(false) end - + it "has a depth of 2" do expect(@node.depth).to eql(2) end - + it "generates subnode" do expect(@node.to_identifier).to eql("bef5a2f9a56a94aab12459f72ad9cf8cf19c7bbe") expect(@node.to_fingerprint).to eql("bef5a2f9") expect(@node.to_address).to eql("1JQheacLPdM5ySCkrZkV66G2ApAXe1mqLj") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368") expect(@node.private_key.to_wif).to eql("KyFAjQ5rgrKvhXvNMtFB5PCSKUYD1yyPEe3xr3T34TZSUHycXtMM") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade4025c1bd648000000012a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19003c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368") - expect(@node.to_serialized_address(:private)).to eql("xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs") + expect(@node.to_bip32(:private)).to eql("xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e025c1bd648000000012a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c1903501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c") - expect(@node.to_serialized_address).to eql("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ") + expect(@node.to_bip32).to eql("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ") end end - + describe "M/0'/1" do before do @node = @master.node_for_path "M/0'/1" end - + it "has an index of 1" do expect(@node.index).to eql(1) end - + it "is public" do expect(@node.is_private?).to eql(false) end - + it "has a depth of 2" do expect(@node.depth).to eql(2) end - + it "generates subnode" do expect(@node.to_identifier).to eql("bef5a2f9a56a94aab12459f72ad9cf8cf19c7bbe") expect(@node.to_fingerprint).to eql("bef5a2f9") expect(@node.to_address).to eql("1JQheacLPdM5ySCkrZkV66G2ApAXe1mqLj") end - + it "does not generate a private key" do expect(@node.private_key).to be_nil end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19") end - + it "generates a serialized private key" do expect { @node.to_serialized_hex(:private) }.to raise_error(MoneyTree::Node::PrivatePublicMismatch) end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e025c1bd648000000012a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c1903501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c") - expect(@node.to_serialized_address).to eql("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ") + expect(@node.to_bip32).to eql("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ") end end - + describe "m/0'/1/2p/2" do before do @node = @master.node_for_path "m/0'/1/2p/2" end - + it "has an index of 2" do expect(@node.index).to eql(2) end - + it "is public" do expect(@node.is_private?).to eql(false) end - + it "has a depth of 4" do expect(@node.depth).to eql(4) end - + it "generates subnode" do expect(@node.to_identifier).to eql("d880d7d893848509a62d8fb74e32148dac68412f") expect(@node.to_fingerprint).to eql("d880d7d8") expect(@node.to_address).to eql("1LjmJcdPnDHhNTUgrWyhLGnRDKxQjoxAgt") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4") expect(@node.private_key.to_wif).to eql("KwjQsVuMjbCP2Zmr3VaFaStav7NvevwjvvkqrWd5Qmh1XVnCteBR") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade404ee7ab90c00000002cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd000f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4") - expect(@node.to_serialized_address(:private)).to eql("xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334") + expect(@node.to_bip32(:private)).to eql("xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e04ee7ab90c00000002cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29") - expect(@node.to_serialized_address).to eql("xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV") + expect(@node.to_bip32).to eql("xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV") end end - + describe "m/0'/1/2'/2/1000000000" do before do @node = @master.node_for_path "m/0'/1/2'/2/1000000000" end - + it "has an index of 1000000000" do expect(@node.index).to eql(1000000000) end - + it "is public" do expect(@node.is_private?).to eql(false) end - + it "has a depth of 2" do expect(@node.depth).to eql(5) end - + it "generates subnode" do expect(@node.to_identifier).to eql("d69aa102255fed74378278c7812701ea641fdf32") expect(@node.to_fingerprint).to eql("d69aa102") expect(@node.to_address).to eql("1LZiqrop2HGR4qrH1ULZPyBpU6AUP49Uam") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8") expect(@node.private_key.to_wif).to eql("Kybw8izYevo5xMh1TK7aUr7jHFCxXS1zv8p3oqFz3o2zFbhRXHYs") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade405d880d7d83b9aca00c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e00471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8") - expect(@node.to_serialized_address(:private)).to eql("xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76") + expect(@node.to_bip32(:private)).to eql("xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e05d880d7d83b9aca00c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011") - expect(@node.to_serialized_address).to eql("xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy") + expect(@node.to_bip32).to eql("xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy") end end end end - + describe "Test vector 2" do describe "from a seed" do before do @master = MoneyTree::Master.new seed_hex: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542" end - + describe "m" do it "has an index of 0" do expect(@master.index).to eql(0) end - + it "has a depth of 0" do expect(@master.depth).to eql(0) end - + it "is private" do expect(@master.is_private?).to eql(true) end - + it "generates master node (Master)" do expect(@master.to_identifier).to eql("bd16bee53961a47d6ad888e29545434a89bdfe95") expect(@master.to_fingerprint).to eql("bd16bee5") expect(@master.to_address).to eql("1JEoxevbLLG8cVqeoGKQiAwoWbNYSUyYjg") end - + + it "generates compressed and uncompressed addresses" do + expect(@master.to_address).to eql("1JEoxevbLLG8cVqeoGKQiAwoWbNYSUyYjg") + expect(@master.to_address(true)).to eql("1JEoxevbLLG8cVqeoGKQiAwoWbNYSUyYjg") + expect(@master.to_address(false)).to eql("1AEg9dFEw29kMgaN4BNHALu7AzX5XUfzSU") + end + it "generates a secret key" do expect(@master.private_key.to_hex).to eql("4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e") expect(@master.private_key.to_wif).to eql("KyjXhyHF9wTphBkfpxjL8hkDXDUSbE3tKANT94kXSyh6vn6nKaoy") end - + it "generates a public key" do expect(@master.public_key.to_hex).to eql("03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7") end - + it "generates a chain code" do expect(@master.chain_code_hex).to eql("60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689") end - + it "generates a serialized private key" do expect(@master.to_serialized_hex(:private)).to eql("0488ade400000000000000000060499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689004b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e") - expect(@master.to_serialized_address(:private)).to eql("xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U") + expect(@master.to_bip32(:private)).to eql("xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U") end - + it "generates a serialized public_key" do expect(@master.to_serialized_hex).to eql("0488b21e00000000000000000060499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd968903cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7") - expect(@master.to_serialized_address).to eql("xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB") + expect(@master.to_bip32).to eql("xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB") end end - + describe "m/0 (testing imported private key)" do before do @master = MoneyTree::Master.new private_key: @master.private_key, chain_code: @master.chain_code @node = @master.node_for_path "m/0" end - + it "has an index of 0" do expect(@node.index).to eql(0) end - + it "has a depth of 1" do expect(@node.depth).to eql(1) end - + it "is public" do expect(@node.is_private?).to eql(false) end - + it "generates subnode" do expect(@node.to_identifier).to eql("5a61ff8eb7aaca3010db97ebda76121610b78096") expect(@node.to_fingerprint).to eql("5a61ff8e") expect(@node.to_address).to eql("19EuDJdgfRkwCmRzbzVBHZWQG9QNWhftbZ") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e") expect(@node.private_key.to_wif).to eql("L2ysLrR6KMSAtx7uPqmYpoTeiRzydXBattRXjXz5GDFPrdfPzKbj") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade401bd16bee500000000f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c00abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e") - expect(@node.to_serialized_address(:private)).to eql("xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt") + expect(@node.to_bip32(:private)).to eql("xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e01bd16bee500000000f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea") - expect(@node.to_serialized_address).to eql("xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH") + expect(@node.to_bip32).to eql("xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH") end end @@ -501,271 +507,271 @@ @master = MoneyTree::Master.new public_key: "03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7", chain_code: @master.chain_code @node = @master.node_for_path "M/0" end - + it "has an index of 0" do expect(@node.index).to eql(0) end - + it "has a depth of 1" do expect(@node.depth).to eql(1) end - + it "is public" do expect(@node.is_private?).to eql(false) end - + it "generates subnode" do expect(@node.to_identifier).to eql("5a61ff8eb7aaca3010db97ebda76121610b78096") expect(@node.to_fingerprint).to eql("5a61ff8e") expect(@node.to_address).to eql("19EuDJdgfRkwCmRzbzVBHZWQG9QNWhftbZ") end - + it "does not generate a private key" do expect(@node.private_key).to be_nil end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c") end - + it "does not generate a serialized private key" do expect { @node.to_serialized_hex(:private) }.to raise_error(MoneyTree::Node::PrivatePublicMismatch) - expect { @node.to_serialized_address(:private) }.to raise_error(MoneyTree::Node::PrivatePublicMismatch) + expect { @node.to_bip32(:private) }.to raise_error(MoneyTree::Node::PrivatePublicMismatch) end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e01bd16bee500000000f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea") - expect(@node.to_serialized_address).to eql("xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH") + expect(@node.to_bip32).to eql("xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH") end end - + describe "m/0/2147483647'" do before do @node = @master.node_for_path "m/0/2147483647'" end - + it "has an index of 2147483647" do expect(@node.index).to eql(4294967295) end - + it "has a depth of 2" do expect(@node.depth).to eql(2) end - + it "is private" do expect(@node.is_private?).to eql(true) end - + it "generates subnode" do expect(@node.to_identifier).to eql("d8ab493736da02f11ed682f88339e720fb0379d1") expect(@node.to_fingerprint).to eql("d8ab4937") expect(@node.to_address).to eql("1Lke9bXGhn5VPrBuXgN12uGUphrttUErmk") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93") expect(@node.private_key.to_wif).to eql("L1m5VpbXmMp57P3knskwhoMTLdhAAaXiHvnGLMribbfwzVRpz2Sr") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("03c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade4025a61ff8effffffffbe17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d900877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93") - expect(@node.to_serialized_address(:private)).to eql("xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9") + expect(@node.to_bip32(:private)).to eql("xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e025a61ff8effffffffbe17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d903c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b") - expect(@node.to_serialized_address).to eql("xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a") + expect(@node.to_bip32).to eql("xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a") end end - + describe "m/0/2147483647'/1" do before do @node = @master.node_for_path "m/0/2147483647'/1" end - + it "has an index of 1" do expect(@node.index).to eql(1) end - + it "has a depth of 3" do expect(@node.depth).to eql(3) end - + it "is private" do expect(@node.is_private?).to eql(false) end - + it "generates subnode" do expect(@node.to_identifier).to eql("78412e3a2296a40de124307b6485bd19833e2e34") expect(@node.to_fingerprint).to eql("78412e3a") expect(@node.to_address).to eql("1BxrAr2pHpeBheusmd6fHDP2tSLAUa3qsW") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7") expect(@node.private_key.to_wif).to eql("KzyzXnznxSv249b4KuNkBwowaN3akiNeEHy5FWoPCJpStZbEKXN2") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade403d8ab493700000001f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb00704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7") - expect(@node.to_serialized_address(:private)).to eql("xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef") + expect(@node.to_bip32(:private)).to eql("xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e03d8ab493700000001f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9") - expect(@node.to_serialized_address).to eql("xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon") + expect(@node.to_bip32).to eql("xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon") end end - + describe "m/0/2147483647p/1/2147483646p" do before do @node = @master.node_for_path "m/0/2147483647p/1/2147483646p" end - + it "has an index of 4294967294" do expect(@node.index).to eql(4294967294) end - + it "has a depth of 4" do expect(@node.depth).to eql(4) end - + it "is private" do expect(@node.is_private?).to eql(true) end - + it "generates subnode" do expect(@node.to_identifier).to eql("31a507b815593dfc51ffc7245ae7e5aee304246e") expect(@node.to_fingerprint).to eql("31a507b8") expect(@node.to_address).to eql("15XVotxCAV7sRx1PSCkQNsGw3W9jT9A94R") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d") expect(@node.private_key.to_wif).to eql("L5KhaMvPYRW1ZoFmRjUtxxPypQ94m6BcDrPhqArhggdaTbbAFJEF") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade40478412e3afffffffe637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e2900f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d") - expect(@node.to_serialized_address(:private)).to eql("xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc") + expect(@node.to_bip32(:private)).to eql("xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e0478412e3afffffffe637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e2902d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0") - expect(@node.to_serialized_address).to eql("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") + expect(@node.to_bip32).to eql("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") end end - + describe "m/0/2147483647p/1/2147483646p/2" do before do @node = @master.node_for_path "m/0/2147483647p/1/2147483646p/2" end - + it "has an index of 2" do expect(@node.index).to eql(2) end - + it "has a depth of 4" do expect(@node.depth).to eql(5) end - + it "is public" do expect(@node.is_private?).to eql(false) end - + it "generates subnode" do expect(@node.to_identifier).to eql("26132fdbe7bf89cbc64cf8dafa3f9f88b8666220") expect(@node.to_fingerprint).to eql("26132fdb") expect(@node.to_address).to eql("14UKfRV9ZPUp6ZC9PLhqbRtxdihW9em3xt") end - + it "generates a private key" do expect(@node.private_key.to_hex).to eql("bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23") expect(@node.private_key.to_wif).to eql("L3WAYNAZPxx1fr7KCz7GN9nD5qMBnNiqEJNJMU1z9MMaannAt4aK") end - + it "generates a public key" do expect(@node.public_key.to_hex).to eql("024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c") end - + it "generates a chain code" do expect(@node.chain_code_hex).to eql("9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271") end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade40531a507b8000000029452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed27100bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23") - expect(@node.to_serialized_address(:private)).to eql("xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j") + expect(@node.to_bip32(:private)).to eql("xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e0531a507b8000000029452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c") - expect(@node.to_serialized_address).to eql("xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt") + expect(@node.to_bip32).to eql("xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt") end end end end - + describe "negative index" do before do @master = MoneyTree::Master.new seed_hex: "000102030405060708090a0b0c0d0e0f" @node = @master.node_for_path "m/0'/-1" end - + it "has an index of 1" do expect(@node.index).to eql(-1) end - + it "is public" do expect(@node.is_private?).to eql(true) end - + it "has a depth of 2" do expect(@node.depth).to eql(2) end - + it "generates a serialized private key" do expect(@node.to_serialized_hex(:private)).to eql("0488ade4025c1bd648ffffffff0f9ca680ee23c81a305d96b86f811947e65590200b6f74d66ecf83936313a9c900235893db08ad0efc6ae4a1eac5b31a90a7d0906403d139d4d7f3c6796fb42c4e") - expect(@node.to_serialized_address(:private)).to eql("xprv9wTYmMFvAM7JHf3RuUidc24a4y2t4gN7aNP5ABreWAqt6BUBcf6xE8RNQxj2vUssYWM8iAZiZi5H1fmKkkpXjtwDCDv1pg8fSfQMk9rhHYt") + expect(@node.to_bip32(:private)).to eql("xprv9wTYmMFvAM7JHf3RuUidc24a4y2t4gN7aNP5ABreWAqt6BUBcf6xE8RNQxj2vUssYWM8iAZiZi5H1fmKkkpXjtwDCDv1pg8fSfQMk9rhHYt") end - + it "generates a serialized public_key" do expect(@node.to_serialized_hex).to eql("0488b21e025c1bd648ffffffff0f9ca680ee23c81a305d96b86f811947e65590200b6f74d66ecf83936313a9c902adb7979a5e99bf8acdfec3680bf482feac9898b28808c22d47db62e98de5d3fa") - expect(@node.to_serialized_address).to eql("xpub6ASuArnozifbW97u1WFdyA1JczsNU95xwbJfxaGG4WNrxyoLACRCmvjrGEojsRsoZULf5FyZXv6AWAtce2UErsshvkpjNaT1fP6sMgTZdc1") + expect(@node.to_bip32).to eql("xpub6ASuArnozifbW97u1WFdyA1JczsNU95xwbJfxaGG4WNrxyoLACRCmvjrGEojsRsoZULf5FyZXv6AWAtce2UErsshvkpjNaT1fP6sMgTZdc1") end end - + describe "importing node" do - describe ".from_serialized_address(address)" do + describe ".from_bip32(address)" do it "imports a valid private node address" do - @node = MoneyTree::Node.from_serialized_address "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7" + @node = MoneyTree::Node.from_bip32 "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7" expect(@node.private_key.to_hex).to eql("edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea") expect(@node.index).to eql(2147483648) expect(@node.is_private?).to eql(true) @@ -774,9 +780,9 @@ expect(@node.chain_code_hex).to eql("47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141") expect(@node.parent_fingerprint).to eql("3442193e") end - + it "imports a valid public node address" do - @node = MoneyTree::Node.from_serialized_address "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw" + @node = MoneyTree::Node.from_bip32 "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw" expect(@node.private_key).to be_nil expect(@node.index).to eql(2147483648) expect(@node.is_private?).to eql(true) @@ -787,5 +793,15 @@ end end end + + describe "deriving a child node" do + describe "#node_for_path" do + it "correctly derives from a node with a chain code represented in 31 bytes" do + @node = MoneyTree::Node.from_bip32 "tpubD6NzVbkrYhZ4WM42MZZmUZ7LjxyjBf5bGjEeLf9nJnMZqocGJWu94drvpqWsE9jE7k3h22v6gjpPGnqgBrqwGsRYwDXVRfQ2M9dfHbXP5zA" + @subnode = @node.node_for_path('m/1') + expect(@subnode.to_bip32(network: :bitcoin_testnet)).to eql("tpubDA7bCxb3Nrcz2ChXyPqXxbG4q5oiAZUHR7wD3LAiXukuxmT65weWw84XYmjhkJTkJEM6LhNWioWTpKEkQp7j2fgVccj3PPc271xHDeMsaTY") + end + end + end end end diff --git a/spec/lib/money-tree/private_key_spec.rb b/spec/lib/money-tree/private_key_spec.rb index cc75765..7541838 100644 --- a/spec/lib/money-tree/private_key_spec.rb +++ b/spec/lib/money-tree/private_key_spec.rb @@ -45,7 +45,7 @@ end it "is valid" do - expect(@key.to_wif(compressed: false)).to eql('5JXz5ZyFk31oHVTQxqce7yitCmTAPxBqeGQ4b7H3Aj3L45wUhoa' ) + expect(@key.to_wif(compressed: false)).to eql('5JXz5ZyFk31oHVTQxqce7yitCmTAPxBqeGQ4b7H3Aj3L45wUhoa') end end @@ -93,7 +93,17 @@ describe "parse_raw_key" do it "returns error if key is not Bignum, hex, base64, or wif formatted" do expect { @key = MoneyTree::PrivateKey.new(key: "Thisisnotakey") }.to raise_error(MoneyTree::Key::KeyFormatNotFound) - + end + + it "raises an error that can be caught using a standard exception block" do + exception_raised = false + + begin + MoneyTree::PrivateKey.new(key: "Thisisnotakey") + rescue => ex + exception_raised = true + end + fail unless exception_raised end end @@ -104,7 +114,7 @@ describe "to_wif" do it "returns same wif" do - expect(@key.to_wif).to eql('cRhes8SBnsF6WizphaRKQKZZfDniDa9Bxcw31yKeEC1KDExhxFgD') + expect(@key.to_wif(network: :bitcoin_testnet)).to eql('cRhes8SBnsF6WizphaRKQKZZfDniDa9Bxcw31yKeEC1KDExhxFgD') end end end diff --git a/spec/lib/money-tree/public_key_spec.rb b/spec/lib/money-tree/public_key_spec.rb index 3d26572..4aabe61 100644 --- a/spec/lib/money-tree/public_key_spec.rb +++ b/spec/lib/money-tree/public_key_spec.rb @@ -161,22 +161,26 @@ context "testnet" do context 'with private key' do before do - @private_key = MoneyTree::PrivateKey.new network: :bitcoin_testnet + @private_key = MoneyTree::PrivateKey.new @key = MoneyTree::PublicKey.new(@private_key) end it "should have an address starting with m or n" do - expect(%w(m n)).to include(@key.to_s[0]) + expect(%w(m n)).to include(@key.to_s(network: :bitcoin_testnet)[0]) + end + + it "should have an uncompressed address starting with m or n" do + expect(%w(m n)).to include(@key.uncompressed.to_s(network: :bitcoin_testnet)[0]) end end context 'without private key' do before do - @key = MoneyTree::PublicKey.new('0297b033ba894611345a0e777861237ef1632370fbd58ebe644eb9f3714e8fe2bc', network: :bitcoin_testnet) + @key = MoneyTree::PublicKey.new('0297b033ba894611345a0e777861237ef1632370fbd58ebe644eb9f3714e8fe2bc') end it "should have an address starting with m or n" do - expect(%w(m n)).to include(@key.to_s[0]) + expect(%w(m n)).to include(@key.to_s(network: :bitcoin_testnet)[0]) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d21871c..c74937c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,2 +1,3 @@ require 'simplecov' require 'money-tree' +require 'pry'