Class: MoneyTree::Node

Inherits:
Object
  • Object
show all
Extended by:
Support
Includes:
Support
Defined in:
lib/money-tree/node.rb

Direct Known Subclasses

Master

Defined Under Namespace

Classes: ImportError, InvalidKeyForIndex, PrivatePublicMismatch, PublicDerivationFailure

Constant Summary collapse

PRIVATE_RANGE_LIMIT =
0x80000000

Constants included from Support

Support::BASE58_CHARS, Support::INT32_MAX, Support::INT64_MAX

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support

base58_to_int, bytes_to_hex, bytes_to_int, convert_p2wpkh_p2sh, custom_hash_160, decode_base58, decode_base64, digestify, encode_base58, encode_base64, encode_p2wpkh_p2sh, from_serialized_base58, hex_to_bytes, hex_to_int, hmac_sha512, hmac_sha512_hex, int_to_base58, int_to_bytes, int_to_hex, ripemd160, sha256, to_serialized_base58, to_serialized_bech32

Constructor Details

#initialize(opts = {}) ⇒ Node

Returns a new instance of Node.



21
22
23
# File 'lib/money-tree/node.rb', line 21

def initialize(opts = {})
  opts.each { |k, v| instance_variable_set "@#{k}", v }
end

Instance Attribute Details

#chain_codeObject (readonly)

Returns the value of attribute chain_code.



8
9
10
# File 'lib/money-tree/node.rb', line 8

def chain_code
  @chain_code
end

#depthObject (readonly)

Returns the value of attribute depth.



10
11
12
# File 'lib/money-tree/node.rb', line 10

def depth
  @depth
end

#indexObject (readonly)

Returns the value of attribute index.



11
12
13
# File 'lib/money-tree/node.rb', line 11

def index
  @index
end

#is_privateObject (readonly)

Returns the value of attribute is_private.



9
10
11
# File 'lib/money-tree/node.rb', line 9

def is_private
  @is_private
end

#parentObject (readonly)

Returns the value of attribute parent.



12
13
14
# File 'lib/money-tree/node.rb', line 12

def parent
  @parent
end

#private_keyObject (readonly)

Returns the value of attribute private_key.



6
7
8
# File 'lib/money-tree/node.rb', line 6

def private_key
  @private_key
end

#public_keyObject (readonly)

Returns the value of attribute public_key.



7
8
9
# File 'lib/money-tree/node.rb', line 7

def public_key
  @public_key
end

Class Method Details

.from_bip32(address, has_version: true) ⇒ Object



25
26
27
28
29
30
31
32
33
34
# File 'lib/money-tree/node.rb', line 25

def self.from_bip32(address, has_version: true)
  hex = from_serialized_base58 address
  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(parse_out_key(hex)))
end

.parse_out_key(hex) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/money-tree/node.rb', line 36

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)
    { public_key: MoneyTree::PublicKey.new(hex) }
  else
    raise ImportError, "Public or private key data does not match version type"
  end
end

Instance Method Details

#chain_code_hexObject



245
246
247
# File 'lib/money-tree/node.rb', line 245

def chain_code_hex
  int_to_hex chain_code, 64
end

#depth_hex(depth) ⇒ Object



62
63
64
# File 'lib/money-tree/node.rb', line 62

def depth_hex(depth)
  depth.to_s(16).rjust(2, "0")
end

#derive_parent_node(parent_key) ⇒ Object



89
90
91
92
93
94
95
96
97
98
# File 'lib/money-tree/node.rb', line 89

def derive_parent_node(parent_key)
  message = parent_key.public_key.to_bytes << i_as_bytes(index)
  hash = hmac_sha512 hex_to_bytes(parent_key.chain_code_hex), message
  priv = (private_key.to_i - left_from_hash(hash)) % MoneyTree::Key::ORDER
  MoneyTree::Node.new(depth: parent_key.depth,
                      index: parent_key.index,
                      private_key: MoneyTree::PrivateKey.new(key: priv),
                      public_key: parent_key.public_key,
                      chain_code: parent_key.chain_code)
end

#derive_private_key(i = 0) ⇒ Object

Raises:



78
79
80
81
82
83
84
85
86
87
# File 'lib/money-tree/node.rb', line 78

def derive_private_key(i = 0)
  message = i >= PRIVATE_RANGE_LIMIT || i < 0 ? private_derivation_message(i) : public_derivation_message(i)
  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
  raise InvalidKeyForIndex, "equal to zero" if child_private_key == 0 # very low probability
  child_chain_code = right_from_hash(hash)
  return child_private_key, child_chain_code
end

#derive_public_key(i = 0) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/money-tree/node.rb', line 100

def derive_public_key(i = 0)
  raise PrivatePublicMismatch if i >= PRIVATE_RANGE_LIMIT
  message = public_derivation_message(i)
  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

  gen_point = public_key.uncompressed.group.generator.mul(factor)

  sum_point_hex = MoneyTree::OpenSSLExtensions.add(gen_point, public_key.uncompressed.point)
  child_public_key = OpenSSL::PKey::EC::Point.new(public_key.group, OpenSSL::BN.new(sum_point_hex, 16)).to_bn.to_i

  raise InvalidKeyForIndex, "at infinity" if child_public_key == 1 / 0.0 # very low probability
  child_chain_code = right_from_hash(hash)
  return child_public_key, child_chain_code
end

#i_as_bytes(i) ⇒ Object



