bdk
bdk copied to clipboard
Add DescriptorKey derive, extend, to_public and to_string functions
Description
Add DescriptorKey functions generate_xprv, restore_xprv, derive, extend, to_public and to_string.
-
DescriptorKey::generate_xprvgenerate anExtendedPrivKeybasedDescriptorKeyfor 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_xprvRestore anExtendedPrivKeybasedDescriptorKeyfor a given network fromMnemonicseed words and optional password (BIP-39). The key will be a master xkey with no origin or extended derivation path. -
DescriptorKey::derivederive a newDescriptorKeyfrom the origin key at a givenDerivationPathkeeping the extended path part the same. -
DescriptorKey::extendextend the extended path of aDescriptorKeywith a givenDerivationPathkeeping the origin part the same. -
DescriptorKey::as_publicdreate a new public key version of theDescriptorKey, 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 fmtandcargo clippybefore 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
@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?
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.
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.
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
DescriptorKeyinstance. It's used internally by thedescriptor!()andfragment!()macros, which calls theinto_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
ExtendedKeyor even aDescriptorKeydirectly by adding the extra data that are usually attached to an extended key inside a descriptor (a derivation path and, optionally, a key origin).IntoDescriptorKeyis automatically implemented for any tuple(K, DerivationPath)or(K, KeySource, DerivationPath)whereKimplementsDerivableKey. Because of this, if you want to implement a custom key type that is always derivable, you should consider implementingDerivableKeyinstead ofIntoDescriptorKeydirectly, 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
randcrate. This trait also defines a genericOptionsassociated type, which can be used to "tweak" the generation process. For example, the options can be used to set whether a WIFPrivateKeyshould 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 theOptionsassociated type also implements theDefaulttrait.
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!()andfragment!()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 theIntoDescriptorKeytrait. -
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
Optionwhich isNoneif the extended key is an xpub).As you may imagine, it implements the
DerivableKeytrait. -
GeneratedKey<K> :key: :one: :infinity:: This is a wrapper over a newly generated key. All the methods of
GeneratableKeyandGeneratableDefaultOptionswhen implemented onKinstead of directly returning aKinstance, they returnGeneratedKey<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
Kinside (into_key()) and also implementsIntoDescriptorKey(always) andDerivableKey(ifKalso implements it). This means that, in general, you don't have to extract the type and you can directly use this indescriptor!()orfragments!()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 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.
Very old PR and not part of 1.0.0 milestone so closing for now.