-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet/rpc: fix listdescriptors RPC fails to return descriptors with private key information when wallet contains descriptors missing any key #32471
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
base: master
Are you sure you want to change the base?
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32471. 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. |
/Users/runner/work/bitcoin/bitcoin/src/test/fuzz/miniscript.cpp:1040:42: error: no matching member function for call to 'ToString'
std::optional<std::string> str{node->ToString(parser_ctx)};
~~~~~~^~~~~~~~
/Users/runner/work/bitcoin/bitcoin/src/script/miniscript.h:830:17: note: candidate function template not viable: requires 2 arguments, but 1 was provided
std::string ToString(const CTx& ctx, bool& has_priv_key) const {
^
/Users/runner/work/bitcoin/bitcoin/src/test/fuzz/miniscript.cpp:1248:31: error: no matching member function for call to 'ToString'
const auto str2 = parsed->ToString(parser_ctx);
~~~~~~~~^~~~~~~~
/Users/runner/work/bitcoin/bitcoin/src/script/miniscript.h:830:17: note: candidate function template not viable: requires 2 arguments, but 1 was provided
std::string ToString(const CTx& ctx, bool& has_priv_key) const {
^
2 errors generated. |
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
Putting draft while I fix some issues with the fuzz tests |
1ff2b00
to
4f4ca3a
Compare
4f4ca3a
to
bb14e8a
Compare
Concept NACK The purpose of the |
Concept NACK It's unclear why someone should receive public keys with |
I have not checked this PR yet but the original intention came from this issue: #32078 (comment) The idea was to show results for descriptors that don't have all the private keys but do have at least one. This is not for the case when the descriptor doesn't have any private key. I do like how the
Also, if the Regardless of the above point, I do feel that preventing
|
Maybe |
I think that's fine to show a result, but I don't think we should show any result if there are no private keys at all. |
I think there has been a misunderstanding. There are private keys to show. This PR doesn't change the behaviour of |
No result is shown if there are no private keys |
It is possible to have descriptors with missing private keys in a non-watchonly wallet. |
A similar problem happens in
It appears to be coming from here when the bitcoin/src/wallet/rpc/wallet.cpp Line 822 in 7763e86
It's due to mismatch in the count of |
37b1fdf
to
a26825c
Compare
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
a26825c
to
45e06e0
Compare
I split the original commit into the following commits: I'm not sure d2b84af makes the miniscript change in 49f4114 easier to follow. I still think they should be one commit, but let me know if it helps. |
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.
ACK 45e06e0
I find that the separation of 23431cd makes it easier to follow. The separation of d2b84af from 49f4114 is less useful, but fine by me.
@achow101 can you update your concept opinion based on the updated behaviour? I think it makes sense to return a result if any private key is present in descriptor, rather than all.
{ | ||
auto it = TEST_DATA.dummy_key_idx_map.find(key); | ||
if (it == TEST_DATA.dummy_key_idx_map.end()) return {}; | ||
if (it == TEST_DATA.dummy_key_idx_map.end()) { |
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.
In 49f4114 descriptor: ToPrivateString() pass if at least 1 priv key exists: maybe for good measure:
*has_priv_key = false;
In both places it seems better to check that priv_key
exists, even if that's always the case in this fuzzer. IIUC in that case you can leave out tmp{false}
and just pass nullptr
below.
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.
Done
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
src/script/descriptor.cpp
Outdated
@@ -841,6 +841,38 @@ class DescriptorImpl : public Descriptor | |||
return true; | |||
} | |||
|
|||
bool IsWatchOnly(const SigningProvider& arg) const override |
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.
In 6421321 "descriptors: add IsWatchOnly()"
I think this function can be vastly simplified by calling GetPrivKey
on all the PubkeyProvider
, and IsWatchOnly
on all subdescriptors.
nit: I don't like this function name since whether the descriptor is watchonly depends on the contents of the provided parameter. It's not a property like a Is*
name implies. Watchonly also is not a property applied to descriptors as private keys are not actually part of the descriptor. Perhaps something like HavePrivateKeys
instead.
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 was able to simplify the code as suggested, and I renamed it to HavePrivateKeys()
src/script/descriptor.cpp
Outdated
@@ -896,24 +896,27 @@ class DescriptorImpl : public Descriptor | |||
} | |||
|
|||
// NOLINTNEXTLINE(misc-no-recursion) | |||
virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const | |||
virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, bool* has_priv_key, const DescriptorCache* cache = nullptr) const |
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.
In 49f4114 "descriptor: ToPrivateString() pass if at least 1 priv key exists"
has_priv_key
should be a reference not a pointer. The pointer-ness is never used in this function.
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 has_priv_key
parameter has been removed.
src/script/descriptor.cpp
Outdated
@@ -922,7 +925,8 @@ class DescriptorImpl : public Descriptor | |||
if (!pubkey->ToNormalizedString(*arg, tmp, cache)) return false; | |||
break; | |||
case StringType::PRIVATE: | |||
if (!pubkey->ToPrivateString(*arg, tmp)) return false; | |||
assert(has_priv_key != nullptr); |
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.
In 49f4114 "descriptor: ToPrivateString() pass if at least 1 priv key exists"
If has_priv_key
is never supposed to be null, then it should be a reference. Otherwise, this should be an if
, we should not rely on caller behavior.
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.
Done.
src/script/descriptor.cpp
Outdated
bool has_priv_key{false}; | ||
// ToStringHelper should never fail for StringType::PRIVATE, | ||
// because it falls back to StringType::PUBLIC when no private key is available. | ||
assert(ToStringHelper(&arg, out, StringType::PRIVATE, &has_priv_key)); |
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.
In 49f4114 "descriptor: ToPrivateString() pass if at least 1 priv key exists"
assert
s should not have side effects. If we are calling a function to actually do something, it should not be in an assert
.
However, if the return value of ToStringHelper
is always supposed to be true
, then it should not be returning anything anyways. Furthermore, I don't see why ToStringHelper
doesn't return whether the string has a private key, as we are doing in this function. That would allow us to drop has_priv_key
from all of the function.
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 refactored the code as suggested and was able to drop has_priv_key
from the parameter list. Now, has_priv_key
is returned if type == StringType::PRIVATE
.
45e06e0
to
8244486
Compare
Previously, to determine if a desc is watchonly, `ToPrivateString()`, was used. It returns `false` if there is at least one pubkey in the descriptor for which the provider does not have a private key. ToPrivateString() behaviour will change in the following commits to only return `false` if no priv keys could be found for the pub keys in the descriptor. HavePrivateKeys() is added here to replace the use of ToPrivateString() for determining if a descriptor is 'watchonly'. Co-authored-by: rkrux <rkrux.connect@gmail.com>
ToPrivateString() behaviour will be modified in the following commits. This change ensures that wallet migration does not break.
This commit modifies the Pubkey providers to return the public string if private data is not available. This is setup for a future commit to make Descriptor::ToPrivateString return strings with missing private key information.
8244486
to
4241edf
Compare
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
Thanks for the reviews! I have:
|
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 miniscript code took some time for me to understand.
lgtm ACK 4241edf
* or if the provider does not have all private keys required by | ||
* the descriptor. | ||
*/ | ||
virtual bool HavePrivateKeys(const SigningProvider& provider) const = 0; |
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.
In 726181b "descriptors: add HavePrivateKeys()"
Naming nit because the function is a part of a singular Descriptor
class, but I see there is a combination of Have*
(not my preference) and Has*
functions in the singular object classes in the codebase, so feel free to ignore.
- virtual bool HavePrivateKeys(const SigningProvider& provider) const = 0;
+ virtual bool HasPrivateKeys(const SigningProvider& provider) const = 0;
- Refactor Descriptor::ToPrivateString() to allow descriptors with missing private keys to be printed. Useful in descriptors with multiple keys e.g tr() etc. - The existing behaviour of listdescriptors is preserved as much as possible, if no private keys are availablle ToPrivateString will return false
4241edf
to
46860de
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.
ACK 46860de
git range-diff 4241edf...46860de
TLDR:
Currently,
listdescriptors [private=true]
will fail for a non-watch-only wallet if any descriptor has a missing private key(e.gtr()
,multi()
, etc.). This PR changes that while making surelistdescriptors [private=true]
still fails if there no private keys. Closes #32078In non-watch-only wallets, it's possible to import descriptors as long as at least one private key is included. It's important that users can still view these descriptors when they need to create a backup—even if some private keys are missing (#32078 (comment)). This change makes it possible to do so.
This change also helps prevent
listdescriptors true
from failing completely, because one descriptor is missing some private keys.Notes
listdescriptors true
still fails for watch-only wallets to preserve existing behaviour Descriptor unit tests and simplifications #24361 (comment)Descriptor::ToPrivateString()
to determine which descriptor was watchonly. This means that modifying theToPrivateString()
behaviour caused descriptors that were previously recognized as "watchonly" to be "non-watchonly". In order to keep the scope of this PR limited to the RPC behaviour, this PR uses a different method to determinewatchonly
descriptors for the purpose of wallet migration. A follow-up PR can be opened to update migration logic to exclude descriptors with some private keys from thewatchonly
migration wallet.Relevant PRs
#24361
#32186
Testing
Functional tests were added to test the new behaviour
EDIT
listdescriptors [private=true]
will still fail when there are no private keys because non-watchonly wallets must have private keys and callinglistdescriptors [private=true]
for watchonly wallet returns an error