descriptors: check duplicate keys in all multipath Miniscript branches #35742

pull yashbhutwala wants to merge 1 commits into bitcoin:master from yashbhutwala:fix/multipath-miniscript-sanity changing 2 files +39 −3
  1. yashbhutwala commented at 8:36 PM on July 17, 2026: contributor

    Fixes #35629.

    Multipath Miniscript descriptors performed duplicate-key sanity checking while parsing the template, when key comparison used the first multipath branch. As a result, duplicate keys present only in a later branch were accepted.

    Select each expanded multipath branch and rerun the existing duplicate-key check before constructing the final descriptors. If a branch contains a duplicate, report the offending concrete Miniscript expression.

    The regression test covers:

    • a valid two-branch wsh control;
    • a duplicate in the second wsh branch;
    • a duplicate in the third of three tr branches.

    Tested with:

    build/bin/test_bitcoin --run_test=descriptor_tests/multipath_miniscript_duplicate_keys
    build/bin/test_bitcoin --run_test=descriptor_tests
    build/bin/test_bitcoin
    
  2. DrahtBot added the label Descriptors on Jul 17, 2026
  3. DrahtBot commented at 8:36 PM on July 17, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK l0rinc, vicjuma

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36122 (BIP460: CISA for Taproot key path spends by fjahr)
    • #35445 (wallet, descriptor: Revert StringType::COMPAT for Miniscript expressions and drop the concept of a Descriptor ID that can be validated 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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. yashbhutwala marked this as ready for review on Aug 2, 2026
  5. Zeegaths commented at 9:05 AM on August 4, 2026: none

    I ran this on master and on pr-35742,

    bitcoin-cli getdescriptorinfo "wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<0;1>),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<2;1>)))"
    

    and this is what I got:

    On master Same public key is generated

    {
      "descriptor": "wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/2)))#kzwg47ay",
      "multipath_expansion": [
        "wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/2)))#kzwg47ay",
        "wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1)))#jkt2efkp"
      ],
      "checksum": "mn7hqkkh",
      "isrange": false,
      "issolvable": true,
      "hasprivatekeys": false
    }
    

    On branch

    error code: -5
    error message:
    or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1)) is not sane: contains duplicate public keys
    

    I also checked using importdescriptors to observe wallet action.

    On master, the unsafe branch is added to the wallet:

    bitcoin-cli -rpcwallet=testwallet_master importdescriptors '[{"desc": "wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<0;1>),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<2;1>)))#mn7hqkkh", "timestamp": "now", "active": false}]'
    
    [
      {
        "success": true
      }
    ]
    

    On the PR:

    [
      {
        "success": false,
        "error": {
          "code": -5,
          "message": "or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1)) is not sane: contains duplicate public keys"
        }
      }
    ]
    

    This shows exactly how the PR fixes the silent passing on master

  6. DrahtBot added the label Needs rebase on Aug 24, 2026
  7. l0rinc commented at 9:51 PM on September 4, 2026: contributor

    Concept ACK, I also found the same issue - please rebase.

  8. descriptor: check duplicate keys in all multipath branches 5ced9e7cfd
  9. yashbhutwala force-pushed on Sep 5, 2026
  10. DrahtBot removed the label Needs rebase on Sep 5, 2026
  11. in src/script/descriptor.cpp:1 in 5ced9e7cfd


    vicjuma commented at 6:26 PM on September 7, 2026:

    #22838 :-}. This change aligns with BIP389. Hardcoding the 0th index assumes that there is always a single path/branch that contradicts the BIP imo. Reproduced the error.

    Before PR

    <img width="3144" height="1008" alt="Image" src="https://github.com/user-attachments/assets/be17f175-b7d4-48a3-bf78-a88c3e48f3bd" />

    After PR

    <img width="3146" height="718" alt="Image" src="https://github.com/user-attachments/assets/27211b0b-f82a-47f2-a141-f0e08f285e20" />

  12. vicjuma commented at 6:27 PM on September 7, 2026: contributor

    Concept ACK

  13. in src/script/descriptor.cpp:2750 in 5ced9e7cfd
    2745 | +                    const auto* insane_node = &node.value();
    2746 | +                    if (const auto sub = node->FindInsaneSub()) insane_node = sub;
    2747 | +                    error = *insane_node->ToString(parser) + " is not sane: contains duplicate public keys";
    2748 | +                    return {};
    2749 | +                }
    2750 | +            }
    


    l0rinc commented at 7:44 PM on September 8, 2026:

    Given that sane already checks for duplicates in https://github.com/bitcoin/bitcoin/blob/1fdd208c1ce32ae2385069f39e7d78972213424a/src/script/miniscript.h#L1699, as far as I can tell this can be simplified:

                // Recheck duplicate keys after multipath expansion, since parsing checked only branch 0
                for (size_t i{1}; i < num_multipath; ++i) {
                    parser.m_multipath_index = i;
                    node->DuplicateKeyCheck(parser);
                    if (!node->CheckDuplicateKey()) {
                        error = *node->FindInsaneSub()->ToString(parser) + " is not sane: contains duplicate public keys";
                        return {};
                    }
                }
    
  14. l0rinc changes_requested
  15. l0rinc commented at 7:51 PM on September 8, 2026: contributor

    I suggest splitting this into focused commits so reviewers can understand the old behavior and the problem before assessing the fix: test infrastructure and controls, characterization of the old behavior with TODOs explaining why those expectations are wrong, then the fix and updated expectations. See https://github.com/l0rinc/bitcoin/pull/295 for how I split the same change.

    We should also take this opportunity to add coverage for mixed single-path/multipath keys, wildcards, hardened paths with and without private keys, differing origins, nested errors, and valid key sharing across branches and Taproot leaves.

    A functional test would demonstrate the wallet impact: these imports currently succeed but prevent reload. With the fix, rejected imports leave the descriptor set unchanged and the wallet reloadable.

    Edit: please make the PR description less robotic, show us how you tested this manually for example


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-09-09 07:56 UTC