descriptors: Disallow hybrid and uncompressed keys when inferring #28602

pull achow101 wants to merge 4 commits into bitcoin:master from achow101:migrate-hybrid-keys changing 3 files +157 −16
  1. achow101 commented at 6:01 PM on October 5, 2023: member

    InferDescriptor was not always checking that the pubkey it was placing into the descriptor was an allowed pubkey. For example, given a P2WPKH script that uses an uncompressed pubkey, it would produce a wpkh() with the uncompressed key. Additionally, the hybrid key check was only being done for pk() scripts, where it should've been done for all scripts.

    This PR moves the key checking into InferPubkey. If the key is not valid for the context, then nullptr is returned and the inferring will fall through to the defaults of either raw() or addr().

    This also resolves an issue with migrating legacy wallets that contain hybrid pubkeys as such watchonly scripts will become raw() or addr() and go to the watchonly wallet. Note that a legacy wallet cannot sign for hybrid pubkeys. A test has been added for the migration case.

    Also added unit tests for InferDescriptor itself as the edge cases with that function are not covered by the descriptor roundtrip test.

  2. DrahtBot commented at 6:01 PM on October 5, 2023: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage

    For detailed information about the code coverage, see the test coverage report.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK furszy, Sjors
    Stale ACK sipa

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #28610 (wallet: Migrate entire address book entries to watchonly and solvables too by achow101)
    • #28609 (wallet: Reload watchonly and solvables wallets after migration by achow101)
    • #22838 (descriptors: Be able to specify change and receiving in a single descriptor string 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. achow101 added this to the milestone 26.0 on Oct 5, 2023
  4. DrahtBot added the label Descriptors on Oct 5, 2023
  5. fanquake requested review from darosior on Oct 5, 2023
  6. fanquake requested review from sipa on Oct 5, 2023
  7. in src/script/descriptor.cpp:1465 in 6c80e82b76 outdated
    1461 | @@ -1454,8 +1462,11 @@ struct KeyParser {
    1462 |          CPubKey pubkey(begin, end);
    1463 |          if (pubkey.IsValidNonHybrid()) {
    1464 |              Key key = m_keys.size();
    1465 | -            m_keys.push_back(InferPubkey(pubkey, ParseScriptContext::P2WSH, *m_in));
    1466 | -            return key;
    1467 | +            std::unique_ptr<PubkeyProvider> pubkey_provider = InferPubkey(pubkey, ParseScriptContext::P2WSH, *m_in);
    


    sipa commented at 7:34 PM on October 5, 2023:

    More concisely:

    if (auto pubkey_provider = InferPubkey(pubkey, ParseScriptContext::P2WSH, *m_in)) {
        m_keys.push_back(std::move(pubkey_provider));
        return key;
    }
    

    achow101 commented at 7:55 PM on October 5, 2023:

    Used the first suggestion. The second does not compile, pubkey_provider is out of scope.

  8. in src/test/descriptor_tests.cpp:394 in 6c80e82b76 outdated
     389 | +    std::vector<unsigned char> script_bytes{ParseHex(script_hex)};
     390 | +    const CScript& script{script_bytes.begin(), script_bytes.end()};
     391 | +
     392 | +    FlatSigningProvider provider;
     393 | +    for (const std::string& prov_script_hex : hex_scripts) {
     394 | +    std::vector<unsigned char> prov_script_bytes{ParseHex(prov_script_hex)};
    


    sipa commented at 7:52 PM on October 5, 2023:

    Indentation?


    achow101 commented at 7:55 PM on October 5, 2023:

    Fixed

  9. achow101 force-pushed on Oct 5, 2023
  10. DrahtBot added the label CI failed on Oct 5, 2023
  11. in src/script/descriptor.cpp:1841 in 25a8a173ad outdated
    1841 | +        bool ok = true;
    1842 |          std::vector<std::unique_ptr<PubkeyProvider>> providers;
    1843 |          for (size_t i = 1; i + 1 < data.size(); ++i) {
    1844 |              CPubKey pubkey(data[i]);
    1845 | -            providers.push_back(InferPubkey(pubkey, ctx, provider));
    1846 | +            if(auto pubkey_provider = InferPubkey(pubkey, ctx, provider)) {
    


    sipa commented at 7:59 PM on October 5, 2023:

    Nit: space after if


    achow101 commented at 10:36 PM on October 5, 2023:

    Done

  12. in src/test/descriptor_tests.cpp:405 in 25a8a173ad outdated
     400 | +        provider.pubkeys.emplace(prov_pubkey.GetID(), prov_pubkey);
     401 | +    }
     402 | +    for (const auto& [pubkey_hex, origin_str] : str_origins) {
     403 | +        CPubKey origin_pubkey{ParseHex(pubkey_hex)};
     404 | +
     405 | +        using namespace spanparsing;
    


    sipa commented at 8:10 PM on October 5, 2023:

    The body of this loop looks like it's duplicating code from existing key parsing code.

    Would it be possible to e.g. construct a [origin]pubkey string, pass that to ParsePubkey, and use the resulting FlatSigningProvider& out?


    achow101 commented at 10:37 PM on October 5, 2023:

    Not without a lot of refactoring that ends up exposing several descriptor parsing internals.

  13. in src/script/descriptor.cpp:1509 in 3e718b49e2 outdated
    1453 | @@ -1454,8 +1454,10 @@ struct KeyParser {
    1454 |          CPubKey pubkey(begin, end);
    1455 |          if (pubkey.IsValidNonHybrid()) {
    1456 |              Key key = m_keys.size();
    1457 | -            m_keys.push_back(InferPubkey(pubkey, ParseScriptContext::P2WSH, *m_in));
    1458 | -            return key;
    1459 | +            if (auto pubkey_provider = InferPubkey(pubkey, ParseScriptContext::P2WSH, *m_in)) {
    1460 | +                m_keys.push_back(std::move(pubkey_provider));
    


    furszy commented at 10:16 PM on October 5, 2023:

    tiny nit: Key key = m_keys.size(); can be inside the InferPubkey block.

    Same for FromPKHBytes().


    achow101 commented at 10:46 PM on October 6, 2023:

    Done

  14. in test/functional/wallet_migration.py:800 in 12f0d05775 outdated
     795 | +        p2pkh_addr = key_to_p2pkh(hybrid_pubkey)
     796 | +        p2pkh_addr_info = wallet.getaddressinfo(p2pkh_addr)
     797 | +        assert_equal(p2pkh_addr_info["iswatchonly"], True)
     798 | +        assert_equal(p2pkh_addr_info["ismine"], False) # Things involving hybrid pubkeys are not spendable
     799 | +
     800 | +        # Also import the p2pkh for the pubkey to make sure we don't migrate it
    


    furszy commented at 10:35 PM on October 5, 2023:

    s/p2pkh/p2wpkh


    achow101 commented at 10:46 PM on October 6, 2023:

    Done

  15. achow101 force-pushed on Oct 5, 2023
  16. DrahtBot removed the label CI failed on Oct 6, 2023
  17. in src/test/descriptor_tests.cpp:653 in 6f00af4bcb outdated
     629 | @@ -582,6 +630,11 @@ BOOST_AUTO_TEST_CASE(descriptor_test)
     630 |      // Same for hash256
     631 |      Check("wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE_FAILS, {{"0020cf62bf97baf977aec69cbc290c372899f913337a9093e8f066ab59b8657a365c"}}, OutputType::BECH32, /*op_desc_id=*/uint256S("8412ba3ac20ba3a30f81442d10d32e0468fa52814960d04e959bf84a9b813b88"), {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {});
     632 |      Check("wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", "wsh(and_v(v:hash256(ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588),pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)))", SIGNABLE, {{"0020cf62bf97baf977aec69cbc290c372899f913337a9093e8f066ab59b8657a365c"}}, OutputType::BECH32, /*op_desc_id=*/uint256S("8412ba3ac20ba3a30f81442d10d32e0468fa52814960d04e959bf84a9b813b88"), {{}}, /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, {{ParseHex("ae253ca2a54debcac7ecf414f6734f48c56421a08bb59182ff9f39a6fffdb588"), ParseHex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")}});
     633 | +
     634 | +    CheckInferDescriptor("a9141a31ad23bf49c247dd531a623c2ef57da3c400c587", "sh(pkh([deadbeef/0h/0h/0]03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", {"76a9149a1c78a507689f6f54b847ad1cef1e614ee23f1e88ac"}, {{"03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", "deadbeef/0h/0h/0"}});
     635 | +    CheckInferDescriptor("41069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4ac", "raw(41069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4ac)", {}, {{"069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4", ""}});
     636 | +    CheckInferDescriptor("76a91445ff7c2327866472639d507334a9a00119dfd32688ac", "addr(17P7ge56F2QcdHxxRBa2NyzmejFggPwBJ9)", {}, {{"069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4", ""}});
     637 | +    CheckInferDescriptor("001422e363a523947a110d9a9eb114820de183aca313", "addr(bc1qyt3k8ffrj3apzrv6n6c3fqsduxp6egcnk2r66j)", {}, {{"049228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4", ""}});
    


    furszy commented at 7:20 PM on October 6, 2023:

    For better maintainability, comments explaining what we are about to test on each case would be nice. Maybe:

    // Infer valid sh(pkh(compressed_key))
    CheckInferDescriptor("a9141a31ad23bf49c247dd531a623c2ef57da3c400c587", "sh(pkh([deadbeef/0h/0h/0]03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", {"76a9149a1c78a507689f6f54b847ad1cef1e614ee23f1e88ac"}, {{"03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd", "deadbeef/0h/0h/0"}});
    // Only can watch an hybrid pk in a P2PK
    CheckInferDescriptor("41069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4ac", "raw(41069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4ac)", {}, {{"069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4", ""}});
    // Only can watch an hybrid pk in a P2PKH
    CheckInferDescriptor("76a91445ff7c2327866472639d507334a9a00119dfd32688ac", "addr(17P7ge56F2QcdHxxRBa2NyzmejFggPwBJ9)", {}, {{"069228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4", ""}});
    // Only can watch an uncompressed pk in a P2WPKH
    CheckInferDescriptor("001422e363a523947a110d9a9eb114820de183aca313", "addr(bc1qyt3k8ffrj3apzrv6n6c3fqsduxp6egcnk2r66j)", {}, {{"049228de6902abb4f541791f6d7f925b10e2078ccb1298856e5ea5cc5fd667f930eac37a00cc07f9a91ef3c2d17bf7a17db04552ff90ac312a5b8b4caca6c97aa4", ""}});
    

    furszy commented at 7:56 PM on October 6, 2023:

    Could also verify the inference process for pkh(uncompressed) and pk(uncompressed):

    // Infer valid pkh(uncompressed_key)
    CheckInferDescriptor("76a914a31725c74421fadc50d35520ab8751ed120af80588ac", "pkh(04c56fe4a92d401bcbf1b3dfbe4ac3dac5602ca155a3681497f02c1b9a733b92d704e2da6ec4162e4846af9236ef4171069ac8b7f8234a8405b6cadd96f34f5a31)", {}, {{"04c56fe4a92d401bcbf1b3dfbe4ac3dac5602ca155a3681497f02c1b9a733b92d704e2da6ec4162e4846af9236ef4171069ac8b7f8234a8405b6cadd96f34f5a31", ""}});
    // Infer valid pk(uncompressed_key)
    CheckInferDescriptor("4104032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220ac", "pk(04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220)#59dvf0e2", {}, {{"04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220", ""}});
    

    achow101 commented at 10:46 PM on October 6, 2023:

    Added some comments.


    achow101 commented at 10:46 PM on October 6, 2023:

    Done

  18. furszy commented at 7:57 PM on October 6, 2023: member

    Code review ACK 6f00af4bc, left few nits, nothing blocking.

  19. achow101 force-pushed on Oct 6, 2023
  20. achow101 force-pushed on Oct 6, 2023
  21. DrahtBot added the label Needs rebase on Oct 8, 2023
  22. achow101 force-pushed on Oct 8, 2023
  23. sipa commented at 6:34 PM on October 8, 2023: member

    utACK a49081f6cea603d354beef1edff480d4b034dafd

  24. DrahtBot requested review from furszy on Oct 8, 2023
  25. DrahtBot removed the label Needs rebase on Oct 8, 2023
  26. in src/script/descriptor.cpp:1503 in fb3c5f3e9f outdated
    1490 | @@ -1491,13 +1491,17 @@ struct KeyParser {
    1491 |          if (miniscript::IsTapscript(m_script_ctx) && end - begin == 32) {
    1492 |              XOnlyPubKey pubkey;
    1493 |              std::copy(begin, end, pubkey.begin());
    1494 | -            m_keys.push_back(InferPubkey(pubkey.GetEvenCorrespondingCPubKey(), ParseContext(), *m_in));
    1495 | +            if (auto pubkey_provider = InferPubkey(pubkey.GetEvenCorrespondingCPubKey(), ParseContext(), *m_in)) {
    1496 | +                m_keys.push_back(std::move(pubkey_provider));
    


    Sjors commented at 7:09 AM on October 9, 2023:

    fb3c5f3e9fb17e2313a6f2aec6db49f3e0da8614 why is return key outside the if context here, but in the below check it's inside?

    Wouldn't that cause it to return 0 instead of {}?


    achow101 commented at 6:07 PM on October 9, 2023:

    Fixed

  27. in src/script/descriptor.cpp:1883 in 62ca75bd99 outdated
    1879 | @@ -1876,7 +1880,7 @@ std::unique_ptr<DescriptorImpl> InferScript(const CScript& script, ParseScriptCo
    1880 |          CKeyID keyid(hash);
    1881 |          CPubKey pubkey;
    1882 |          if (provider.GetPubKey(keyid, pubkey)) {
    1883 | -            if (auto pubkey_provider = InferPubkey(pubkey, ctx, provider)) {
    1884 | +            if (auto pubkey_provider = InferPubkey(pubkey, ParseScriptContext::P2WPKH, provider)) {
    


    Sjors commented at 7:20 AM on October 9, 2023:

    62ca75bd99b42f686ed1879481b856c882b40025: what's wrong with passing ctx here?


    achow101 commented at 6:09 PM on October 9, 2023:

    The context for the pubkey inferring needs to be P2WPKH. ctx won't ever be that, instead it will either be TOP or P2SH, both of which allow uncompressed pubkeys, but P2WPKH does not. So the context needs to be updated in order to enforce that.

  28. Sjors commented at 7:26 AM on October 9, 2023: member

    Concept ACK

    Mostly happy with a49081f6cea603d354beef1edff480d4b034dafd

  29. DrahtBot requested review from Sjors on Oct 9, 2023
  30. Sjors commented at 7:27 AM on October 9, 2023: member

    @DrahtBot you're drunk, I just reviewed it.

  31. maflcko removed review request from Sjors on Oct 9, 2023
  32. maflcko commented at 10:21 AM on October 9, 2023: member

    Thanks, fixed in https://github.com/maflcko/DrahtBot/commit/f8d7c68cda2c9eb2cf147666f28c4bd96d735f81

    You'll be re-requested to review at a later point in time.

  33. descriptors: Check result of InferPubkey
    InferPubkey can return a nullptr, so check it's result before continuing
    with creating the inferred descriptor.
    b7485f11ab
  34. descriptors: Move InferScript's pubkey validity checks to InferPubkey 37b9b73477
  35. test: Scripts with hybrid pubkeys are migrated to watchonly
    Descriptors disallows hybrid pubkeys. Anything with hybrid pubkeys
    should becomes a raw() descriptor that shows up in the watchonly wallet.
    f895f97014
  36. test: Unit test for inferring scripts with hybrid and uncompressed keys 74c77825e5
  37. achow101 force-pushed on Oct 9, 2023
  38. in src/script/descriptor.cpp:1896 in b7485f11ab outdated
    1896 | +                ok = false;
    1897 | +                break;
    1898 | +            }
    1899 |          }
    1900 | -        return std::make_unique<MultisigDescriptor>((int)data[0][0], std::move(providers));
    1901 | +        if (ok) return std::make_unique<MultisigDescriptor>((int)data[0][0], std::move(providers));
    


    furszy commented at 10:21 PM on October 9, 2023:

    just a coding style nit. No need to implement it if you don't like it.

    std::vector<std::unique_ptr<PubkeyProvider>> providers;
    for (size_t i = 1; i + 1 < data.size(); ++i) {
       CPubKey pubkey(data[i]);
       auto pubkey_provider = InferPubkey(pubkey, ctx, provider);
       if (!pubkey_provider) break;
       providers.push_back(std::move(pubkey_provider));
    }
    size_t expected_num = data.size() - 2; // First position is the min number of signers, and the last one is the op_checkmultisig
    if (providers.size() == expected_num) return std::make_unique<MultisigDescriptor>((int)data[0][0], std::move(providers));
    
  39. in test/functional/wallet_migration.py:828 in f895f97014 outdated
     823 | +        for desc in watchonly_wallet.listdescriptors()["descriptors"]:
     824 | +            if desc["desc"].startswith("raw(") or desc["desc"].startswith("addr("):
     825 | +                continue
     826 | +            assert False, "Hybrid pubkey watchonly wallet has more than just raw() and addr()"
     827 | +
     828 | +        wallet.unloadwallet()
    


    furszy commented at 10:32 PM on October 9, 2023:

    In f895f970: would be good to also unload the watchonly_wallet wallet.

  40. furszy commented at 10:33 PM on October 9, 2023: member

    ACK 74c77825

  41. DrahtBot requested review from sipa on Oct 9, 2023
  42. DrahtBot requested review from Sjors on Oct 9, 2023
  43. Sjors approved
  44. Sjors commented at 7:32 AM on October 11, 2023: member

    utACK 74c77825e5ab68bfa575dad86444506c43ef6c06

  45. fanquake merged this on Oct 11, 2023
  46. fanquake closed this on Oct 11, 2023

  47. Frank-GER referenced this in commit 2a48d53f31 on Oct 13, 2023
  48. luke-jr referenced this in commit cc6d6f301b on Oct 18, 2023
  49. luke-jr referenced this in commit 549401a089 on Oct 18, 2023
  50. luke-jr referenced this in commit 3ca5d9b6ef on Oct 18, 2023
  51. luke-jr referenced this in commit 98208a813c on Oct 18, 2023
  52. bitcoin locked this on Oct 10, 2024

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: 2026-05-08 12:13 UTC

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