money-tree 0.10.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,18 @@
1
- require 'openssl'
2
- require 'base64'
1
+ require "base64"
2
+ require "bech32"
3
+ require "openssl"
3
4
 
4
5
  module MoneyTree
5
6
  module Support
6
7
  include OpenSSL
7
-
8
+ extend self
9
+
8
10
  INT32_MAX = 256 ** [1].pack("L*").size
9
11
  INT64_MAX = 256 ** [1].pack("Q*").size
10
12
  BASE58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
11
-
12
- def int_to_base58(int_val, leading_zero_bytes=0)
13
- base58_val, base = '', BASE58_CHARS.size
13
+
14
+ def int_to_base58(int_val, leading_zero_bytes = 0)
15
+ base58_val, base = "", BASE58_CHARS.size
14
16
  while int_val > 0
15
17
  int_val, remainder = int_val.divmod(base)
16
18
  base58_val = BASE58_CHARS[remainder] + base58_val
@@ -20,27 +22,28 @@ module MoneyTree
20
22
 
21
23
  def base58_to_int(base58_val)
22
24
  int_val, base = 0, BASE58_CHARS.size
23
- base58_val.reverse.each_char.with_index do |char,index|
24
- raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = BASE58_CHARS.index(char)
25
- int_val += char_index*(base**index)
25
+ base58_val.reverse.each_char.with_index do |char, index|
26
+ raise ArgumentError, "Value not a valid Base58 String." unless char_index = BASE58_CHARS.index(char)
27
+ int_val += char_index * (base ** index)
26
28
  end
27
29
  int_val
28
30
  end
29
31
 
30
32
  def encode_base58(hex)
31
- leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
32
- ("1"*leading_zero_bytes) + int_to_base58( hex.to_i(16) )
33
+ leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : "").size / 2
34
+ ("1" * leading_zero_bytes) + int_to_base58(hex.to_i(16))
33
35
  end
34
36
 
35
37
  def decode_base58(base58_val)
36
- s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? '0'+s : s)
37
- s = '' if s == '00'
38
- leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : '').size
39
- s = ("00"*leading_zero_bytes) + s if leading_zero_bytes > 0
38
+ s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? "0" + s : s)
39
+ s = "" if s == "00"
40
+ leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : "").size
41
+ s = ("00" * leading_zero_bytes) + s if leading_zero_bytes > 0
40
42
  s
41
43
  end
44
+
42
45
  alias_method :base58_to_hex, :decode_base58
43
-
46
+
44
47
  def to_serialized_base58(hex)
45
48
  hash = sha256 hex
46
49
  hash = sha256 hash
@@ -48,7 +51,14 @@ module MoneyTree
48
51
  address = hex + checksum
49
52
  encode_base58 address
50
53
  end
51
-
54
+
55
+ def to_serialized_bech32(human_readable_part, hex)
56
+ segwit_addr = Bech32::SegwitAddr.new
57
+ segwit_addr.hrp = human_readable_part
58
+ segwit_addr.script_pubkey = "0000" + hex
59
+ segwit_addr.addr
60
+ end
61
+
52
62
  def from_serialized_base58(base58)
53
63
  hex = decode_base58 base58
54
64
  checksum = hex.slice!(-8..-1)
@@ -56,46 +66,46 @@ module MoneyTree
56
66
  raise EncodingError unless checksum == compare_checksum
57
67
  hex
58
68
  end
59
-
69
+
60
70
  def digestify(digest_type, source, opts = {})
61
71
  source = [source].pack("H*") unless opts[:ascii]
62
72
  bytes_to_hex Digest.digest(digest_type, source)
63
73
  end
64
-
74
+
65
75
  def sha256(source, opts = {})
66
- digestify('SHA256', source, opts)
76
+ digestify("SHA256", source, opts)
67
77
  end
68
-
78
+
69
79
  def ripemd160(source, opts = {})
70
- digestify('RIPEMD160', source, opts)
80
+ digestify("RIPEMD160", source, opts)
71
81
  end
72
-
82
+
73
83
  def encode_base64(hex)
74
84
  Base64.encode64([hex].pack("H*")).chomp
75
85
  end
76
-
86
+
77
87
  def decode_base64(base64)
78
88
  Base64.decode64(base64).unpack("H*")[0]
79
89
  end
80
-
90
+
81
91
  def hmac_sha512(key, message)
82
92
  digest = Digest::SHA512.new
83
93
  HMAC.digest digest, key, message
84
94
  end
85
-
95
+
86
96
  def hmac_sha512_hex(key, message)
87
97
  md = hmac_sha512(key, message)
88
- md.unpack("H*").first.rjust(64, '0')
98
+ md.unpack("H*").first.rjust(64, "0")
89
99
  end
90
-
100
+
91
101
  def bytes_to_int(bytes, base = 16)
92
102
  if bytes.is_a?(Array)
93
103
  bytes = bytes.pack("C*")
94
104
  end
95
105
  bytes.unpack("H*")[0].to_i(16)
96
106
  end