74
75
76
# File 'lib/money-tree/node.rb', line 74

def i_as_bytes(i)
  [i].pack("N")
end

#index_hex(i = index) ⇒ Object



54
55
56
57
58
59
60
# File 'lib/money-tree/node.rb', line 54

def index_hex(i = index)
  if i < 0
    [i].pack("l>").unpack("H*").first
  else
    i.to_s(16).rjust(8, "0")
  end
end

#is_private?Boolean

Returns:

  • (Boolean)


50
51
52
# File 'lib/money-tree/node.rb', line 50

def is_private?
  index >= PRIVATE_RANGE_LIMIT || index < 0
end

#left_from_hash(hash) ⇒ Object



118
119
120
# File 'lib/money-tree/node.rb', line 118

def left_from_hash(hash)
  bytes_to_int hash.bytes.to_a[0..31]
end

#negative?(path_part) ⇒ Boolean

Returns:

  • (Boolean)


226
227
228
229
230
231
232
233
# File 'lib/money-tree/node.rb', line 226

def negative?(path_part)
  relative_index = path_part.to_i
  prime_symbol_present = %w(p ').include?(path_part[-1])
  minus_present = path_part[0] == "-"
  private_range = relative_index >= PRIVATE_RANGE_LIMIT
  negative_value = relative_index < 0
  prime_symbol_present || minus_present || private_range || negative_value
end

#node_for_path(path) ⇒ Object

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.



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/money-tree/node.rb', line 203

def node_for_path(path)
  force_public = path[-4..-1] == ".pub"
  path = path[0..-5] if force_public
  parts = path.split("/")
  nodes = []
  parts.each_with_index do |part, depth|
    if part =~ /m/i
      nodes << self
    else
      i = parse_index(part)
      node = nodes.last || self
      nodes << node.subnode(i)
    end
  end
  if force_public or parts.first == "M"
    node = nodes.last
    node.strip_private_info!
    node
  else
    nodes.last
  end
end

#parent_fingerprintObject



151
152
153
154
155
156
157
# File 'lib/money-tree/node.rb', line 151

def parent_fingerprint
  if @parent_fingerprint
    @parent_fingerprint
  else
    depth.zero? ? "00000000" : parent.to_fingerprint
  end
end

#parse_index(path_part) ⇒ Object



235
236
237
238
239
# File 'lib/money-tree/node.rb', line 235

def parse_index(path_part)
  index = path_part.to_i
  return index | PRIVATE_RANGE_LIMIT if negative?(path_part)
  index
end

#private_derivation_message(i) ⇒ Object



66
67
68
# File 'lib/money-tree/node.rb', line 66

def private_derivation_message(i)
  "\x00" + private_key.to_bytes + i_as_bytes(i)
end

#public_derivation_message(i) ⇒ Object



70
71
72
# File 'lib/money-tree/node.rb', line 70

def public_derivation_message(i)
  public_key.to_bytes << i_as_bytes(i)
end

#right_from_hash(hash) ⇒ Object



122
123
124
# File 'lib/money-tree/node.rb', line 122

def right_from_hash(hash)
  bytes_to_int hash.bytes.to_a[32..-1]
end

#strip_private_info!Object



241
242
243
# File 'lib/money-tree/node.rb', line 241

def strip_private_info!
  @private_key = nil
end

#subnode(i = 0, opts = {}) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/money-tree/node.rb', line 174

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
  else
    child_private_key, child_chain_code = derive_private_key(i)
    child_private_key = MoneyTree::PrivateKey.new key: child_private_key
    child_public_key = MoneyTree::PublicKey.new child_private_key
  end

  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)
end

#to_address(compressed = true, network: :bitcoin) ⇒ Object



159
160
161
162
# File 'lib/money-tree/node.rb', line 159

def to_address(compressed = true, network: :bitcoin)
  address = NETWORKS[network][:address_version] + to_identifier(compressed)
  to_serialized_base58 address
end

#to_bech32_address(network: :bitcoin) ⇒ Object



168
169
170
171
172
# File 'lib/money-tree/node.rb', line 168

def to_bech32_address(network: :bitcoin)
  hrp = NETWORKS[network][:human_readable_part]
  witprog = to_identifier
  to_serialized_bech32(hrp, witprog)
end

#to_bip32(type = :public, network: :bitcoin) ⇒ Object



137
138
139
140
# File 'lib/money-tree/node.rb', line 137

def to_bip32(type = :public, network: :bitcoin)
  raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
  to_serialized_base58 to_serialized_hex(type, network: network)
end

#to_fingerprintObject



147
148
149
# File 'lib/money-tree/node.rb', line 147

def to_fingerprint
  public_key.compressed.to_fingerprint
end

#to_identifier(compressed = true) ⇒ Object



142
143
144
145
# File 'lib/money-tree/node.rb', line 142

def to_identifier(compressed = true)
  key = compressed ? public_key.compressed : public_key.uncompressed
  key.to_ripemd160
end

#to_p2wpkh_p2sh(network: :bitcoin) ⇒ Object



164
165
166
# File 'lib/money-tree/node.rb', line 164

def to_p2wpkh_p2sh(network: :bitcoin)
  public_key.to_p2wpkh_p2sh(network: network)
end

#to_serialized_hex(type = :public, network: :bitcoin) ⇒ Object



126
127
128
129
130
131
132
133
134
135
# File 'lib/money-tree/node.rb', line 126

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 = 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