wallet: Automatically repair corrupted metadata with doubled derivation path #29124

pull achow101 wants to merge 2 commits into bitcoin:master from achow101:fix-double-keypath changing 3 files +134 −24
  1. achow101 commented at 8:47 pm on December 20, 2023: member

    A bug in 0.21.x and 22.x resulted in some wallets storing invalid derivation paths that are the concatenation of two derivation paths. This corruption follows a rigid pattern and can be easily detected and repaired.

    The bug that resulted in such wallets was fixed in #23304 but wallets that already ran into it would still have bad data stored. This PR should fix such wallets.

    Fixes #29109

  2. DrahtBot commented at 8:47 pm on December 20, 2023: contributor

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

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/29124.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK rkrux
    Stale ACK furszy

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #31250 (wallet: Disable creating and loading legacy wallets by achow101)

    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.

  3. DrahtBot added the label Wallet on Dec 20, 2023
  4. achow101 force-pushed on Dec 20, 2023
  5. DrahtBot added the label CI failed on Dec 20, 2023
  6. achow101 force-pushed on Dec 20, 2023
  7. achow101 force-pushed on Dec 21, 2023
  8. achow101 force-pushed on Dec 21, 2023
  9. DrahtBot removed the label CI failed on Dec 21, 2023
  10. DrahtBot added the label CI failed on Jan 15, 2024
  11. DrahtBot commented at 7:42 am on April 5, 2024: contributor
    Could rebase for fresh CI, if still relevant?
  12. achow101 force-pushed on Apr 5, 2024
  13. DrahtBot removed the label CI failed on Apr 5, 2024
  14. in src/wallet/walletdb.cpp:699 in 627216a174 outdated
    694         return DBErrors::LOAD_OK;
    695     });
    696     result = std::max(result, keymeta_res.m_result);
    697 
    698+    // Update any changed metadata
    699+    if (updated_metas.size()) {
    


    furszy commented at 2:40 pm on May 22, 2024:
    nit: !updated_metas.empty()

    achow101 commented at 11:54 pm on June 4, 2024:
    Done
  15. in src/wallet/walletdb.cpp:530 in 627216a174 outdated
    526@@ -527,7 +527,7 @@ static LoadResult LoadRecords(CWallet* pwallet, DatabaseBatch& batch, const std:
    527     return LoadRecords(pwallet, batch, key, prefix, load_func);
    528 }
    529 
    530-static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch, int last_client) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
    531+static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch, WalletBatch& w_batch, int last_client) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
    


    furszy commented at 2:46 pm on May 22, 2024:
    Non-blocking because it is the only way now but it is kinda unnatural to provide batch and w_batch when w_batch contains the batch object.

    achow101 commented at 11:55 pm on June 4, 2024:
    I’ve made m_batch public so that we can just pass the WalletBatch but still use the underlying batch object. However I’m not sure if that’s something we want to do in the long term, but I think it should be okay.

    furszy commented at 1:49 pm on June 5, 2024:

    I’ve made m_batch public so that we can just pass the WalletBatch but still use the underlying batch object. However I’m not sure if that’s something we want to do in the long term, but I think it should be okay.

    Yeah.. I’m not happy reading all the the batch.m_batch usages neither but np.

  16. in src/wallet/walletdb.cpp:704 in 627216a174 outdated
    699+    if (updated_metas.size()) {
    700+        LegacyScriptPubKeyMan* spkm = pwallet->GetLegacyScriptPubKeyMan();
    701+        Assert(spkm);
    702+        LOCK(spkm->cs_KeyStore);
    703+        for (const auto& pubkey : updated_metas) {
    704+            w_batch.WriteKeyMetadata(spkm->mapKeyMetadata.at(pubkey.GetID()), pubkey, true);
    


    furszy commented at 2:46 pm on May 22, 2024:
    nit: should check WriteKeyMetadata return value.

    achow101 commented at 11:55 pm on June 4, 2024:
    Done
  17. furszy commented at 2:54 pm on May 22, 2024: member
    Code review ACK 078c2d74c2779dd459e42daf67ab450fbf223506. Nothing blocking, just tiny nits.
  18. achow101 force-pushed on Jun 4, 2024
  19. DrahtBot added the label CI failed on Jun 5, 2024
  20. DrahtBot commented at 3:21 am on June 5, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    Leave a comment here, if you need help tracking down a confusing failure.

    Debug: https://github.com/bitcoin/bitcoin/runs/25814613888

  21. achow101 force-pushed on Jun 5, 2024
  22. furszy commented at 1:50 pm on June 5, 2024: member
    ACK 9db2d8b
  23. DrahtBot removed the label CI failed on Jun 6, 2024
  24. DrahtBot added the label Needs rebase on Jul 11, 2024
  25. achow101 force-pushed on Jul 22, 2024
  26. DrahtBot removed the label Needs rebase on Jul 22, 2024
  27. furszy commented at 1:42 pm on July 23, 2024: member

    ACK 1c3462e908994f0d63c1f39b5908f48d083dd10e

    Could minimize the first commit diff https://github.com/furszy/bitcoin-core/commit/145737e46e71859608334ab738817f7b323df367.

  28. DrahtBot added the label CI failed on Sep 6, 2024
  29. achow101 force-pushed on Sep 10, 2024
  30. DrahtBot removed the label CI failed on Sep 11, 2024
  31. achow101 requested review from murchandamus on Oct 15, 2024
  32. achow101 commented at 3:53 pm on October 15, 2024: member
  33. DrahtBot added the label CI failed on Oct 20, 2024
  34. DrahtBot added the label Needs rebase on Oct 24, 2024
  35. achow101 force-pushed on Oct 24, 2024
  36. DrahtBot removed the label Needs rebase on Oct 24, 2024
  37. DrahtBot removed the label CI failed on Oct 25, 2024
  38. in test/functional/wallet_backwards_compatibility.py:154 in e92dc1a3d3 outdated
    150+        with open(dump_path, encoding="utf8") as f:
    151+            for line in f:
    152+                if "hdkeypath=m/0'/0'/9'" in line:
    153+                    addr = line.split(" ")[4].split("=")[1]
    154+                elif " hdseed=1 " in line:
    155+                    seed = line.split(" ")[0]
    


    rkrux commented at 10:58 am on February 3, 2025:
  39. in src/wallet/walletdb.cpp:650 in 92f4ab6dd7 outdated
    647+                    // See https://github.com/bitcoin/bitcoin/issues/29109
    648+                    // Markers for bad derivation:
    649+                    // - 6 indexes
    650+                    // - path[5] == path[2] + 1
    651+                    // - path[0:2] == path[3:5]
    652+                    // - path[3:6] matches hdKeypath
    


    rkrux commented at 11:11 am on February 3, 2025:
    I had been wondering why such a path can’t occur besides this case, this comment in DeriveNewChildKey function answers it: https://github.com/bitcoin/bitcoin/blob/28.x/src/wallet/scriptpubkeyman.cpp#L1129
  40. in test/functional/wallet_backwards_compatibility.py:152 in e92dc1a3d3 outdated
    147+        wallet.dumpwallet(dump_path)
    148+        addr = None
    149+        seed = None
    150+        with open(dump_path, encoding="utf8") as f:
    151+            for line in f:
    152+                if "hdkeypath=m/0'/0'/9'" in line:
    


    rkrux commented at 11:37 am on February 3, 2025:

    I spent some time trying to understand why this one is picked here only to realise later that 9 is the last index at this chain because of keypool size set to 10 at the top of this test here: https://github.com/bitcoin/bitcoin/pull/29124/commits/e92dc1a3d36be02a8b16eed26159a43cfbe3dbcc#diff-b5055a65809b46e2775f6e28aa6eb8171e3fcc7a535cce5687a548b9ff233669R44

    This 9 and 10 can be tied to each other with a constant for better readability.


    rkrux commented at 11:38 am on February 3, 2025:
    Can this happen for internal keychain as well?

    rkrux commented at 8:58 am on February 4, 2025:
    Yes, it can as per this comment in the issue: #29109 (comment) The cpp code condition covers it as well though.

    achow101 commented at 8:01 pm on February 10, 2025:
    Added a constant.
  41. in test/functional/wallet_backwards_compatibility.py:169 in e92dc1a3d3 outdated
    164+        self.sync_all(nodes=[self.nodes[0], node_master, node_v22])
    165+        node_v22.loadwallet(wallet_name)
    166+
    167+        # Dump again to find bad hd keypath
    168+        bad_deriv_path = "m/0'/0'/9'/0'/0'/10'"
    169+        good_deriv_path = "m/0'/0'/10'"
    


    rkrux commented at 11:39 am on February 3, 2025:
    Similar tying to a constant here if possible.

    achow101 commented at 8:01 pm on February 10, 2025:
    Done
  42. in test/functional/wallet_backwards_compatibility.py:152 in e92dc1a3d3 outdated
    148+        addr = None
    149+        seed = None
    150+        with open(dump_path, encoding="utf8") as f:
    151+            for line in f:
    152+                if "hdkeypath=m/0'/0'/9'" in line:
    153+                    addr = line.split(" ")[4].split("=")[1]
    


    rkrux commented at 11:46 am on February 3, 2025:
  43. rkrux commented at 11:58 am on February 3, 2025: none
    Build and tests successful at e92dc1a3d36be02a8b16eed26159a43cfbe3dbcc Some initial comments, will give it another pass again.
  44. in test/functional/wallet_backwards_compatibility.py:185 in e92dc1a3d3 outdated
    180+        # Verify that this addr is actually at m/0'/0'/10' by making a new wallet with the same seed
    181+        node_v22.createwallet(wallet_name="path_verify", descriptors=False, blank=True)
    182+        verify_wallet = node_v22.get_wallet_rpc("path_verify")
    183+        verify_wallet.sethdseed(True, seed)
    184+        # Bad addr is after keypool, so need to generate it by refilling
    185+        verify_wallet.keypoolrefill(12)
    


    rkrux commented at 9:30 am on February 4, 2025:
    I’m realising that tying these constants up to the keypool size would make it easier to reason about this test.

    achow101 commented at 8:02 pm on February 10, 2025:
    Done
  45. in test/functional/wallet_backwards_compatibility.py:202 in e92dc1a3d3 outdated
    195+
    196+        # Check persistence
    197+        wallet_master.unloadwallet()
    198+        with node_master.assert_debug_log(expected_msgs=[], unexpected_msgs=["Repairing derivation path for"]):
    199+            node_master.loadwallet(wallet_name)
    200+        assert_equal(wallet_master.getaddressinfo(bad_path_addr)["hdkeypath"], good_deriv_path)
    


    rkrux commented at 9:33 am on February 4, 2025:
    Nice touch testing for persistence by loading it again!
  46. in test/functional/wallet_backwards_compatibility.py:196 in e92dc1a3d3 outdated
    189+        backup_path = node_v22.wallets_path / f"{wallet_name}.bak"
    190+        wallet.backupwallet(backup_path)
    191+        with node_master.assert_debug_log(expected_msgs=["Repairing derivation path for"]):
    192+            node_master.restorewallet(wallet_name, backup_path)
    193+        wallet_master = node_master.get_wallet_rpc(wallet_name)
    194+        assert_equal(wallet_master.getaddressinfo(bad_path_addr)["hdkeypath"], good_deriv_path)
    


    rkrux commented at 10:45 am on February 4, 2025:
    0'pubkey': '029edc63efe154f7f0da891f0def969b65cc4706893f8277580093eccbd09cb11d', 'ischange': True, 'timestamp': 1738665397, 'hdkeypath': "m/0'/0'/10'",
    

    I found it a little weird that an external keychain address is categorised as change. I am assuming that internal keychain addresses are for changes.


    achow101 commented at 7:19 pm on February 10, 2025:
    ischange does not actually look at the derivation path. Instead it looks at some heuristics which result in derived-but-not-yet-requested external addresses being identified as change.
  47. in src/wallet/walletdb.h:307 in 92f4ab6dd7 outdated
    301@@ -302,8 +302,8 @@ class WalletBatch
    302     //! Registers db txn callback functions
    303     void RegisterTxnListener(const DbTxnListener& l);
    304 
    305-private:
    306     std::unique_ptr<DatabaseBatch> m_batch;
    307+private:
    


    rkrux commented at 10:50 am on February 4, 2025:
    Nit: This change and the corresponding function call updates can be the first commit followed by the actual repairing. But I can understand if it’s a bit much for this PR given its limited focus area.

    achow101 commented at 7:20 pm on February 10, 2025:
    Going to leave this as is.
  48. rkrux approved
  49. rkrux commented at 10:53 am on February 4, 2025: none

    tACK e92dc1a3d36be02a8b16eed26159a43cfbe3dbcc

    Thanks for this PR, I enjoyed reviewing it. I ran the tests and debugged the compatibility test.

    Can / Will this also be back-ported to the affected version or is the affected version too far back to be back-ported into?

  50. DrahtBot requested review from furszy on Feb 4, 2025
  51. achow101 force-pushed on Feb 10, 2025
  52. achow101 force-pushed on Feb 10, 2025
  53. walletdb: Repair corrupted doubled derivation paths
    A bug in 0.21.x and 22.x resulted in some wallets having invalid
    derivation paths that are the concatenation of two derivation paths.
    This corruption follows a rigid pattern and can be easily detected and
    repaired.
    2eaa7e2de6
  54. tests: Test that bad inactive chain derivation path is cleaned up 78cb0df4d5
  55. achow101 force-pushed on Feb 10, 2025
  56. achow101 commented at 10:48 pm on February 10, 2025: member
    Updated this to work even after the legacy wallet is deleted.
  57. in test/functional/wallet_backwards_compatibility.py:44 in 78cb0df4d5
    40@@ -41,7 +41,7 @@ def set_test_params(self):
    41             ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v25.0
    42             ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v24.0.1
    43             ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v23.0
    44-            ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v22.0
    45+            ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1", "-keypool=10"], # v22.0
    


    rkrux commented at 11:26 am on February 12, 2025:
    I had in mind tying this 10 to the constant as well. Consider if you end up retouching, otherwise it’s fine.
  58. in test/functional/wallet_backwards_compatibility.py:184 in 78cb0df4d5
    179+        # Verify that this addr is actually at m/0'/0'/10' by making a new wallet with the same seed
    180+        node_v22.createwallet(wallet_name="path_verify", descriptors=False, blank=True)
    181+        verify_wallet = node_v22.get_wallet_rpc("path_verify")
    182+        verify_wallet.sethdseed(True, seed)
    183+        # Bad addr is after keypool, so need to generate it by refilling
    184+        verify_wallet.keypoolrefill(LAST_KEYPOOL_INDEX + 2)
    


    rkrux commented at 8:30 am on February 13, 2025:
    This was using 12 earlier, now 11. Fine since the test passes.
  59. rkrux commented at 8:52 am on February 13, 2025: none

    reACK 78cb0df4d5b9cc30dbe60a6cf24ab6cac5d3f421

    I reviewed the range diff.

    0git range-diff e92dc1a3d36be02a8b16eed26159a43cfbe3dbcc...78cb0df4d5b9cc30dbe60a6cf24ab6cac5d3f421
    

    Newer changes are related to using the constant LAST_KEYPOOL_INDEX as suggested and use migratewallet instead of restorewallet due to which the format of good_deriv_path is changed from ' to ‘h’. Using migratewallet is fine because it also ends up calling LoadLegacyWalletRecords internally where the bug fix lies, and additionally it also ensures the fix works for the newer wallets.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-02-23 00:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me