-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: importdescriptors update existing #19651
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
Could we change |
I think we should go with option 2: To update the existing spkman. The keys should either already exist or are being imported at that time. I don't think it's good to add keys in memory and not write them to disk as this could potentially result in lost keys. |
fcf3da0
to
d8eb7b5
Compare
@elichai I believe it's better to process the command rather than just show better error message, because it allows to modify some meta data: for example descriptor's range. However I added a trivial commit to make error message less misleading.
@achow101 Indeed, this aligns with my intuition as well. I changed the implementation and it looks cleaner to me. Hope I got the update semantics correct for the |
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsNo conflicts as of last run. |
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 commit adding the test should be after or be the same commit as the one adding the fix. This way all commits pass tests.
It would be nice if the tests checked the update behavior (e.g. new ranges, changed active-ness, etc.) but we can do that in a followup.
d8eb7b5
to
16dfa98
Compare
@achow101 I squashed the test and the fix commits into one and extracted new function Please note, descriptor deactivation is not working right now so there is no test for it. I'm currently working on it and will do as a separate PR. |
7b603d9
to
51ea4c6
Compare
51ea4c6
to
f06b47e
Compare
Rebased and removed 51ea4c6 as it was already added to master |
auto spk_man = m_spk_managers.at(id).get(); | ||
spk_man->SetInternal(internal); | ||
spk_mans[type] = spk_man; | ||
|
||
if (spk_mans_other[type] == spk_man) { | ||
spk_mans_other[type] = 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.
Why? I don't think it is necessarily wrong for a ScriptPubKeyMan
to serve both internal and external addresses. We do this in the legacy wallet. Also, it seems like this should break the legacy 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.
Two reasons:
-
If we don't do that, than semantics of
importdescriptors
changes. What I'm trying to achieve with this pull request is to make the command idempotent and allow for updates. In case when we don't remove specified SPK from previous map in the wallet (m_internal_spk_managers
/m_external_spk_managers
) than it's not really possible for a user to changeinternal
flag. When she would try to do so, we would just register same descriptor in the second map. And it's not what I would expect. -
This is required to keep consistency when we move descriptor from internal to external and vice versa.
DescriptorScriptPubKeyMan
hasm_internal
flag. It begs for some bugs if this flag is inconsistent with the descriptor placements in the wallet. For example it will affectsKeypoolCountExternalKeys()
and as a consequence the reporting of internal/external key pool sizes.
While this indeed goes against how legacy wallet operates it looks like it doesn't break it. The reason being is that legacy wallet is configured by SetupLegacyScriptPubKeyMan
and don't use LoadActiveScriptPubKeyMan
. I tried to verify by using a wallet from 0.16.3 and calling getnewaddress
and getrawchagneaddress
and both worked returning a key from the same hdseedid
. Anyway, let me think how I can improve it. I'm also open from your suggestions.
P.S. I also looked at the possibility of removing m_internal
but that doesn't seems possible due to the public interface of SPK. My motivation was to completely illuminate possible inconsistency between wallet and SPK in terms of internal
flag. Do you think this is an idea worth exploring?
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.
While legacy wallets do use SetupLegacyScriptPubKeyMan
, they also have externalspk
and internalspk
entries which are handled by LoadActiveScriptPubKeyMan
. So it should break legacy wallets, and I'm not sure why it doesn't.
I would prefer that the changing of the activeness and internalness is handled by another function which should also have a comment about why it should remain separate from LoadActiveScriptPubKeyMan
. As it is now, I think it makes the loading code more fragile and difficult to understand.
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 analyzed the code and couldn't find any uses of LoadActiveScriptPubKeyMan
for legacy wallets. The function is only called when 1) we create new wallet, 2) call importdescriptors
command or 3) when loading a wallet from disk. But if I'm not mistaken there is no active SPKs on disk for legacy wallets. That makes me believe this function is only relevant for descriptor wallets. So I put a guard at the top to throw an exception when we call it for legacy wallet. And all wallet functional tests pass on my laptop for now. I'm happy to reproduce any scenario in which this code should break legacy 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.
Hmm, SetupLegacyScriptPubKeyMan
adds the LegacySPKM
to the maps that track internal/external, but doesn't write them to disk. I thought it wrote them to disk and now I'm wondering whether not doing so is a bug. But since it doesn't, that's why this works.
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.
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 covered in the test section under "Check we can change descriptor internal flag"
f06b47e
to
39e1c5a
Compare
Added |
@achow101 thanks for your time and review. I pushed changes as fixup commits for easier incremental review, when you believe the code is ready to be merged I'll squash them into original commits. |
ACK, please squash. |
2bc7a7c
to
908e18e
Compare
This should also fix #21716 |
re-ACK 5d96704 |
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 + light code review, I'll finish reviewing soon
m_wallet_descriptor = descriptor; | ||
} | ||
|
||
bool DescriptorScriptPubKeyMan::CanUpdateToWalletDescriptor(const WalletDescriptor& descriptor, std::string& error) |
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 feel like the name of this function is confusing and could be improved.
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've spent some time, but couldn't come up with anything better. Any suggestions?
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 think a start could be to drop To
from the name, so that it matches UpdateWalletDescriptor
<-> CanUpdateWalletDescriptor
I'll try to review this later today. |
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/Approach ACK 5d96704 reviewed, debug-built and ran unit tests and wallet_importdescriptors.py on each commit. I don't know this part of the codebase well; the code and behavioral changes seem ok but my concern is if there is test coverage of all the changes in behavior.
auto spk_man = m_spk_managers.at(id).get(); | ||
spk_man->SetInternal(internal); | ||
spk_mans[type] = spk_man; | ||
|
||
if (spk_mans_other[type] == spk_man) { | ||
spk_mans_other[type] = 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.
WalletLogPrintf("Deactivate spkMan: id = %s, type = %d, internal = %d\n", id.ToString(), static_cast<int>(type), static_cast<int>(internal)); | ||
WalletBatch batch(GetDatabase()); | ||
if (!batch.EraseActiveScriptPubKeyMan(static_cast<uint8_t>(type), internal)) { | ||
throw std::runtime_error(std::string(__func__) + ": erasing active ScriptPubKeyMan id failed"); |
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.
5d96704 test coverage?
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.
Idk how to cover this. One need to introduce a low-level database error to hit this line. I'm not sure it's worth it
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
My comments are not that important but I agree with @meshcollider that CanUpdateToWalletDescriptor
could be named better and with @jonatack that test coverage should be improved.
5d96704
to
d44c9e6
Compare
d44c9e6
to
3efaf83
Compare
re-ACK 3efaf83 |
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.
Code review ACK 3efaf83
m_wallet_descriptor = descriptor; | ||
} | ||
|
||
bool DescriptorScriptPubKeyMan::CanUpdateToWalletDescriptor(const WalletDescriptor& descriptor, std::string& error) |
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 think a start could be to drop To
from the name, so that it matches UpdateWalletDescriptor
<-> CanUpdateWalletDescriptor
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.
Light ACK 3efaf83 per git range-diff a000cb0 5d96704 3efaf83
and as a sanity check, re-debug-built on debian with gcc 10.2.1 and clang 11, ran wallet_importdescriptors.py
Some touch-ups if you update.
self.test_importdesc({**range_request, "range": [0, 10]}, wallet=wpriv, success=False, | ||
error_code=-8, error_message='new range must include current range = [0,20]') | ||
self.test_importdesc({**range_request, "range": [5, 20]}, wallet=wpriv, success=False, | ||
error_code=-8, error_message='new range must include current range = [0,20]') |
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.
6737d96 suggestion, separate the salient from the redundant
- self.test_importdesc({**range_request, "range": [5, 10]}, wallet=wpriv, success=False,
- error_code=-8, error_message='new range must include current range = [0,20]')
- self.test_importdesc({**range_request, "range": [0, 10]}, wallet=wpriv, success=False,
- error_code=-8, error_message='new range must include current range = [0,20]')
- self.test_importdesc({**range_request, "range": [5, 20]}, wallet=wpriv, success=False,
- error_code=-8, error_message='new range must include current range = [0,20]')
+ for r in [[5, 10], [0, 10], [5, 20]]:
+ self.test_importdesc(req={**range_request, "range": r}, wallet=wpriv, success=False, error_code=-8, error_message="new range must include current range = [0,20]")
success=True) | ||
import_request = {"desc": descsum_create("pkh(" + key.pubkey + ")"), | ||
"timestamp": "now", | ||
"label": "Descriptor import test"} |
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.
bf68ebc formatting nit
import_request = {"desc": descsum_create("pkh(" + key.pubkey + ")"),
- "timestamp": "now",
- "label": "Descriptor import test"}
+ "timestamp": "now",
+ "label": "Descriptor import test"}
or
- import_request = {"desc": descsum_create("pkh(" + key.pubkey + ")"),
- "timestamp": "now",
- "label": "Descriptor import test"}
+ import_request = {
+ "desc": descsum_create("pkh(" + key.pubkey + ")"),
+ "timestamp": "now",
+ "label": "Descriptor import test",
+ }
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21) | ||
# Can keep range the same | ||
self.test_importdesc({**range_request, "range": [0, 20]}, wallet=wpriv, success=True) | ||
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21) |
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.
6737d96 readability nit
self.log.info("Verify we can only extend descriptor's range")
range_request = {"desc": descsum_create(desc), "timestamp": "now", "range": [5, 10], 'active': True}
+
self.test_importdesc(range_request, wallet=wpriv, success=True)
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 6)
+
self.test_importdesc({**range_request, "range": [0, 10]}, wallet=wpriv, success=True)
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 11)
+
self.test_importdesc({**range_request, "range": [0, 20]}, wallet=wpriv, success=True)
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21)
+
# Can keep range the same
self.test_importdesc({**range_request, "range": [0, 20]}, wallet=wpriv, success=True)
assert_equal(wpriv.getwalletinfo()['keypoolsize'], 21)
3efaf83 wallet: deactivate descriptor (S3RK) 6737d96 test: wallet importdescriptors update existing (S3RK) 586f1d5 wallet: maintain SPK consistency on internal flag change (S3RK) f1b7db1 wallet: don't mute exceptions in importdescriptors (S3RK) bf68ebc wallet: allow to import same descriptor twice (S3RK) Pull request description: Rationale: allow updating existing descriptors with `importdescriptors` command. Currently if you run same `importdescriptors` command twice with a descriptor containing private key you will get very confusing error — `Missing required fields`. What happens is that Wallet tries to write imported private key to the disk, but it exists already so we get `DB_KEYEXIST (-30995)` from BerkelyDB. Please note, that we set `DB_NOOVERWRITE` (I guess not to lose some keys accidentally). The exception is caught in `catch (...)` in rpcdump.cpp with a generic error. With this PR if a descriptor is already present than we will update its activeness, internalness, label, range and next_index. For the range only expansion is allowed (range start can only decrease, range end increase). ACKs for top commit: achow101: re-ACK 3efaf83 meshcollider: Code review ACK 3efaf83 jonatack: Light ACK 3efaf83 per `git range-diff a000cb0 5d96704 3efaf83` and as a sanity check, re-debug-built on debian with gcc 10.2.1 and clang 11, ran wallet_importdescriptors.py Tree-SHA512: 122c4b621d64ec8a3b625f3aed9f01a2b5cbaf2029ad0325b5ff38d67fff5cd35324335fabe2dd5169548b01b267c81be6ae0f5c834342f3d5f6eeed515c4843
Summary: Rationale: allow updating existing descriptors with `importdescriptors` command. Currently if you run same `importdescriptors` command twice with a descriptor containing private key you will get very confusing error — `Missing required fields`. What happens is that Wallet tries to write imported private key to the disk, but it exists already so we get `DB_KEYEXIST (-30995)` from BerkelyDB. Please note, that we set `DB_NOOVERWRITE` (I guess not to lose some keys accidentally). The exception is caught in `catch (...)` in rpcdump.cpp with a generic error. With this PR if a descriptor is already present than we will update its activeness, internalness, label, range and next_index. For the range only expansion is allowed (range start can only decrease, range end increase). > wallet: allow to import same descriptor twice > wallet: don't mute exceptions in importdescriptors > wallet: maintain SPK consistency on internal flag change > test: wallet importdescriptors update existing > wallet: deactivate descriptor This is a backport of [[bitcoin/bitcoin#19651 | core#19651]] Test Plan: `ninja all check-all` ``` $ ecashaddress convert mpA2Wh9dvZT7yfELq1UnrUmAoc5qCkMetg --prefix ecregtest ecregtest:qp0v86h53rc92hjrlpwzpjtdlgzsxu25svv6g40fpl ``` Reviewers: #bitcoin_abc, Fabien Reviewed By: #bitcoin_abc, Fabien Subscribers: Fabien Differential Revision: https://reviews.bitcoinabc.org/D14207
Rationale: allow updating existing descriptors with
importdescriptors
command.Currently if you run same
importdescriptors
command twice with a descriptor containing private key you will get very confusing error —Missing required fields
. What happens is that Wallet tries to write imported private key to the disk, but it exists already so we getDB_KEYEXIST (-30995)
from BerkelyDB. Please note, that we setDB_NOOVERWRITE
(I guess not to lose some keys accidentally). The exception is caught incatch (...)
in rpcdump.cpp with a generic error.With this PR if a descriptor is already present than we will update its activeness, internalness, label, range and next_index.
For the range only expansion is allowed (range start can only decrease, range end increase).