wallet: fix crash during watch-only wallet migration #31374

pull furszy wants to merge 3 commits into bitcoin:master from furszy:2024_migration_watch-only_crash_fix changing 2 files +66 −2
  1. furszy commented at 4:41 pm on November 26, 2024: member

    The crash occurs because we assume the cached scripts structure will not be empty, but it can be empty for watch-only wallets that start blank.

    This also adds test coverage for standalone imported keys, which were also crashing because pubkey imports are treated the same way as hex script imports through importaddress().

    Testing Notes: This can be verified by cherry-picking and running any of the test commits on master. It will crash there but pass on this branch.

  2. DrahtBot commented at 4:41 pm on November 26, 2024: 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/31374.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK theStack, brunoerg, achow101

    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:

    • #31423 (wallet: migration, don’t create spendable wallet from a watch-only legacy wallet by furszy)
    • #31420 (test: implements helper functions for unit conversion by wfzyx)
    • #30328 (wallet: Remove IsMine from migration code 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 Nov 26, 2024
  4. furszy force-pushed on Nov 26, 2024
  5. DrahtBot added the label CI failed on Nov 26, 2024
  6. DrahtBot commented at 4:46 pm on November 26, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/33553306813

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly 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.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

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

  7. DrahtBot removed the label CI failed on Nov 26, 2024
  8. in test/functional/wallet_migration.py:1076 in bdd42ed8de outdated
    1071+        expected_descs = [pk_desc, pkh_desc, sh_wpkh_desc, wpkh_desc]
    1072+
    1073+        migrated_desc = []
    1074+        for desc_info in wo_wallet.listdescriptors()['descriptors']:
    1075+            desc = desc_info['desc']
    1076+            if pubkey.hex() in desc:
    


    brunoerg commented at 1:51 pm on December 3, 2024:

    In bdd42ed8de8a92ae3e69e7b2dfceae7330f32676: I think it can be simplified. See:

     0diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py
     1index 928ab4acb4..06bc4e1fbb 100755
     2--- a/test/functional/wallet_migration.py
     3+++ b/test/functional/wallet_migration.py
     4@@ -1070,12 +1070,7 @@ class WalletMigrationTest(BitcoinTestFramework):
     5         wpkh_desc = descsum_create(f'wpkh({pubkey_hex})')
     6         expected_descs = [pk_desc, pkh_desc, sh_wpkh_desc, wpkh_desc]
     7 
     8-        migrated_desc = []
     9-        for desc_info in wo_wallet.listdescriptors()['descriptors']:
    10-            desc = desc_info['desc']
    11-            if pubkey.hex() in desc:
    12-                migrated_desc.append(desc)
    13-
    14+        migrated_desc = [item['desc'] for item in wo_wallet.listdescriptors()['descriptors']]
    15         # Verify all expected descriptors were migrated
    16         assert_equal(expected_descs, migrated_desc)
    17         wallet.unloadwallet()
    

    I don’t see why we need to check if pubkey is into the descriptors, we expect all of them to have it.


    furszy commented at 2:35 pm on December 3, 2024:

    I don’t see why we need to check if pubkey is into the descriptors, we expect all of them to have it.

    Thanks. I just blindly re-used the code I applied for the first check. Suggestion applied, and also inlined the first check too.

  9. brunoerg approved
  10. brunoerg commented at 2:05 pm on December 3, 2024: contributor

    ACK bdd42ed8de8a92ae3e69e7b2dfceae7330f32676

    I verified that wallet_migration test fails on master. Also, I ran the spkm_migration fuzz target (from #29694) for over 24 hours and did not got any crash.

  11. furszy force-pushed on Dec 3, 2024
  12. furszy force-pushed on Dec 4, 2024
  13. in test/functional/wallet_migration.py:1022 in a65117610d outdated
    1013@@ -1012,6 +1014,18 @@ def check_comments():
    1014 
    1015         wallet.unloadwallet()
    1016 
    1017+    def test_migrate_simple_watch_only(self):
    1018+        self.log.info("Test migrating a watch-only p2pk script")
    1019+        wallet = self.create_legacy_wallet("bare_p2pk", blank=True)
    1020+        privkey = ECKey()
    1021+        privkey.generate()
    1022+        pubkey = privkey.get_pubkey()
    


    theStack commented at 10:56 pm on December 4, 2024:

    in a65117610de13f0de7db5f9dac380d047085d862: could use the generate_keypair helper already in this commit, so you don’t have to import ECKey (and get rid of .get_bytes() calls below):

    0        _, pubkey = generate_keypair()
    

    furszy commented at 2:40 am on December 5, 2024:
    yeah sure, applied. Thanks!
  14. in test/functional/wallet_migration.py:1093 in b63cd866a1 outdated
    1093@@ -1050,7 +1094,7 @@ def run_test(self):
    1094         self.test_preserve_tx_extra_info()
    1095         self.test_blank()
    1096         self.test_migrate_simple_watch_only()
    1097-
    1098+        self.test_manual_keys_import()
    


    theStack commented at 11:16 pm on December 4, 2024:
    nit: empty line lost here

    furszy commented at 2:40 am on December 5, 2024:
    fixed
  15. in test/functional/wallet_migration.py:1017 in a65117610d outdated
    1023+        p2pk_script = key_to_p2pk_script(pubkey.get_bytes())
    1024+        wallet.importaddress(address=p2pk_script.hex())
    1025+        res = wallet.migratewallet()
    1026+        wo_wallet = self.nodes[0].get_wallet_rpc(res['watchonly_name'])
    1027+        assert_equal(wo_wallet.listdescriptors()['descriptors'][0]['desc'], descsum_create(f'pk({pubkey.get_bytes().hex()})'))
    1028+        wallet.unloadwallet()
    


    theStack commented at 11:19 pm on December 4, 2024:
    nit: for sake of completeness, could also unload wo_wallet here (as done in the next manual keys import subtests)

    furszy commented at 2:40 am on December 5, 2024:
    sure, applied. Thanks
  16. theStack commented at 11:23 pm on December 4, 2024: contributor
    LGTM, will run the fuzz test from #29694 on top during the night and ACK tomorrow if there’s no crash. Only left some test nits below.
  17. furszy force-pushed on Dec 5, 2024
  18. furszy force-pushed on Dec 5, 2024
  19. DrahtBot added the label CI failed on Dec 5, 2024
  20. DrahtBot removed the label CI failed on Dec 5, 2024
  21. Tarhanshop approved
  22. achow101 commented at 7:42 pm on December 5, 2024: member
    ACK 8c68ba50eb97b4ffef9a359892f2f46a89453e9a
  23. DrahtBot requested review from theStack on Dec 5, 2024
  24. DrahtBot requested review from brunoerg on Dec 5, 2024
  25. brunoerg approved
  26. brunoerg commented at 9:01 pm on December 5, 2024: contributor
    ACK 8c68ba50eb97b4ffef9a359892f2f46a89453e9a
  27. theStack approved
  28. theStack commented at 10:37 pm on December 5, 2024: contributor
    ACK 8c68ba50eb97b4ffef9a359892f2f46a89453e9a
  29. wallet: fix crash during watch-only wallet migration
    The crash occurs because we assume the cached scripts
    structure will not be empty, but it can be empty when
    the legacy wallet contained only watch-only and
    solvable but not spendable scripts
    932cd1e92b
  30. furszy force-pushed on Dec 6, 2024
  31. furszy commented at 5:10 pm on December 6, 2024: member
    Rebased due to a silent conflict with the recently merged #31248. The tests now use the previous release node to create the wallet and the latest version node to migrate it. Ready to go.
  32. brunoerg approved
  33. brunoerg commented at 5:18 pm on December 6, 2024: contributor
    reACK 080e7ec110b0e40e56b9aad608cd144d75b971f7
  34. DrahtBot requested review from achow101 on Dec 6, 2024
  35. DrahtBot requested review from theStack on Dec 6, 2024
  36. in test/functional/wallet_migration.py:1032 in 080e7ec110 outdated
    1027+        privkey, pubkey = generate_keypair(wif=True)
    1028+        wallet.importprivkey(privkey=privkey, label="hi", rescan=False)
    1029+        wallet.unloadwallet()
    1030+
    1031+        # Now migrate wallet in the latest node
    1032+        res = self.master_node.migratewallet(self.old_node.wallets_path / "import_privkeys")
    


    achow101 commented at 7:06 pm on December 6, 2024:

    In 080e7ec110b0e40e56b9aad608cd144d75b971f7 “test: add coverage for migrating standalone imported keys”

    Use self.migrate_and_get_rpc


    furszy commented at 7:14 pm on December 6, 2024:
    Ups.. reimplemented the wheel.
  37. DrahtBot requested review from achow101 on Dec 6, 2024
  38. test: add coverage for migrating watch-only script 297a876c98
  39. test: add coverage for migrating standalone imported keys cdd207c0e4
  40. furszy force-pushed on Dec 6, 2024
  41. in test/functional/wallet_migration.py:1020 in 297a876c98 outdated
    1015+        wallet.importaddress(address=p2pk_script.hex())
    1016+        # Migrate wallet in the latest node
    1017+        res, _ = self.migrate_and_get_rpc("bare_p2pk")
    1018+        wo_wallet = self.master_node.get_wallet_rpc(res['watchonly_name'])
    1019+        assert_equal(wo_wallet.listdescriptors()['descriptors'][0]['desc'], descsum_create(f'pk({pubkey.hex()})'))
    1020+        wo_wallet.unloadwallet()
    


    theStack commented at 10:40 pm on December 8, 2024:
    nit: could still unload the migrated wallet here by using the second return value of migrate_and_get_rpc (but OTOH, the unloading is not done consistently in other sub-tests anyway, so maybe material for a different PR…)

    furszy commented at 2:54 pm on December 9, 2024:

    nit: could still unload the migrated wallet here by using the second return value of migrate_and_get_rpc (but OTOH, the unloading is not done consistently in other sub-tests anyway, so maybe material for a different PR…)

    Actually, I didn’t unload it on purpose. Aligned this PR to #31423 expected outcome. Short description: This test creates a blank legacy wallet, imports a watch-only P2PK script, and executes migration –> This scenario shouldn’t create a brand-new spendable wallet as it is doing right now. The pre-migration wallet is a watch-only wallet with no key material.

  42. in test/functional/wallet_migration.py:1027 in cdd207c0e4
    1019@@ -1019,6 +1020,50 @@ def test_migrate_simple_watch_only(self):
    1020         assert_equal(wo_wallet.listdescriptors()['descriptors'][0]['desc'], descsum_create(f'pk({pubkey.hex()})'))
    1021         wo_wallet.unloadwallet()
    1022 
    1023+    def test_manual_keys_import(self):
    1024+        self.log.info("Test migrating standalone private keys")
    1025+        wallet = self.create_legacy_wallet("import_privkeys", blank=True)
    1026+        privkey, pubkey = generate_keypair(wif=True)
    1027+        wallet.importprivkey(privkey=privkey, label="hi", rescan=False)
    


    theStack commented at 11:00 pm on December 8, 2024:

    nit, seems there is no need for a label here

    0        wallet.importprivkey(privkey=privkey, rescan=False)
    
  43. theStack approved
  44. theStack commented at 11:01 pm on December 8, 2024: contributor

    re-ACK cdd207c0e48081ab13e2c8c9886f3e2d5da1857e

    Left two nits (not worth to tackle if you don’t need to retouch for other reasons imho).

  45. DrahtBot requested review from brunoerg on Dec 8, 2024
  46. brunoerg approved
  47. brunoerg commented at 11:31 am on December 9, 2024: contributor
    reACK cdd207c0e48081ab13e2c8c9886f3e2d5da1857e
  48. in test/functional/wallet_migration.py:1039 in cdd207c0e4
    1034+        pubkey_hex = pubkey.hex()
    1035+        pk_desc = descsum_create(f'pk([{key_origin}]{pubkey_hex})')
    1036+        pkh_desc = descsum_create(f'pkh([{key_origin}]{pubkey_hex})')
    1037+        sh_wpkh_desc = descsum_create(f'sh(wpkh([{key_origin}]{pubkey_hex}))')
    1038+        wpkh_desc = descsum_create(f'wpkh([{key_origin}]{pubkey_hex})')
    1039+        expected_descs = [pk_desc, pkh_desc, sh_wpkh_desc, wpkh_desc]
    


    achow101 commented at 7:53 pm on December 9, 2024:

    It was surprising to me that the migrated wallet would make all of these individual descriptors when it could make a single comb(). Further investigation reveals that there is a minor bug in the handling of non-HD keys that would result in the separate descriptors rather than the combo().

    Semantically, these 4 descriptors are the same as combo(), so this would never have caused any funds loss, however it is unexpected and should be fixed in a followup.

  49. in test/functional/wallet_migration.py:1060 in cdd207c0e4
    1055+        # As we imported the pubkey only, there will be no key origin in the following descriptors
    1056+        pk_desc = descsum_create(f'pk({pubkey_hex})')
    1057+        pkh_desc = descsum_create(f'pkh({pubkey_hex})')
    1058+        sh_wpkh_desc = descsum_create(f'sh(wpkh({pubkey_hex}))')
    1059+        wpkh_desc = descsum_create(f'wpkh({pubkey_hex})')
    1060+        expected_descs = [pk_desc, pkh_desc, sh_wpkh_desc, wpkh_desc]
    


    achow101 commented at 7:53 pm on December 9, 2024:

    Similar to the above, however this is more expected since watchonly pubkeys are not being specially treated to produce combo() descriptors, but we should probably make combo() for them as well.

    Edit: On further thought, this appears to not be as possible since we can have watchonly pubkeys but not be watching all of the combo() output scripts for them.


    furszy commented at 3:23 am on December 11, 2024:

    Edit: On further thought, this appears to not be as possible since we can have watchonly pubkeys but not be watching all of the combo() output scripts for them.

    Not sure how this could be possible, but I think it would be better to add the combo and track more scripts instead of fewer. Importing a private key and a public key should behave the same at the “scan for X level”. The only difference should be that one lets you spend the script, while the other doesn’t.

    nvm, just checked the code. importpubkey does not save the key to disk. Only scripts created from the pubkey are stored.

  50. achow101 commented at 7:54 pm on December 9, 2024: member
    ACK cdd207c0e48081ab13e2c8c9886f3e2d5da1857e
  51. achow101 merged this on Dec 9, 2024
  52. achow101 closed this on Dec 9, 2024

  53. furszy deleted the branch on Dec 9, 2024
  54. achow101 referenced this in commit fdd82d9619 on Dec 9, 2024
  55. achow101 added the label Needs backport (28.x) on Dec 11, 2024
  56. achow101 referenced this in commit e2c508450c on Dec 11, 2024
  57. achow101 referenced this in commit f6bc1a7934 on Dec 11, 2024
  58. achow101 referenced this in commit 4a4ec8338f on Dec 11, 2024
  59. fanquake referenced this in commit d5ab5a47f0 on Dec 13, 2024
  60. fanquake removed the label Needs backport (28.x) on Dec 17, 2024
  61. fanquake commented at 11:16 am on December 17, 2024: member
    Removed the label as this isn’t being backported.

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: 2024-12-19 00:12 UTC

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