bdk icon indicating copy to clipboard operation
bdk copied to clipboard

Add DescriptorKey derive, extend, to_public and to_string functions

Open notmandatory opened this issue 3 years ago • 5 comments

Description

Add DescriptorKey functions generate_xprv, restore_xprv, derive, extend, to_public and to_string.

  • DescriptorKey::generate_xprv generate an ExtendedPrivKey based DescriptorKey for a given network with the default generate options and random entropy. The key will be a master xkey with no origin or extended derivation path.

  • DescriptorKey::restore_xprv Restore an ExtendedPrivKey based DescriptorKey for a given network from Mnemonic seed words and optional password (BIP-39). The key will be a master xkey with no origin or extended derivation path.

  • DescriptorKey::derive derive a new DescriptorKey from the origin key at a given DerivationPath keeping the extended path part the same.

  • DescriptorKey::extend extend the extended path of a DescriptorKey with a given DerivationPath keeping the origin part the same.

  • DescriptorKey::as_public dreate a new public key version of the DescriptorKey, if already public return a copy of the same key.

Notes to the reviewers

The changes in this PR are based on work and discussion around https://github.com/bitcoindevkit/bdk-ffi/pull/154.

The goal is to create simplified functions that can be used in the bdk-ffi and bdk-cli projects.

Checklists

All Submissions:

  • [x] I've signed all my commits
  • [x] I followed the contribution guidelines
  • [x] I ran cargo fmt and cargo clippy before committing

New Features:

  • [x] I've added tests for the new feature
  • [x] I've added docs for the new feature
  • [x] I've updated CHANGELOG.md

notmandatory avatar Jul 15 '22 19:07 notmandatory

@afilini this is the PR I mentioned in the language bindings call today. Would be great to have a sanity check on the API. I put the new functions at the DescriptorKey level, but not sure if this is the right place. Maybe it makes more sense to add these new functions on DescriptorXKey?

notmandatory avatar Jul 26 '22 13:07 notmandatory

I just looked into how it'd work to add my new functions to DescriptorXKey and realized it's in the miniscript crate, so not as easy to change.

notmandatory avatar Jul 26 '22 14:07 notmandatory

Considering those are just wrappers for our own API, I think maybe it makes more sense to provide those as examples and let our users either copy/paste those or figure out if they can take advantage of the API in a different way.

I'll add some comments to the code directly if there's anything that can be done better.

afilini avatar Jul 27 '22 13:07 afilini

I'll try to write down a comprehensive description of all our key traits because I understand they are pretty complicated, but I think there's a good reason for it. We can consider adding this as well to our docs together with the examples.

Here's the meaning of all the emoji I'm going to use to mark what a given type/trait can work with:

  • :key:: Private key
  • :lock:: Public key
  • :one:: Single key (either a raw public key or WIF when private)
  • :infinity:: Extended/derivable key (either xpub if public or xprv if private)

Now all of our traits:

  • IntoDescriptorKey :key: :lock: :one: :infinity:: This trait is used to convert any key type into a DescriptorKey instance. It's used internally by the descriptor!() and fragment!() macros, which calls the into_descriptor_key() method on all the keys passed to the macro. If you want to implement a custom key type that works with BDK, you can implement this trait on your type.

  • DerivableKey: :key: :lock: :infinity:: This trait is implemented on public or private keys that can be derived. It can be used to obtain an ExtendedKey or even a DescriptorKey directly by adding the extra data that are usually attached to an extended key inside a descriptor (a derivation path and, optionally, a key origin).

    IntoDescriptorKey is automatically implemented for any tuple (K, DerivationPath) or (K, KeySource, DerivationPath) where K implements DerivableKey. Because of this, if you want to implement a custom key type that is always derivable, you should consider implementing DerivableKey instead of IntoDescriptorKey directly, because it will allow you to "compose" your type with the standard derivation path and origin types.

  • GeneratableKey: :key: :one: :infinity:: This trait is implemented on key types that can be generated from an entropy source. It provides two methods to generate from a given entropy or from self-generated entropy using the rand crate. This trait also defines a generic Options associated type, which can be used to "tweak" the generation process. For example, the options can be used to set whether a WIF PrivateKey should be compressed or not. Or it can be used to set the number of words and language of a BIP39 mnemonic.

    When generating with internal or external entropy the options must always be provided, which can be annoying in some cases (see GeneratableDefaultOptions).

    If you have a custom key type that can be generated from entropy, you should implement this trait as well on your type.

  • GeneratableDefaultOptions: :key: :one: :infinity:: This trait provides two more methods to generate keys using default options, either with externally provided or with self-generated entropy. It's automatically implemented on any type that also implements GeneratableKey, provided the Options associated type also implements the Default trait.