97
-
98
- def int_to_hex(i, size=nil)
107
+
108
+ def int_to_hex(i, size = nil)
99
109
  hex = i.to_s(16).downcase
100
110
  if (hex.size % 2) != 0
101
111
  hex = "#{0}#{hex}"
@@ -107,21 +117,36 @@ module MoneyTree
107
117
  hex
108
118
  end
109
119
  end
110
-
120
+
111
121
  def int_to_bytes(i)
112
122
  [int_to_hex(i)].pack("H*")
113
123
  end
114
-
124
+
115
125
  def bytes_to_hex(bytes)
116
126
  bytes.unpack("H*")[0].downcase
117
127
  end
118
-
128
+
119
129
  def hex_to_bytes(hex)
120
130
  [hex].pack("H*")
121
131
  end
122
-
132
+
123
133
  def hex_to_int(hex)
124
134
  hex.to_i(16)
125
135
  end
136
+
137
+ def encode_p2wpkh_p2sh(value)
138
+ chk = [Digest::SHA256.hexdigest(Digest::SHA256.digest(value))].pack("H*")[0...4]
139
+ encode_base58 (value + chk).unpack("H*")[0]
140
+ end
141
+
142
+ def custom_hash_160(value)
143
+ [OpenSSL::Digest::RIPEMD160.hexdigest(Digest::SHA256.digest(value))].pack("H*")
144
+ end
145
+
146
+ def convert_p2wpkh_p2sh(key_hex, prefix)
147
+ push_20 = ["0014"].pack("H*")
148
+ script_sig = push_20 + custom_hash_160([key_hex].pack("H*"))
149
+ encode_p2wpkh_p2sh(prefix + custom_hash_160(script_sig))
150
+ end
126
151
  end
127
152
  end
@@ -1,3 +1,3 @@
1
1
  module MoneyTree
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.1"
3
3
  end
data/lib/money-tree.rb CHANGED
@@ -1,12 +1,11 @@
1
- require "openssl_extensions"
2
- require "money-tree/version"
3
1
  require "money-tree/support"
4
- require "money-tree/networks"
5
- require "money-tree/key"
2
+
6
3
  require "money-tree/address"
4
+ require "money-tree/key"
7
5
  require "money-tree/networks"
8
6
  require "money-tree/node"
7
+ require "money-tree/version"
8
+
9
+ require "openssl_extensions"
9
10
 
10
- module MoneyTree
11
-
12
- end
11
+ module MoneyTree; end
@@ -1,73 +1,30 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- require 'openssl'
4
- require 'ffi'
3
+ require "openssl"
5
4
 
6
5
  module MoneyTree
7
6
  module OpenSSLExtensions
8
- extend FFI::Library
9
- ffi_lib ['libssl.so.1.0.0', 'libssl.so.10', 'libssl1.0.0', 'ssl']
7
+ extend self
10
8
 
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
- def self.add(point_0, point_1)
9
+ def add(point_0, point_1)
25
10
  validate_points(point_0, point_1)
26
- eckey = EC_KEY_new_by_curve_name(NID_secp256k1)
27
- group = EC_KEY_get0_group(eckey)
28
-
11
+ group = OpenSSL::PKey::EC::Group.new("secp256k1")
29
12
  point_0_hex = point_0.to_bn.to_s(16)
30
- point_0_pt = EC_POINT_hex2point(group, point_0_hex, nil, nil)
13
+ point_0_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_0_hex, 16))
31
14
  point_1_hex = point_1.to_bn.to_s(16)
32
- point_1_pt = EC_POINT_hex2point(group, point_1_hex, nil, nil)
33
-
34
- sum_point = EC_POINT_new(group)
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
15
+ point_1_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_1_hex, 16))
16
+ sum_point = point_0_pt.add(point_1_pt)
17
+ sum_point.to_bn.to_s(16)
50
18
  end
51
19
 
52
- def self.validate_points(*points)
20
+ def validate_points(*points)
53
21
  points.each do |point|
54
22
  if !point.is_a?(OpenSSL::PKey::EC::Point)
55
- raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
23
+ raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
56
24
  elsif point.infinity?
57
- raise ArgumentError, "point must not be infinity"
25
+ raise ArgumentError, "point must not be infinity"
58
26
  end
59
27
  end
60
28
  end
61
29
  end
62
30
  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
@@ -1,38 +1,30 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path("../lib", __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'money-tree/version'
4
+ require "money-tree/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "money-tree"
8
- spec.version = MoneyTree::VERSION
9
- spec.authors = ["Micah Winkelspecht"]
10
- spec.email = ["winkelspecht@gmail.com"]
11
- spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets}
12
- spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
13
- spec.homepage = "https://github.com/gemhq/money-tree"
14
- spec.license = "MIT"
7
+ spec.name = "money-tree"
8
+ spec.version = MoneyTree::VERSION
9
+ spec.authors = ["Micah Winkelspecht", "Afri Schoedon"]
10
+ spec.email = ["winkelspecht@gmail.com", "gems@q9f.cc"]
11
+ spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets}
12
+ spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
13
+ spec.homepage = "https://github.com/GemHQ/money-tree"
14
+ spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
-
21
- # used with gem install ... -P HighSecurity
22
- spec.cert_chain = ["certs/mattatgemco.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
- spec.add_dependency "ffi"
31
-
32
- spec.add_development_dependency "bundler", "~> 1.3"
33
- spec.add_development_dependency "rake"
34
- spec.add_development_dependency "rspec"
35
- spec.add_development_dependency "simplecov"
36
- spec.add_development_dependency "coveralls"
37
- spec.add_development_dependency "pry"
21
+ spec.metadata = {
22
+ "homepage_uri" => "https://github.com/GemHQ/money-tree",
23
+ "source_code_uri" => "https://github.com/GemHQ/money-tree",
24
+ "github_repo" => "https://github.com/GemHQ/money-tree",
25
+ "bug_tracker_uri" => "https://github.com/GemHQ/money-tree/issues",
26
+ }.freeze
27
+
28
+ spec.platform = Gem::Platform::RUBY
29
+ spec.required_ruby_version = ">= 2.6", "< 4.0"
38
30
  end
@@ -1,17 +1,23 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
3
  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")
@@ -19,37 +25,65 @@ describe MoneyTree::Address do
19
25
  expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
20
26
  expect(address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
21
27
  expect(address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
28
+ expect(address.to_p2wpkh_p2sh).to eql("31vNN7WVDxjvc5XZVKW3qV4B3nFLxsRPnE")
29
+ expect(address.to_bech32).to eql("bc1qrlwlgt5d0sdtq882qvk3jc0sywucn76fwcmqma")
22
30
  end
23
-
31
+
24
32
  it "imports a private key in compressed wif format" do
25
33
  address = MoneyTree::Address.new private_key: "KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK"
26
34
  expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
27
35
  expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
28
36
  expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
37
+ expect(address.to_p2wpkh_p2sh).to eql("31vNN7WVDxjvc5XZVKW3qV4B3nFLxsRPnE")
38
+ expect(address.to_bech32).to eql("bc1qrlwlgt5d0sdtq882qvk3jc0sywucn76fwcmqma")
29
39
  end
30
-
40
+
31
41
  it "imports a private key in uncompressed wif format" do
32
42
  address = MoneyTree::Address.new private_key: "5JXz5ZyFk31oHVTQxqce7yitCmTAPxBqeGQ4b7H3Aj3L45wUhoa"
33
43
  expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
34
44
  expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
35
45
  end
36
46
  end
37
-
47
+
38
48
  describe "to_s" do
39
49
  before do
40
50
  @address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
41
51
  end
42
-
52
+
43
53
  it "returns compressed base58 public key" do
44
54
  expect(@address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
45
55
  expect(@address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
56
+ expect(@address.to_p2wpkh_p2sh).to eql("31vNN7WVDxjvc5XZVKW3qV4B3nFLxsRPnE")
57
+ expect(@address.to_bech32).to eql("bc1qrlwlgt5d0sdtq882qvk3jc0sywucn76fwcmqma")
46
58
  end
47
-
59
+
48
60
  it "returns compressed WIF private key" do
49
61
  expect(@address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
50
62
  end
51
63
  end
52
64
 
65
+ context "bitcoin wiki" do
66
+ # ref https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses
67
+ subject(:wiki_v1) { MoneyTree::Address.new private_key: "18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725" }
68
+
69
+ it "always regenerates the bitcoin wiki v1 example" do
70
+ expect(wiki_v1.public_key.key).to eq "0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352"
71
+ expect(wiki_v1.to_s).to eq "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs"
72
+ expect(wiki_v1.to_p2wpkh_p2sh).to eql("3BxwGNjvG4CP14tAZodgYyZ7UTjruYDyAM")
73
+ expect(wiki_v1.to_bech32).to eql("bc1q7499s50fxu4c0qg23esvm5h8elvqkm33r2tdza")
74
+ end
75
+
76
+ # ref https://en.bitcoin.it/wiki/Bech32
77
+ subject(:wiki_bech32) { MoneyTree::Address.new private_key: "0000000000000000000000000000000000000000000000000000000000000001" }
78
+
79
+ it "always regenerates the bitcoin wiki v1 example" do
80
+ expect(wiki_bech32.public_key.key).to eq "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
81
+ expect(wiki_bech32.to_s).to eq "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"
82
+ expect(wiki_bech32.to_p2wpkh_p2sh).to eql("3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN")
83
+ expect(wiki_bech32.to_bech32).to eql("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
84
+ end
85
+ end
86
+
53
87
  context "testnet3" do
54
88
  before do
55
89
  @address = MoneyTree::Address.new network: :bitcoin_testnet
@@ -0,0 +1,9 @@
1
+ require "spec_helper"
2
+
3
+ describe MoneyTree do
4
+ it "0.11.1 works" do
5
+
6
+ # placeholder to set up spec in future
7
+ expect(MoneyTree::VERSION).to eq "0.11.1"
8
+ end
9
+ end