Skip to content

Conversation

S3RK
Copy link
Contributor

@S3RK S3RK commented Aug 3, 2020

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).

@fanquake fanquake added the Wallet label Aug 3, 2020
@elichai
Copy link
Contributor

elichai commented Aug 3, 2020

Could we change BerkeleyBatch::WriteKey to return an enum describing the error? that will be inconsistent with the current API though.
but it will allow us to print a good error for the user, as he should probably know he already have that descriptor

@achow101
Copy link
Member

achow101 commented Aug 3, 2020

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.

@S3RK S3RK force-pushed the importdescriptors_twice branch from fcf3da0 to d8eb7b5 Compare August 6, 2020 08:40
@S3RK
Copy link
Contributor Author

S3RK commented Aug 6, 2020

Could we change BerkeleyBatch::WriteKey to return an enum describing the error? that will be inconsistent with the current API though.
but it will allow us to print a good error for the user, as he should probably know he already have that descriptor

@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.

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.

@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 DescriptorScriptPubKeyMan. Could you please check?

@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 6, 2020

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

No conflicts as of last run.

Copy link
Member

@achow101 achow101 left a 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.

@S3RK
Copy link
Contributor Author

S3RK commented Aug 17, 2020

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.

@achow101 I squashed the test and the fix commits into one and extracted new function CanUpdateToWalletDescriptor to avoid code duplication. I also added some tests to verify various update scenarios: label change, range change, next_index, internal flag change, descriptor activation. A minor change were required in DescriptorScriptPubKeyMan::SetCache to make the tests pass.

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.

@S3RK S3RK force-pushed the importdescriptors_twice branch from 7b603d9 to 51ea4c6 Compare August 18, 2020 11:19
@S3RK S3RK force-pushed the importdescriptors_twice branch from 51ea4c6 to f06b47e Compare August 19, 2020 10:51
@S3RK
Copy link
Contributor Author

S3RK commented Aug 19, 2020

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;
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two reasons:

  1. 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 change internal 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.

  2. This is required to keep consistency when we move descriptor from internal to external and vice versa. DescriptorScriptPubKeyMan has m_internal flag. It begs for some bugs if this flag is inconsistent with the descriptor placements in the wallet. For example it will affects KeypoolCountExternalKeys() 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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cead7ed ISTM this change should have test coverage (more so given the discussion) to spec the behavior and provide regression coverage. Edit: Ah, if I understand correctly some test coverage is added in the next commit b66b26a.

Copy link
Contributor Author

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"

@S3RK S3RK force-pushed the importdescriptors_twice branch from f06b47e to 39e1c5a Compare August 27, 2020 11:06
@S3RK
Copy link
Contributor Author

S3RK commented Aug 27, 2020

Added wallet: deactivate descriptor commit from #19774

@S3RK S3RK changed the title wallet: allow import same descriptor twice wallet: importdescriptors update existing Aug 27, 2020
@S3RK
Copy link
Contributor Author

S3RK commented Aug 30, 2020

@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.

@achow101
Copy link
Member

achow101 commented Sep 3, 2020

ACK, please squash.

@S3RK S3RK force-pushed the importdescriptors_twice branch from 2bc7a7c to 908e18e Compare September 4, 2020 10:40
@S3RK
Copy link
Contributor Author

S3RK commented May 10, 2021

This should also fix #21716

@achow101
Copy link
Member

achow101 commented Jun 3, 2021

re-ACK 5d96704

@meshcollider meshcollider added this to the 22.0 milestone Jun 3, 2021
Copy link
Contributor

@meshcollider meshcollider left a 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)
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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

@jonatack
Copy link
Member

I'll try to review this later today.

Copy link
Member

@jonatack jonatack left a 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cead7ed ISTM this change should have test coverage (more so given the discussion) to spec the behavior and provide regression coverage. Edit: Ah, if I understand correctly some test coverage is added in the next commit b66b26a.

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5d96704 test coverage?

Copy link
Contributor Author

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

Copy link
Contributor

@fjahr fjahr left a 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.

@S3RK S3RK force-pushed the importdescriptors_twice branch from 5d96704 to d44c9e6 Compare June 28, 2021 19:38
@S3RK S3RK force-pushed the importdescriptors_twice branch from d44c9e6 to 3efaf83 Compare June 28, 2021 19:45
@achow101
Copy link
Member

re-ACK 3efaf83

Copy link
Contributor

@meshcollider meshcollider left a 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)
Copy link
Contributor

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

Copy link
Member

@jonatack jonatack left a 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]')
Copy link
Member

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"}
Copy link
Member

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)
Copy link
Member

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)

@fanquake fanquake requested review from Sjors and instagibbs June 30, 2021 01:34
@fanquake fanquake merged commit 045bb06 into bitcoin:master Jul 1, 2021
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Jul 1, 2021
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
gwillen pushed a commit to ElementsProject/elements that referenced this pull request Jun 1, 2022
Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Jul 5, 2023
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
@bitcoin bitcoin locked and limited conversation to collaborators Aug 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants