Class: BTC::Keychain

Inherits:
Object
  • Object
show all
Defined in:
lib/btcruby/keychain.rb

Constant Summary collapse

MAX_INDEX =
0x7fffffff
PUBLIC_MAINNET_VERSION =

xpub

0x0488B21E
PRIVATE_MAINNET_VERSION =

xprv

0x0488ADE4
PUBLIC_TESNET_VERSION =

tpub

0x043587CF
PRIVATE_TESTNET_VERSION =

tprv

0x04358394

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(seed: nil, extended_key: nil, xpub: nil, xprv: nil, network: nil, _components: nil) ⇒ Keychain

Instantiates Keychain with a binary seed or a base58-encoded extended public/private key. Usage:

  • Keychain.new(seed: …[, network: …])

  • Keychain.new(extended_key: “xpub…” or “xprv…”)

  • Keychain.new(xpub: “xpub…”)

  • Keychain.new(xprv: “xprv…”)



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/btcruby/keychain.rb', line 187

def initialize(seed: nil,
               extended_key: nil,
               xpub: nil,
               xprv: nil,
               network: nil,
               _components: nil # private API
               )

  if seed
    init_with_seed(seed, network: network)
  elsif xkey = (xprv || xpub || extended_key)
    if network
      raise ArgumentError, "Cannot use network argument with extended key to initialize BTC::Keychain (network type is already encoded in the key)"
    end
    if [xprv, xpub, extended_key].compact.size != 1
      raise ArgumentError, "Only one of xpub/xprv/extended_key arguments could be used to initialize BTC::Keychain"
    end
    init_with_extended_key(xkey)
  elsif _components
    init_with_components(*_components)
  else
    raise ArgumentError, "Either seed or an extended " if !private_key && !public_key
  end
end

Instance Attribute Details

#chain_codeObject (readonly)

32-byte binary “chain code” string.



35
36
37
# File 'lib/btcruby/keychain.rb', line 35

def chain_code
  @chain_code
end

#depthObject (readonly)

Depth in the hierarchy (integer). Returns 0 for master keychain.



65
66
67
# File 'lib/btcruby/keychain.rb', line 65

def depth
  @depth
end

#extended_private_keyObject (readonly)

Base58Check-encoded extended private key. Returns nil if it’s a public-only keychain.



43
44
45
# File 'lib/btcruby/keychain.rb', line 43

def extended_private_key
  @extended_private_key
end

#extended_public_keyObject (readonly)

Base58Check-encoded extended public key.



38
39
40
# File 'lib/btcruby/keychain.rb', line 38

def extended_public_key
  @extended_public_key
end

#fingerprintObject (readonly)

Fingerprint of the keychain (integer).



53
54
55
# File 'lib/btcruby/keychain.rb', line 53

def fingerprint
  @fingerprint
end

#identifierObject (readonly)

160-bit binary identifier (aka “hash”) of the keychain (RIPEMD160(SHA256(pubkey)))



47
48
49
# File 'lib/btcruby/keychain.rb', line 47

def identifier
  @identifier
end

#identifier_base58Object (readonly)

Base58Check-encoded identifier.



50
51
52
# File 'lib/btcruby/keychain.rb', line 50

def identifier_base58
  @identifier_base58
end

#indexObject (readonly)

Index in the parent keychain (integer). Returns 0 for master keychain.



61
62
63
# File 'lib/btcruby/keychain.rb', line 61

def index
  @index
end

#keyObject (readonly)

Instance of BTC::Key that is a “head” of this keychain. If the keychain is public-only, key does not have a private component.



32
33
34
# File 'lib/btcruby/keychain.rb', line 32

def key
  @key
end

#networkObject

Network.mainnet or Network.testnet. Default is BTC::Network.default (mainnet if not overriden).



69
70
71
# File 'lib/btcruby/keychain.rb', line 69

def network
  @network
end

#parent_fingerprintObject (readonly)

Fingerprint of the parent keychain (integer). For master keychain it is always 0.



57
58
59
# File 'lib/btcruby/keychain.rb', line 57