And all of our types:

  • DescriptorKey :key: :lock: :one: :infinity:: This is the most generic key type possible. It can contain anything, private or public, derivable or not, keys. It's kind of a "blind" type, you can't really look into it from the outside. BDK uses this in its descriptor!() and fragment!() macros: since we can't mix different types in vectors and containers, we turn all the keys passed to the macro into this type to make sure they all have the same type. Why this type specifically? Because it can contain anything. How do we perform the conversion? Using the IntoDescriptorKey trait.

  • ExtendedKey :key: :lock: :infinity:: This type is an enum that can contain either a private or a public extended key. This type can be "looked into" from the outside and provides methods to extract the key as public or secret (the latter returning an Option which is None if the extended key is an xpub).

    As you may imagine, it implements the DerivableKey trait.

  • GeneratedKey<K> :key: :one: :infinity:: This is a wrapper over a newly generated key. All the methods of GeneratableKey and GeneratableDefaultOptions when implemented on K instead of directly returning a K instance, they return GeneratedKey<K>. This is because we have to deal with types that internally encode a network in which they are valid, but right after generation we don't know on which network the key is going to be used. So we keep our own set of valid networks alongside the key in this wrapper.

    This type provides a method to extract the K inside (into_key()) and also implements IntoDescriptorKey (always) and DerivableKey (if K also implements it). This means that, in general, you don't have to extract the type and you can directly use this in descriptor!() or fragments!() macro calls.

Why all this complexity?

Because we want to support custom key types. BDK itself supports all the key types from rust-bitcoin out of the box (xprv, xpub, wif, raw pubkey) and BIP39 keys when compiled with the keys-bip39 feature. But there are still a ton of other types out there that we don't support (yet) and we want to be compatible with them as well.

With these traits you can add support for custom key types/encoding and use them directly with BDK descriptors.

You can also integrate this with key storage mechanisms: imagine having a SecureEnclave type that is a wrapper that can talk to the enclave of a modern mobile phone. You can have a method "unlock" that returns an UnlockedSecureEnclave when successful, and then implement IntoDescriptorKey on the unlocked enclave type. When into_descriptor_key() is called you actually extract the key from the enclave and return it. Your code could look like this:

let enclave = SecureEnclave::new();
let wallet = Wallet::new(descriptor!(wpkh(enclave.unlock()?)), ...)?;

afilini avatar Jul 27 '22 15:07 afilini

@afilini thanks for the great explanations of all the key traits, I like the idea of combining your writeup with some example code rather than adding the new functions in this PR. The immediate goal here is to figure out what the bdk-ffi API needs to look like for generating and deriving xkeys and extending their path. The part about deriving a new key from the origin is something the Block team asked for, I'll need to get more specifics from them.

I think the way forward for bdk-ffi is to make some simple wrapper types for ExtendedPrivKey and ExtendedPubKey since that's all we need right now and it's easy to convert those into a DescriptorKey when needed.

I'll clean up this code just to capture it for later examples and use in bdk-ffi and then close this PR.

notmandatory avatar Jul 28 '22 14:07 notmandatory

Very old PR and not part of 1.0.0 milestone so closing for now.

notmandatory avatar Mar 24 '24 23:03 notmandatory