Caravan export#594
Conversation
|
@bucko13 here's a first cut at a import/export tool. Happy to walk through it with you some time. I have a few open questions, see above. |
1e84248 to
6d52e69
Compare
afilini
left a comment
There was a problem hiding this comment.
This looks very good! I've added a few minor comments, mostly suggestions to better use the rust language.
|
|
||
| /// A caravan client | ||
| #[derive(Debug, Serialize, Deserialize)] | ||
| pub struct CaravanClient { |
There was a problem hiding this comment.
I guess this could be made into an enum? Is there a list of valid values for the client?
There was a problem hiding this comment.
I don't know what the values should be but this question is on my list.
There was a problem hiding this comment.
So I think this is a good argument in favor of the generalized approach detailed in my comment where these details could/should be left up to the consumer of this import/export utility. In Caravan, the client can be public and then we just assume that points to blockstream.info. There's the private one which requires some access info for your node (host, rpcuser, rpcpass), but i don't think bdk needs to care about that.
| name: String, | ||
| client_type: String, | ||
| network: Network, | ||
| descriptor: &Descriptor<DescriptorPublicKey>, |
There was a problem hiding this comment.
I think it would be better if you could take a reference to the wallet and get the descriptor yourself. It would also give you a chance to inspect the internal descriptor like we do for fully noded.
I'm guessing the caravan wallet only supports similar descriptors for the internal keychain
There was a problem hiding this comment.
I also have it on my issues list to determine if Caravan can support change descriptors, and if so how. I expect all I need to do is change the derivation from m/0/* to m/1/* but need to confirm.
There was a problem hiding this comment.
Caravan will create a change and a receive descriptor based on the settings imported into it. We call this data structure a "braid", since once it's not a string anymore it's not really a descriptor anymore. Basically nearly all wallets today contain two braids, one at index 0 and one at 1. I'd also like to support BIP88 style, but this should be done separate from this utility imo. i.e. in JS expand the single bip88 descriptor into two (or more?) descriptors and then feed that through bdk.
|
As a sidenote: the way this is coded it only supports importing/exporting public keys. Is this the goal of caravan, giving you a watch-only descriptor? If you want to support private keys as well it get a bit more complicated, but still very doable! |
|
Thanks for the suggestions! will get those fixed. AFAIK caravan only supports pub keys but will add that to my list of issues to get an answer on. |
|
🎉 exciting to see progress on this! One general comment that will probably impact the further development of this is that I think it might be easier, especially for long term maintenance, to keep this as general as possible and maybe not even Caravan specific at all. One of the primary things that I think is missing from the space is a way to effectively parse and encode descriptors in JavaScript based applications. And to do it in a way that is relying on the canonical implementations of the descriptor/miniscript tooling would be ideal so we know we have all the guarantees that the reference implementation(s) provide (without having to maintain). The primary use case I envision for bdk bindings in wasm is that we would be able to feed some function a descriptor string (maybe even support for more complex policies for future extensibility, but really multisig descriptors is all Caravan and nearly every other coordinator in the space really cares about) to a function and get a parseable object/JSON of the Extended Key Derivation/Origin information (XKey from bdk kind of I think? There are so many names for this amalgamation of information). The rest, such as client information, is totally ancillary and implementation specific. If you think about where this would be used in Caravan, it would be in the main wallet loading area. A descriptor string text input could supersede most of the fields on this screen, and that's what I think would be most useful for bdk to handle. The nice thing about generalizing it this way too is that your team doesn't have to try to keep up maintainability with whatever the caravan config may or may not need to support. So considering the screenshot above, a possible user flow would look like:
The nice thing about this too imo is that it could be used as a common interface to convert to any other wallet config pretty easily once we have this to/from descriptor api, e.g. Descriptor -> Caravan -> Coldcard. The final step could be facilitated completely in the JS side with something that looks like this: const coldcardConfig = '...'
const wallet = ColdcardConfig.deserialize(coldcardConfig)
wallet.getDescriptors() // ["wsh(sortedmulti(2,[3h8280]xpub.../0/*, [748d8s0]xpub.../0/*))", "..."]
const descriptor = wallet.toBip88Descriptor() // "wsh(sortedmulti(2,[3h8280]xpub.../{0,1}/*, [748d8s0]xpub.../{0,1}/*))"
const carvanConfig = CaravanWalletConfig.fromBip88Descriptor(
descriptor,
{
...wallet, // wallet.name, wallet.uuid, wallet.network
startingAddressIndex: 10
}
)
caravanConfig.serialize() // { ... } -> caravan compatible json
wallet.serialize() // "..." -> coldcard wallet config text
// these should be the same and export standard descriptors
// that can be imported to bitcoind (for example)
assert_equal(caravanConfig.getDescriptors(), wallet.getDescriptors())Under the hood, the Anyway, that's just how I've been thinking about it and I feel like should make your work easier. For priv keys: (other questions answered inline) |
| //! "quorum": { | ||
| //! "requiredSigners": 2, | ||
| //! "totalSigners": 2 | ||
| //! }, |
There was a problem hiding this comment.
Ugh, yeah just seeing this in here, I'd hate to have you have to support our janky serialization standard. This is really redundant information. We should just have requiredSigners and then "total" can be inferred from the number of extended public keys. We have to support this for backwards compatibility but I don't think it's worth bdk having to take on that code debt.
| .map(|k| { | ||
| let fingerprint = k.xfp; | ||
| let key_path = k.bip32_path.clone(); | ||
| let key_source = fingerprint.zip(key_path); |
There was a problem hiding this comment.
Forgive the rust ignorance, but what's the advantage of doing this rather than
let key_source = (fingerprint, key_path)
There was a problem hiding this comment.
fingerprint and key_path are both optional items, but what you want is not a tuple of options but an optional tuple!
doing what you described would yield the type (Option<Fingerprint>, Option<DerivationPath>), but what we want here is a Option<(Fingerprint, DerivationPath)>.
There was a problem hiding this comment.
Essentially you want either to have both of them or none of them. Having a tuple of options allows you to have just one but not the other, but if you change it to an optional tuple you either have the tuple or don't.
There was a problem hiding this comment.
Fantastic. Thanks for the explanation! Still new to rust :). Learning that Options are an important building block.
| ) -> Result<Self, Error> { | ||
| let (address_type, quorum, descriptor_public_keys) = match descriptor { | ||
| Descriptor::Sh(sh) => match sh.as_inner() { | ||
| ShInner::SortedMulti(smv) => { |
There was a problem hiding this comment.
I guess it's an abbreviation of "sorted multi value" (?).
It doesn't matter too much, in that context it's basically used as a variable name, you could call it anything.
There was a problem hiding this comment.
Yep, makes sense. Just curious as it helps to follow the code imo.
| Self::export(network, descriptor, name, client_type) | ||
| } else { | ||
| Err(Error::Generic( | ||
| "Can not export a wallet with a change descriptor to Caravan.".to_string(), |
There was a problem hiding this comment.
So, I think this would need to change right? Caravan does have change as well as receive index support.
e6155cd to
c71a9a6
Compare
|
For the larger discussion about using BDK for generic parsing of descriptors for use in a TypeScript app I think we can do this with the existing (but still experimental) descriptor::policy module. This module will parse any descriptor into a common If you want to see what the Policy JSON output looks like you can use the bdk-cli wallet -d $ALICE_DESCRIPTOR policiesIf this looks like something you can use I'll make an example for doing the same parsing from TypeScript using the Ultimately I believe descriptors (with public keys) will be the "lingua franca" for all wallet configs since they contain everything a user needs (besides the master keys) to restore a wallet. Even if you include other ancillary info in your config that other wallets won't understand such as wallet name, mapping user entered names to key fingerprints, the blockchain client configs, etc. the user can still always use the descriptors to monitor and spend using any descriptor based wallet software. |
c71a9a6 to
7f1e05a
Compare
Yep, this is exactly my thinking and what my larger goal for using this tool would be. So, I tested out bdk-cli locally: $ ➜ ALICE="wsh(sortedmulti(1,[9d120b19/48'/0'/0'/2']xpub6FDrnnUsgQSwRFazYbVDs9eadQaNV13f5dtQDoWrCuMNq2qgMH7GevctMAm3PeHq3KBkh9BgA8iPfaHYACHFpfueYdeAUtjjEH3vMJWEKfu,[5c9e228d/48'/0'/0'/2']xpub6EgGHjcvovyN3nK921zAGPfuB41cJXkYRdt3tLGmiMyvbgHpss4X1eRZwShbEBb1znz2e2bCkCED87QZpin3sSYKbmCzQ9Sc7LaV98ngdeX))#dh7v9p4x"
$ ➜ bdk-cli -n bitcoin wallet -d $ALICE policies With the following return: {
"external": {
"contribution": {
"items": [],
"m": 1,
"n": 2,
"sorted": true,
"type": "PARTIAL"
},
"id": "hw3hgusf",
"keys": [
{
"fingerprint": "9d120b19"
},
{
"fingerprint": "5c9e228d"
}
],
"satisfaction": {
"items": [],
"m": 1,
"n": 2,
"sorted": true,
"type": "PARTIAL"
},
"threshold": 1,
"type": "MULTISIG"
},
"internal": null
}
Information it looks like we do get from
Unless there's something in my command that I'm missing (didn't see any additional options in the
inferring the network from the xpub version as well would be nice, but as discussed elsewhere, it's not a guarantee (could let that be the responsibility of the consuming application to validate or allow to be overwritten). It looks like most of this work is being done in this PR by the parse_descriptor function |
No there's no other options right now to get the above extra info. But I agree if we added that info somewhere in the |
| if wallet.change_descriptor.is_none() { | ||
| return Err(Error::Generic( | ||
| "Wallet much have an internal descriptor".to_string(), | ||
| )); | ||
| } | ||
| let internal_descriptor = wallet | ||
| .change_descriptor | ||
| .as_ref() | ||
| .expect("internal descriptor"); |
There was a problem hiding this comment.
Would it better to use a match statement here?
Also there seems to be a typo on line 206: "much" -> "must"
46199c5 to
0343ab6
Compare
0343ab6 to
8a39387
Compare
|
@bucko13 I've extracted this code into a new standalone project that uses the new bdk 1.0-alpha as a dependency and is compiled to wasm for use in a web page. I also created a very basic web page example. Everything should work for converting between descriptors and caravan config json. But much docs and cleanup needed before publishing it as a npm package. https://github.com/notmandatory/caravan-rs |
|
Very old PR and not part of 1.0 milestone so closing for now. May take it back up post 1.0 as an example or new add-on module. |

Description
This is a new module for importing and exporting a wallet descriptor and network from and to a Caravan config JSON file.
Notes to the reviewers
I only have minimal happy path tests now. More negative test cases are needed.
Open issues:
client.typefield? are there enum values?.extendedPublicKeys[].namefields when exporting?Checklists
All Submissions:
cargo fmtandcargo clippybefore committingNew Features:
CHANGELOG.mdBugfixes: