-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: Have the wallet store the key for automatically generated descriptors #26728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code CoverageFor detailed information about the code coverage, see the test coverage report. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
27a4176
to
4b9ac3e
Compare
4b9ac3e
to
b756f22
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approach ACK
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for taking your time to rework #23417. It looks like a great improvement
I left some high-level comments inline and will continue with more in depth review.
src/wallet/wallet.h
Outdated
//! The extended public key to be used for new automatically generated descriptors | ||
CExtPubKey m_xpub GUARDED_BY(cs_wallet); | ||
//! The previous and current extended keys that are used for automatically generated descriptors | ||
std::map<CExtPubKey, CKey> m_hd_keys GUARDED_BY(cs_wallet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This presupposes that we will and should allow changing active HD key for a given wallet. I'd like to better understand for what use-cases this is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
abac3e3 There's also the scenario where we generate a new HD key and mark the existing one inactive. We could at that point delete the old HD(C)KEY
record, in which case m_hd_key
could just hold one key. Not sure if that's much better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly what I'm curious about. Should we allow to generate a new HD key for existing wallet? I'm trying to understand why someone would like to generate new HD key for existing wallet instead of just creating a new wallet?
Or should we make a rule 1 wallet = 1 HD key. Which is much simpler to understand and manage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure of the reasoning, but it's been requested as required feature.
src/wallet/walletdb.cpp
Outdated
} | ||
|
||
CExtKey extkey(it->second, key); | ||
pwallet->AddHDKey(extkey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably I don't follow, but it seems we don't need to add all descriptor keys as HD keys to the wallet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicates are skipped (see 8b0af8a).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9a17347: this function can fail, and SetActiveHDKey
should probably called last.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant, that we can write only the key for best_xpub
. Why do we need to write all the candidates as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK. Here's my initial code review (b756f22).
src/wallet/wallet.h
Outdated
//! The extended public key to be used for new automatically generated descriptors | ||
CExtPubKey m_xpub GUARDED_BY(cs_wallet); | ||
//! The previous and current extended keys that are used for automatically generated descriptors | ||
std::map<CExtPubKey, CKey> m_hd_keys GUARDED_BY(cs_wallet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
abac3e3 There's also the scenario where we generate a new HD key and mark the existing one inactive. We could at that point delete the old HD(C)KEY
record, in which case m_hd_key
could just hold one key. Not sure if that's much better.
src/wallet/walletdb.cpp
Outdated
} | ||
|
||
CExtKey extkey(it->second, key); | ||
pwallet->AddHDKey(extkey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9a17347: this function can fail, and SetActiveHDKey
should probably called last.
src/wallet/walletdb.cpp
Outdated
std::map<std::string, int> tmpl = {{"pkh(", 0}, {"sh(wpkh(", 0}, {"wpkh(", 0}}; | ||
|
||
// Find root xpubs used in pkh(), sh(wpkh()), and wpkh() descriptors | ||
for (const auto& spkm : pwallet->GetAllScriptPubKeyMans()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9a17347: Why not limit ourselves to GetActiveScriptPubKeyMans
? Not sure how useful it is to store older HD keys, and it does seem to add some complexity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user may have imported other keys for some active spkms, so we'd be unable to determine the master if we only looked at the active ones.
b756f22
to
e1f97a3
Compare
e1f97a3
to
5cd42d2
Compare
5cd42d2
to
0c4b2bc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/wallet/walletdb.cpp
Outdated
std::map<CExtPubKey, std::pair<std::map<OutputType, int>, uint64_t>> descs_keys; | ||
std::map<OutputType, int> tmpl = {{OutputType::LEGACY, 0}, {OutputType::P2SH_SEGWIT, 0}, {OutputType::BECH32, 0}, {OutputType::BECH32M, 0}}; | ||
|
||
// Find root xpubs used in pkh(), sh(wpkh()), and wpkh() descriptors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
6bb3bee nit: and tr()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re: #26728 (comment)
re: #26728 (review)
This is essentially what we do today, albeit with hd keys rather than hd seeds.
Right. It seems like we can add the features we want without making the wallet data model more complicated, so I think we should do that. If there are features that can't work under the current model, or would work worse under the current model, that would change things, but in all of the cases I've read about it actually looks like the features would work better and be less confusing in the current model compared to the new model.
I think it is useful to have a
gethdkey
method similar to the one added here that would return the hd key associated with various descriptors. But that could be implemented more simply without changing the database format, or adding a flag, or bringing in other complexity introduced in this PR.What would the API for that look like?
Instead of returning an object with "xpub" and "xpriv" fields, it would return a list of objects with "xpub", "xpriv", and "descriptors" fields. The "descriptors" field would contain a list of descriptors using the hd key.
It would also be good for the method to accept an "active_only" option, and only return hd keys associated with active descriptors by default.
This would make it easy to get the hd key, and easy to understand how the hd key is referenced by descriptors. In the typical case it would only return 1 hd key, and in more complicated cases it would be easier to understand than the gethdmethod
in this PR, because it would give you all the information about hd keys, instead of just choosing one hd key and not telling you which descriptors it is associated with.
Ultimately, the work in this PR is the product of different iterations of how to implement
createwalletdescriptor
. The main thing blocking that is the question of which key should be used in the new descriptors. I've tried a couple of different approaches, and they all have drawbacks in some form: [...]
Thanks for describing all those approaches. It clarifies a lot, and I agree with all the drawbacks you mentioned for them. The approach I would suggest is making the hd key an optional parameter to createwalletdescriptor
, and if it is not specified, using the same hd key as other active descriptors. Unless I am misunderstanding something, in normal cases we would expect there to be exactly one hd key. But in cases where there is more than one key, we could choose the active descriptor for the default output type (like getnewaddress
), or be more conservative and raise an exception asking for an hd key to be specified to disambiguate.
For cases where the hd key is specified to createwalletdescriptor
, I think we would want it to be flexible and accept multiple formats, but accepting "xpub" value from gethdkey
would be an obvious start. It could also accept the xpriv value, or even the seed keys or bytes. If it accepted private keys or seeds, we could treat it as a generalized alternative to sethdseed
, which would be great because it would simplify usage, and let us deprecate the sethdseed
method instead of keeping both methods.
The goal of this PR is to provide an answer for this question. Whenever there is more than one hd key, there is a UX problem in how the user is supposed to specify it. So this PR resolves that by picking only one key. Then there is the problem with upgrading. This PR solves that by being consistent with new wallets - new wallets have a master key that is automatically generated and used in all the automatically generated descriptors, so it tries to reverse engineer the hd key from the existing descriptors. Using the automatically generated key means that all wallets, both old and new, should behave consistently.
Yeah I appreciate the cleverness of the implementation, and the fact that you actually found a way to add a new field to the database that depends on existing fields, and still allow the wallet to be opened by older software, and magically get the new software to detect whether the existing fields changed to update the new field.
But unless I'm missing something (which is possible and maybe likely), I don't actually see how this magic improves the UX. It seems like it only returns less information to the user and makes wallet behavior more opaque in the use-cases I've seen so far.
does it make sense for getwalletinfo to start returning an hdseedid value?
We can't do that since we're dealing with hd keys rather than hd seeds. The seed is hashed in order to become a master key, and descriptor wallets always throw that away once the master key is computed as it is no longer needed. We wouldn't be able to compute this for old wallets.
Wow yeah, I didn't realize descriptor wallets actually generate the seed but then throw it away only keeping the master key. I guess there are pros and cons to that, but it's another topic and mostly unrelated.
98b6fad
to
ea6a87a
Compare
Hmm. I suppose that could be a viable approach. I will experiment with it. I know that @Sjors has some ideas for |
IIUC the approach proposed by @ryanofsky is to basically figure out the key on demand instead of storing it in the DB. So this works for generating tr() descriptors for existing wallets, but it doesn't work that well for #24861. The desired flow is to have a wallet with HD key but without descriptors and then retrieve an xpub to construct multisig descriptor. How would it work if we don't have an HD key associated with the wallet? In my opinion, approach proposed in this PR resolves a regression we had with transition to descriptor wallets. In legacy we actually had a proper HD key residing in a wallet. And this is the same thing we want to bring back to descriptors. 1 wallet = 1 HD key. Yes, the matter is complicated by the fact that users can add any other keys they wish, but the model and the default flow are actually pretty straightforward. |
@achow101 wrote:
Another drawback of that approach is that it requires a new backup, e.g. for anyone who just wants to add taproot support to a wallet that doesn't have it yet. Something I still need to do, but waiting for this PR to land, is to enable adding a @S3RK wrote:
Indeed. There's different proposals out there of how to derive keys for a multisig wallet, but I'm mainly thinking of two approaches:
I prefer (2) because I think you shouldn't co-mingle multisig and single sig funds in the same wallet, for privacy reason and because it's potentially confusing. Multisig setups always require a new backup, so if we need to generate a fresh master key that's not a big deal to me. |
Best reviewed with `git show -w`
…ster The test creates a wallet on master, downgrades and encrypts the wallet. Then, it tries to open it again on master.
ea6a87a
to
d1caefc
Compare
@Sjors and @S3RK, can you help me out by being specific about the flows you have in mind? Like in terms of what RPC calls users will make to do a particular operation? I'm trying to fill in the gaps myself, but in the cases I can think of, it seems like adding the concept of a sticky master key doesn't actually make things simpler, just more opaque and confusing. I understand that if you were designing a new wallet model and API, you would both prefer an approach that required all active descriptors to have a single master key, and would take advantage of having that master key to simplify certain things. But I see real drawbacks to usability and code complexity if we are going to try to pretend that the wallet only has an single active hd key while not actually enforcing this. I think you if compare the API implemented in this PR and its followups to the API proposed in #26728 (review) I think you would see the same problems and hopefully agree. To be specific:
For external signing and multisig, I could believe in theory that there are cases that may be more complicated by not having a sticky hd key. But I haven't seen examples of that yet, and don't think it should be hard to come up with them if they exist. Also I wouldn't want to make the wallet model more complicated prematurely if it seems like all the improvements we want to make are possible without this. re: #26728 (comment)
This flow actually sounds more complicated than what I would expect. If you have a blank wallet, why would you want creating a multisig desriptor and creating the hd key to be two separate steps, and require a separate
Yes I can see the aspiration to do this, and I could imagine a different wallet model with a less flexible re: #26728 (comment)
I think I am probably missing the point and don't have enough context, but I don't see how the current PR helps with this. I don't see how the wallet having a sticky hd key would help working with an external signer or creating a tr() descriptor. Like in general I could see how the wallet storing more information could help it work with external signers, but I don't understand how storing this particular bit of information would help.
I see, but that suggests this PR is good because the limitations it adds will force users to choose approach (2). So if you have a wallet with default descriptors you can't import a multisig using the same hd key, and if you have a wallet with a multisig descriptor you can't create default descriptors with the same hd key. But it seems like the approach in this PR would only be enforcing that selectively, and the resulting behavior would probably just be confusing. Wouldn't it be simpler and clearer to just have explicit checks when adding descriptors for the combinations of descriptors you don't want to allow? Again here I think it would be helpful to be more concrete and think about the specific RPC methods that would be used for these flows. I definitely don't understand everything and there could be real reasons why the approach in this PR makes sense. Just so far I haven't seen what the benefits are, and the drawbacks seem unpleasant. |
re: #26728 (comment)
Replying to my own comment, since I had a realization after I posted it and made the edit above. I think neither the approach in this PR nor the suggested approach in #26728 (review) will support multisig well by themselves. But I think this PR's approach of deriving a master key from existing (non-multisig) descriptors, storing it separately, and adding new wallet code in followup PRs to rely on it puts us in a worse starting place for providing a good multisig UX than the suggested approach. Mostly because of the complexity it adds, but also because it does not assign a specific purpose to the HD key, so it could encourages uses that "co-mingle multisig and single sig funds in the same wallet" like Sjors was trying to avoid. I don't think the flow for how we support multisig has to be decided now, since it's beyond the scope of this PR. But I do want to spell out how I think things could work more simply without this PR. I think to support multisig and other more complicated cases well we will want to give the wallet the ability to store an HD master key not associated with any descriptor, and which can be used later to create complicated descriptors. But this could be supported with a new wallet field holding a This would allow a simpler flow for creating multisig wallets without adding a permanent burden and increase in complexity after they are created. |
achow101, S3RK, Sjors, and me talked about this on a call. I think the plan for now is that achow will make 2 independent PRs targeted at different use-cases:
Update: replacement for this PR which adds |
re: #26728 (comment)
This is not actually true. Thinking about this more, I wonder if a more natural way of representing standalone keys in the wallet is just to treat them as a new descriptor type, like We could treat this as an internal implementation detail, just using But I think the main advantage of this approach would just be fitting better into the existing database format, keeping it clean and easy to reason about, and also having more backwards compatibility. The idea of naming the descriptor |
@ryanofsky interesting idea, I especially like the backup implication, because descriptors can be used for paper backups. |
Thanks, I was also trying to think of names for these other than void descriptors. For example maybe they could be called data descriptors and written like data(xpub...) to convey the idea that these descriptors are just used to hold data and don't produce scriptpubkeys. |
Nice idea, I think that could work. These could be imported with |
Superseded by #29130 I don't think there's a reason to come back to this design, but the branch won't be deleted so we can reopen if desired. |
…Cs for adding new automatically generated descriptors 746b6d8 test: Add test for createwalletdescriptor (Ava Chow) 2402b63 wallet: Test upgrade of pre-taproot wallet to have tr() descriptors (Ava Chow) 460ae1b wallet, rpc: Add createwalletdescriptor RPC (Ava Chow) 8e1a475 wallet: Be able to retrieve single key from descriptors (Ava Chow) 85b1fb1 wallet: Add GetActiveHDPubKeys to retrieve xpubs from active descriptors (Ava Chow) 73926f2 wallet, descspkm: Refactor wallet descriptor generation to standalone func (Andrew Chow) 54e74f4 wallet: Refactor function for single DescSPKM setup (Andrew Chow) 3b09d0e tests: Test for gethdkeys (Ava Chow) 5febe28 wallet, rpc: Add gethdkeys RPC (Ava Chow) 66632e5 wallet: Add IsActiveScriptPubKeyMan (Ava Chow) fa6a259 desc spkm: Add functions to retrieve specific private keys (Ava Chow) fe67841 descriptor: Be able to get the pubkeys involved in a descriptor (Ava Chow) ef67458 key: Add constructor for CExtKey that takes CExtPubKey and CKey (Ava Chow) Pull request description: This PR adds a `createwalletdescriptor` RPC which allows users to add new automatically generated descriptors to their wallet, e.g. to upgrade a 0.21.x wallet to contain a taproot descriptor. This RPC takes 3 arguments: the output type to create a descriptor for, whether the descriptor will be internal or external, and the HD key to use if the user wishes to use a specific key. The HD key is an optional parameter. If it is not specified, the wallet will use the key shared by the active descriptors, if they are all single key. For most users in the expected upgrade scenario, this should be sufficient. In more advanced cases, the user must specify the HD key to use. Currently, specified HD keys must already exist in the wallet. To make it easier for the user to know, `gethdkeys` is also added to list out the HD keys in use by all of the descriptors in the wallet. This will include all HD keys, whether we have the private key, for it, which descriptors use it and their activeness, and optionally the extended private key. In this way, users with more complex wallets will be still be able to get HD keys from their wallet for use in other scenarios, and if they want to use `createwalletdescriptor`, they can easily get the keys that they can specify to it. See also #26728 (comment) ACKs for top commit: Sjors: re-utACK 746b6d8 furszy: ACK 746b6d8 ryanofsky: Code review ACK 746b6d8, and this looks ready to merge. There were various suggested changes since last review where main change seems to be switching `gethdkeys` output to use normalized descriptors (removing hardened path components). Tree-SHA512: f2849101e6fbf1f59cb031eaaaee97af5b1ae92aaab54c5716940d210f08ab4fc952df2725b636596cd5747b8f5beb1a7a533425bc10d09da02659473516fbda
The wallet currently does not know the master key that was used to generate the automatically generated descriptors. This makes it difficult to add new automatically generated descriptors when new ones are introduced. So instead of losing this information after the descriptors are created, have
CWallet
store it. The xpub will be stored in a newactivehdkey
field. The private key must be one of the keys that is used by the descriptors, and will be extracted upon loading.As this is a new field, wallets will be automatically upgraded upon loading. This loading is backwards compatible and uses a new non-required flag
WALLET_FLAG_GLOBAL_HD_KEY
to signal that the upgrade completed. The upgrade will search for an xpub that is shared bypkh()
,wpkh()
, andsh(wpkh()
descriptors with the derivation pattern that we use. For new wallets, the xpub will be set during descriptor creation rather than trying to reverse engineer it. The flag will be set for all wallets, regardless of whether such an xpub was found or can even exist, in order to indicate that the upgrade will not need to be run in the future.This allows us to have a
gethdkey
command which is useful for those who need a simple way to get an xpub from a wallet.gethdkey
can also take a boolean parameter to indicate whether it should also output the corresponding xprv.Supercedes #23417