def parent_fingerprint
  @parent_fingerprint
end

#xprvObject (readonly)

Returns the value of attribute xprv.



44
45
46
# File 'lib/btcruby/keychain.rb', line 44

def xprv
  @xprv
end

#xpubObject (readonly)

Returns the value of attribute xpub.



39
40
41
# File 'lib/btcruby/keychain.rb', line 39

def xpub
  @xpub
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



155
156
157
158
159
# File 'lib/btcruby/keychain.rb', line 155

def ==(other)
  self.identifier == other.identifier &&
    self.private? == other.private? &&
    self.mainnet? == other.mainnet?
end

#derived_key(index_or_path, hardened: nil) ⇒ Object

Returns a derived BTC::Key from this keychain. This is a convenient way to do keychain.derived_keychain(i).key If the receiver contains a private key, child key will also contain a private key. If the receiver contains only a public key, child key will only contain a public key. (Or nil will be returned if hardened = true.) By default, a normal (non-hardened) derivation is used.



442
443
444
# File 'lib/btcruby/keychain.rb', line 442

def derived_key(index_or_path, hardened: nil)
  self.derived_keychain(index_or_path, hardened: hardened).key
end

#derived_keychain(index_or_path, hardened: nil) ⇒ Object

Returns a derived keychain at a given index. If hardened = true, uses hardened derivation (possible only when private key is present; otherwise returns nil). Index must be less of equal BTC::Keychain::MAX_INDEX, otherwise raises ArgumentError. Raises BTCError for some indexes (when hashing leads to invalid EC points) which is very rare (chance is below 2^-127), but must be expected. In such case, simply use next index. By default, a normal (non-hardened) derivation is used.

Raises:

  • (ArgumentError)


330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/btcruby/keychain.rb', line 330

def derived_keychain(index_or_path, hardened: nil)

  if index_or_path.is_a?(String)
    if hardened != nil
      raise ArgumentError, "Ambiguous use of `hardened` flag when deriving keychain with a string path"
    end
    return derived_keychain_with_path(index_or_path)
  end

  index = index_or_path

  raise ArgumentError, "Index must not be nil" if !index

  # As we use explicit "hardened" argument, do not allow higher bit set.
  if index < 0 || index > 0x7fffffff || (0x80000000 & index) != 0
    raise ArgumentError, "Index >= 0x80000000 is not valid. Use `hardened: true` argument instead."
  end

  if hardened && !@private_key
    # Not possible to derive hardened keychain without a private key.
    raise BTCError, "Not possible to derive a hardened keychain without a private key (index: #{index})."
  end

  private_key = nil
  public_key = nil
  chain_code = nil

  data = "".b

  if hardened
    data << "\x00" << @private_key
  else
    data << @public_key
  end

  data << BTC::WireFormat.encode_uint32be(hardened ? (0x80000000 | index) : index)

  digest = BTC.hmac_sha512(data: data, key: @chain_code)

  chain_code = digest[32,32]

  lib = BTC::OpenSSL
  lib.autorelease do |pool|

    factor = pool.new_bn(digest[0,32])
    n = lib.group_order

    if lib.BN_cmp(factor, n) >= 0
      raise BTCError, "Factor for index #{index} is greater than curve order."
    end

    if @private_key
      pk = pool.new_bn(@private_key)
      lib.BN_mod_add_quick(pk, pk, factor, n) # pk = (pk + factor) % n

      # Check for invalid derivation.

      if lib.BN_cmp(pk, pool.new_bn("\x00")) == 0
        raise BTCError, "Private key is zero for index #{index}."
      end

      private_key = lib.data_from_bn(pk, min_length: 32)
    else

      # Convert pubkey to a EC point
      pubkey_x = pool.new_bn(@public_key)
      pubkey_point = pool.new_ec_point
      lib.EC_POINT_bn2point(lib.group, pubkey_x, pubkey_point, pool.bn_ctx)

      # Compute point = pubkey + factor*G
      point = pool.new_ec_point
      # /** Computes r = generator * n + q * m
      #  *  \param  group  underlying EC_GROUP object
      #  *  \param  r      EC_POINT object for the result
      #  *  \param  n      BIGNUM with the multiplier for the group generator (optional)
      #  *  \param  q      EC_POINT object with the first factor of the second summand
      #  *  \param  m      BIGNUM with the second factor of the second summand
      #  *  \param  ctx    BN_CTX object (optional)
      #  *  \return 1 on success and 0 if an error occured
      #  */
      # int EC_POINT_mul(const EC_GROUP *group, EC_POINT *r, const BIGNUM *n, const EC_POINT *q, const BIGNUM *m, BN_CTX *ctx);
      lib.EC_POINT_mul(lib.group, point, factor, pubkey_point, pool.new_bn("\x01"), pool.bn_ctx)

      # Check for invalid derivation.
      if 1 == lib.EC_POINT_is_at_infinity(lib.group, point)
        raise BTCError, "Resulting point is at infinity for index #{index}."
      end

      lib.EC_POINT_point2bn(lib.group, point, BTC::OpenSSL::POINT_CONVERSION_COMPRESSED, pubkey_x, pool.bn_ctx)

      public_key = lib.data_from_bn(pubkey_x, required_length: 33)
    end
  end

  self.class.new(_components: [
                 private_key,
                 public_key,
                 chain_code,
                 nil,
                 self.fingerprint,
                 index,
                 @depth + 1,
                 !!hardened,
                 @network])
end

#derived_keychain_with_path(path) ⇒ Object



446
447
448
449
450
451
452
453
454
# File 'lib/btcruby/keychain.rb', line 446

def derived_keychain_with_path(path)
  path.gsub(/^m\/?/, "").split("/").inject(self) do |keychain, segment|
    if segment =~ /^\d+\'?$/
      keychain.derived_keychain(segment.to_i, hardened: (segment[-1] == "'"))
    else
      raise ArgumentError, "Incorrect path format. Should be (\d+'?) separated by '/'."
    end
  end
end

#dupObject



162
163
164
# File 'lib/btcruby/keychain.rb', line 162

def dup
  Keychain.new(extended_key: self.xprv || self.xpub)
end

#hardened?Boolean

Returns true if the keychain was derived via hardened derivation from its parent. This means internally parameter i = 0x80000000 | self.index. For the master keychain index is zero and hardened? returns false.

Returns:

  • (Boolean)


84
85
86
# File 'lib/btcruby/keychain.rb', line 84

def hardened?
  !!@hardened
end

#mainnet?Boolean

Returns true if this keychain is intended for mainnet.

Returns:

  • (Boolean)


99
100
101
# File 'lib/btcruby/keychain.rb', line 99

def mainnet?
  network.mainnet?
end

#private?Boolean

Returns true if the keychain can derive private keys (opposite of #public?).

Returns:

  • (Boolean)


72
73
74
# File 'lib/btcruby/keychain.rb', line 72

def private?
  !!@private_key
end

#public?Boolean

Returns true if the keychain can only derive public keys (opposite of #private?).

Returns:

  • (Boolean)


77
78
79
# File 'lib/btcruby/keychain.rb', line 77

def public?
  !@private_key
end

#public_keychainObject

Returns a copy of the keychain stripped of the private key. Equivalent to BTC::Keychain.new(xpub: keychain.xpub)



168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/btcruby/keychain.rb', line 168

def public_keychain
  self.class.new(_components: [
                 nil, # private_key
                 @public_key,
                 @chain_code,
                 @fingerprint,
                 @parent_fingerprint,
                 @index,
                 @depth,
                 @hardened,
                 @network])
end

#testnet?Boolean

Returns true if this keychain is intended for testnet.

Returns:

  • (Boolean)


104
105
106
# File 'lib/btcruby/keychain.rb', line 104

def testnet?
  network.testnet?
end

#to_sObject



116
117
118
# File 'lib/btcruby/keychain.rb', line 116

def to_s
  private? ? xprv : xpub
end