miniscript: Use Func and Expr when parsing keys, hashes, and locktimes #34141

pull achow101 wants to merge 5 commits into bitcoin:master from achow101:musig-miniscript changing 9 files +148 −103
  1. achow101 commented at 9:36 pm on December 22, 2025: member

    The miniscript parser currently only looks for the next ) when parsing key, hash, and locktime expressions. This fails to parse when the expressions contain a nested expression. Currently, this is only possible with musig() inside of key expressions. However, this pattern can be generalized to handling hashes and locktimes, so I implemented those too.

    Fixes #34076

  2. DrahtBot added the label Descriptors on Dec 22, 2025
  3. DrahtBot commented at 9:37 pm on December 22, 2025: 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/34141.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK sipa
    Stale ACK scgbckbone

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet 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.

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • std::make_unique(key_exp_index, pubkey, false) in src/script/descriptor.cpp
    • std::make_unique(key_exp_index, pubkey, true) in src/script/descriptor.cpp
    • std::make_unique(key_exp_index, pubkey, true) in src/script/descriptor.cpp

    2026-02-02 23:25:49

  4. achow101 force-pushed on Dec 22, 2025
  5. DrahtBot added the label CI failed on Dec 22, 2025
  6. DrahtBot commented at 10:11 pm on December 22, 2025: contributor

    🚧 At least one of the CI tasks failed. Task TSan: https://github.com/bitcoin/bitcoin/actions/runs/20444643161/job/58745329801 LLM reason (✨ experimental): descriptor_tests failed with Subprocess aborted (CTest exit code 8).

    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 added the label Needs rebase on Dec 23, 2025
  8. achow101 force-pushed on Dec 23, 2025
  9. DrahtBot removed the label Needs rebase on Dec 23, 2025
  10. scgbckbone commented at 2:57 pm on December 23, 2025: contributor

    Import now works OK, but there is a problem with address generation. Test case:

    0self.test_success_case("time-locked musig",
    1                     "tr(musig($0,$1,$2)/<0;1>/*,{and_v(v:pk(musig($0,$1)/<0;1>/*),after(1)),{and_v(v:pk(musig($0,$2)/<0;1>/*),after(1)),and_v(v:pk(musig($1,$2)/<0;1>/*),after(1))}})",
    2                     scriptpath=True)
    

    Particular case:

     0core_desc_object = [{'desc': 'tr(musig([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1,[fdc7b3da/44h/1h/0h]tpubDDdbJUqUksL8pDjkh18Z6YC3kvBW7zufRKnawvvToRh3oTYVvuV3mNyEgav7VZf7JVMvSUxDxAc1MipVETVe6jmTzcaTNMRGf3Gh3eCQFsY,[44fbc2f9/44h/1h/0h]tpubDDMKx1DX6V3wfubHm2jSX11ToHMdYYwmtS3Sm3VJ5ky5efECGPa1kN3zN8hqdeEqrbqez33f8YHLARxUoEGkr2r68ViwXWAxhVcU8Tw1pSV)/<2;3>/*,{and_v(v:pk(musig([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1,[44fbc2f9/44h/1h/0h]tpubDDMKx1DX6V3wfubHm2jSX11ToHMdYYwmtS3Sm3VJ5ky5efECGPa1kN3zN8hqdeEqrbqez33f8YHLARxUoEGkr2r68ViwXWAxhVcU8Tw1pSV)/<0;1>/*),older(10)),{and_v(v:pk(musig([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1,[fdc7b3da/44h/1h/0h]tpubDDdbJUqUksL8pDjkh18Z6YC3kvBW7zufRKnawvvToRh3oTYVvuV3mNyEgav7VZf7JVMvSUxDxAc1MipVETVe6jmTzcaTNMRGf3Gh3eCQFsY)/<0;1>/*),older(10)),and_v(v:pk(musig([fdc7b3da/44h/1h/0h]tpubDDdbJUqUksL8pDjkh18Z6YC3kvBW7zufRKnawvvToRh3oTYVvuV3mNyEgav7VZf7JVMvSUxDxAc1MipVETVe6jmTzcaTNMRGf3Gh3eCQFsY,[44fbc2f9/44h/1h/0h]tpubDDMKx1DX6V3wfubHm2jSX11ToHMdYYwmtS3Sm3VJ5ky5efECGPa1kN3zN8hqdeEqrbqez33f8YHLARxUoEGkr2r68ViwXWAxhVcU8Tw1pSV)/<0;1>/*),older(10))}})#3xfc5d0t', 'active': True, 'range': [0, 100], 'timestamp': 'now'}]
     1
     2
     3ms = bitcoind.create_wallet(
     4    wallet_name=name, disable_private_keys=True,
     5    blank=True, passphrase=None, avoid_reuse=False, descriptors=True
     6)
     7
     8res = ms.importdescriptors(core_desc_object)
     9# res = [{'success': True}]
    10
    11addr = ms.getnewaddress("", "bech32m")
    12
    13# just random other wallet with funds
    14x = bitcoind.supply_wallet.sendtoaddress(addr, 49)
    15# x = a166d63b05162525752a1c911454eb7e3e27a3f509b2efcd5d74069d98de4382
    16
    17# mine
    18y =bitcoind.supply_wallet.generatetoaddress(1,bitcoind.supply_wallet.getnewaddress())
    19# y = ['1826120d4e15d6c880ec5633ca74912bbae3a28c749a502de15379ce39af61a2']
    20
    21# must have unspent now - but we don't
    22unspent = ms.listunspent()
    23# unspent = []
    24
    25# first external address is not even recognized as ours
    26ms.getaddressinfo(addr)
    27# {'address': 'bcrt1pwvnfa5ypmfvnmcguwllehxqreq9ljrvz58hgeqxpwy9xqfta7r6scptqp0', 
    28# 'scriptPubKey': '512073269ed081da593de11c77ff9b9803c80bf90d82a1ee8c80c1710a60257df0f5', 
    29# 'ismine': False, 'solvable': False, 'iswatchonly': False, 'isscript': True, 'iswitness': True, 
    30# 'witness_version': 1,
    31# 'witness_program': '73269ed081da593de11c77ff9b9803c80bf90d82a1ee8c80c1710a60257df0f5', 
    32# 'ischange': False, 'labels': ['']}
    33
    34# generating next address (index 1) works, and is recognized as ours
    35next_addr = ms.getnewaddress("", "bech32m")
    36bitcoind.supply_wallet.sendtoaddress(next_addr, 10)
    37bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
    38ms.listunspent()
    39# [{'txid': 'bd893fdd25dced57a193cc8b5fe15bc8b7309744c5156c7932712e4e34f7141f', 'vout': 1, 'address': 'bcrt1pgkt8zf54dgkpf66zt56etr8ksctwczn453ec4m2gqydknstuk7mqg0rrlu', 'label': '', 'scriptPubKey': '512045967126956a2c14eb425d35958cf68616ec0a75a4738aed48011b69c17cb7b6', 'amount': Decimal('10.00000000'), 'confirmations': 1, 'spendable': True, 'solvable': True, 'desc': 'tr([26cdffeb/2/1]9978b573defe38f5d849c2a076a6c666302d49c030ff160ebf40f7ee0a2662dc,{{and_v(v:pk([7a999231/0/1]2d40be15b6fd07671c9249a1ebe321e50e7f1332840cb560324613193e067efb),older(10)),and_v(v:pk([92d0b6b5/0/1]aa6d417376de11e860d2dc78280c6035ac547aa74f09514b4af630242efc7e37),older(10))},and_v(v:pk([0b835bd9/0/1]d30d1e36b2bcd571a07c40342d39e2afd498ac3c914ee50698bdf3df6b5f605f),older(10))})#7asktx0z', 'parent_descs': ['tr(musig([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1,[fdc7b3da/44h/1h/0h]tpubDDdbJUqUksL8pDjkh18Z6YC3kvBW7zufRKnawvvToRh3oTYVvuV3mNyEgav7VZf7JVMvSUxDxAc1MipVETVe6jmTzcaTNMRGf3Gh3eCQFsY,[44fbc2f9/44h/1h/0h]tpubDDMKx1DX6V3wfubHm2jSX11ToHMdYYwmtS3Sm3VJ5ky5efECGPa1kN3zN8hqdeEqrbqez33f8YHLARxUoEGkr2r68ViwXWAxhVcU8Tw1pSV)/2/*,{and_v(v:pk(musig([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1,[44fbc2f9/44h/1h/0h]tpubDDMKx1DX6V3wfubHm2jSX11ToHMdYYwmtS3Sm3VJ5ky5efECGPa1kN3zN8hqdeEqrbqez33f8YHLARxUoEGkr2r68ViwXWAxhVcU8Tw1pSV)/0/*),older(10)),{and_v(v:pk(musig([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1,[fdc7b3da/44h/1h/0h]tpubDDdbJUqUksL8pDjkh18Z6YC3kvBW7zufRKnawvvToRh3oTYVvuV3mNyEgav7VZf7JVMvSUxDxAc1MipVETVe6jmTzcaTNMRGf3Gh3eCQFsY)/0/*),older(10)),and_v(v:pk(musig([fdc7b3da/44h/1h/0h]tpubDDdbJUqUksL8pDjkh18Z6YC3kvBW7zufRKnawvvToRh3oTYVvuV3mNyEgav7VZf7JVMvSUxDxAc1MipVETVe6jmTzcaTNMRGf3Gh3eCQFsY,[44fbc2f9/44h/1h/0h]tpubDDMKx1DX6V3wfubHm2jSX11ToHMdYYwmtS3Sm3VJ5ky5efECGPa1kN3zN8hqdeEqrbqez33f8YHLARxUoEGkr2r68ViwXWAxhVcU8Tw1pSV)/0/*),older(10))}})#862yfmft'], 'safe': True}]
    

    According to my calculations the address is still incorrect even tho recognized as is_mine. My app calculated first two external addresses:

    1. bcrt1p4e7gsehp8xg87gclgf0fct4ng6sgg0a9ctg4laj5u3wuylw0xcfsyh86xw
    2. bcrt1pp4294qkmk43rsmzsan3ckecpv9fktckfkgew2knvnpfh65zl0dqqdaytlp

    Core calculated addresses with getnewaddress:

    1. bcrt1pwvnfa5ypmfvnmcguwllehxqreq9ljrvz58hgeqxpwy9xqfta7r6scptqp0
    2. bcrt1pgkt8zf54dgkpf66zt56etr8ksctwczn453ec4m2gqydknstuk7mqg0rrlu

    Funnily enough, when I use external descriptor from listdescriptors and run deriveaddresses it matches what my app calculates, not what getnewaddress provides.

  11. achow101 force-pushed on Dec 23, 2025
  12. achow101 commented at 9:54 pm on December 23, 2025: member
    Looks like that was caused by the key expression index being counted incorrectly for musig inside of miniscript, and therefore looking up the wrong key in the cache. Should be fixed now.
  13. maflcko commented at 8:50 am on December 24, 2025: member

    The CI fails due to UB:

    0/cxx_build/include/c++/v1/__iterator/bounded_iter.h:123: libc++ Hardening assertion __current_ != __end_ failed: __bounded_iter::operator*: Attempt to dereference an iterator at the end
    
  14. achow101 force-pushed on Jan 2, 2026
  15. achow101 force-pushed on Jan 3, 2026
  16. DrahtBot removed the label CI failed on Jan 3, 2026
  17. achow101 added this to the milestone 31.0 on Jan 5, 2026
  18. in src/script/descriptor.cpp:2061 in fdd46088a4
    2060+    uint32_t& m_expr_index;
    2061 
    2062     KeyParser(FlatSigningProvider* out LIFETIMEBOUND, const SigningProvider* in LIFETIMEBOUND,
    2063-              miniscript::MiniscriptContext ctx, uint32_t offset = 0)
    2064-        : m_out(out), m_in(in), m_script_ctx(ctx), m_offset(offset) {}
    2065+              miniscript::MiniscriptContext ctx, uint32_t& key_exp_index)
    


    sipa commented at 3:25 pm on January 13, 2026:

    In commit “miniscript: Use a reference to key_exp_index in KeyParser”

    Nit: worth putting a LIFETIMEBOUND on the key_exp_index argument here?


    achow101 commented at 8:59 pm on January 13, 2026:
    Done
  19. in src/script/miniscript.h:1749 in e6d64c19c6
    1747@@ -1748,27 +1748,25 @@ int FindNextChar(std::span<const char> in, const char m);
    1748 
    1749 /** Parse a key string ending at the end of the fragment's text representation. */
    


    sipa commented at 3:50 pm on January 13, 2026:

    In commit “miniscript: Using Func and Expr when parsing keys, hashes, and locktimes”

    This comment seems outdated now, as it will also stop at commas (plus the comment should probably explain the new func argument).


    achow101 commented at 8:59 pm on January 13, 2026:
    Updated the comment
  20. in src/script/miniscript.h:1758 in e6d64c19c6
    1759+    std::span<const char> expr = script::Expr(in);
    1760+    if (!script::Func(func, expr)) return {};
    1761+    return ctx.FromString(expr);
    1762 }
    1763 
    1764 /** Parse a hex string ending at the end of the fragment's text representation. */
    


    sipa commented at 3:51 pm on January 13, 2026:

    In commit “miniscript: Using Func and Expr when parsing keys, hashes, and locktimes”

    Same here.


    achow101 commented at 9:00 pm on January 13, 2026:
    Updated the comment
  21. achow101 force-pushed on Jan 13, 2026
  22. in src/script/descriptor.cpp:2266 in 3e37970a06 outdated
    2174@@ -2174,7 +2175,6 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
    2175             error = strprintf("combo(): %s", error);
    2176             return {};
    2177         }
    2178-        ++key_exp_index;
    


    sipa commented at 9:23 pm on January 13, 2026:
    I tried to @brunoerg-style revert this line, and none of the tests fail (I tried unit and functional tests). Is there a way to get some assurance that the updating is done right?

    achow101 commented at 7:26 pm on January 15, 2026:

    I think this is actually quite hard to test for currently. For top level single key expressions like combo(), incrementing key_exp_index fundamentally doesn’t matter as it’s not going to be used by anything afterwards.

    Really the only place that we could observe a difference in behavior is in looking up the wrong key in the cache. But so long as the incrementing is consistent, I don’t think there even is any observable misbehavior. All that is necessary is that the expression index matches the expression index in the cache, so it can be wrong as long as it is consistently wrong.

    The reason that this change is necessary is because musig wasn’t even incrementing at all which led to the cache entries colliding and being overwritten.


    sipa commented at 3:01 pm on January 20, 2026:

    Thanks for explaining.

    So the concern is about whether two keys might be ending up on the same index. Is there a way to test that? E.g. count the keys anywhere in an expression, and see that the number of used cache positions at the end equals that?


    achow101 commented at 10:19 pm on January 20, 2026:
    I added a test case to check that the expression index matches the key count. However, this does require 2 test only functions to be added to Descriptor, and I’m not entirely sure that I like this approach. But it did reveal a bug in the counting for OriginPubkeyProvider.

    sipa commented at 9:58 pm on January 29, 2026:
    The best way to judge a test approach is by how many bugs it catches. Seems like this one did its job.
  23. achow101 force-pushed on Jan 20, 2026
  24. achow101 force-pushed on Jan 20, 2026
  25. in src/script/miniscript.h:1825 in cdf0218954 outdated
    1801-    int hash_size = FindNextChar(in, ')');
    1802-    if (hash_size < 1) return {};
    1803-    std::string val = std::string(in.begin(), in.begin() + hash_size);
    1804+    std::span<const char> expr = script::Expr(in);
    1805+    if (!script::Func(func, expr)) return {};
    1806+    std::string val = std::string(expr.begin(), expr.end());
    


    sipa commented at 6:42 pm on January 29, 2026:
    Mental note: we should really rewrite all of the span parsing logic to use std::string_views instead, so these conversions aren’t needed.
  26. in src/script/descriptor.cpp:1052 in 2d44d4176b outdated
    1046@@ -1041,6 +1047,32 @@ class DescriptorImpl : public Descriptor
    1047         }
    1048         return all;
    1049     }
    1050+
    1051+    // NOLINTNEXTLINE(misc-no-recursion)
    1052+    uint32_t GetMaxKeyExpr() const override
    


    sipa commented at 9:47 pm on January 29, 2026:
    I’d prefer non-recursive versions here, but given that they’re test-only, we can ignore that until it becomes a problem I guess?

    achow101 commented at 9:58 pm on January 29, 2026:
    Do you mean having a GetMaxKeyExpr() implemented for each *Descriptor class? I actually had done that originally, but the implementations were all just about close enough to identical that it seemed pointless to do that.

    sipa commented at 10:25 pm on January 29, 2026:

    (untested)

     0 uint32_t GetMaxKeyExpr() const final
     1 {
     2    uint32_t max_key_expr{0};
     3    std::vector<const DescriptorImpl*> todo = {this};
     4    while (!todo.empty()) {
     5        const DescriptorImpl* desc = todo.back();
     6        todo.pop_back();
     7        for (const auto& p : desc->m_pubkey_args) {
     8            max_key_expr = std::max(max_key_expr, p->m_expr_index);
     9        }
    10        for (const auto& s : desc->m_subdescriptor_args) todo.push_back(s.get());
    11    }
    12    return max_key_expr;
    13}
    14
    15size_t GetKeyCount() const final
    16{
    17    size_t count{0};
    18    std::vector<const DescriptorImpl*> todo = {this};
    19    while (!todo.empty()) {
    20        const DescriptorImpl* desc = todo.back();
    21        todo.pop_back();
    22        for (const auto& p : desc->m_pubkey_args) count += p->GetKeyCount();
    23        for (const auto& s : desc->m_subdescriptor_args) todo.push_back(s.get());
    24    }
    25    return count;
    26}
    

    (note the final; this doesn’t support individually overridden versions)


    achow101 commented at 11:25 pm on February 2, 2026:
    Pulled this suggestion and the fuzz test.
  27. sipa commented at 9:48 pm on January 29, 2026: member

    crACK 2d44d4176b1f1d790e71aa496c18f3c8229b2a89. I added this, and spent some time fuzzing the descriptor_parse and mocked_descriptor_parse fuzz tests:

     0diff --git a/src/test/fuzz/descriptor_parse.cpp b/src/test/fuzz/descriptor_parse.cpp
     1index eeba5f1369e..6b3084e23d1 100644
     2--- a/src/test/fuzz/descriptor_parse.cpp
     3+++ b/src/test/fuzz/descriptor_parse.cpp
     4@@ -61,6 +61,10 @@ static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_prov
     5     const bool is_nontop_or_nonsolvable{!*is_solvable || !desc.GetOutputType()};
     6     const bool is_input_size_info_set{max_sat_maxsig && max_sat_nonmaxsig && max_elems};
     7     assert(is_input_size_info_set || is_nontop_or_nonsolvable);
     8+
     9+    auto max_key_expr = desc.GetMaxKeyExpr();
    10+    auto key_count = desc.GetKeyCount();
    11+    assert((max_key_expr == 0 && key_count == 0) || max_key_expr + 1 == key_count);
    12 }
    13 
    14 void initialize_descriptor_parse()
    
  28. DrahtBot requested review from scgbckbone on Jan 29, 2026
  29. achow101 force-pushed on Jan 30, 2026
  30. sipa commented at 10:13 pm on February 2, 2026: member

    Partially reverting the diff here doesn’t catch any errors:

     0diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
     1index 298a9501e61..9a5600ae444 100644
     2--- a/src/script/descriptor.cpp
     3+++ b/src/script/descriptor.cpp
     4@@ -2285,6 +2285,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
     5             error = strprintf("pkh(): %s", error);
     6             return {};
     7         }
     8+        ++key_exp_index;
     9         for (auto& pubkey : pubkeys) {
    10             ret.emplace_back(std::make_unique<PKHDescriptor>(std::move(pubkey)));
    11         }
    12@@ -2296,6 +2297,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
    13             error = strprintf("combo(): %s", error);
    14             return {};
    15         }
    16+        ++key_exp_index;
    17         for (auto& pubkey : pubkeys) {
    18             ret.emplace_back(std::make_unique<ComboDescriptor>(std::move(pubkey)));
    19         }
    20@@ -2404,6 +2406,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
    21             error = strprintf("wpkh(): %s", error);
    22             return {};
    23         }
    24+        ++key_exp_index;
    25         for (auto& pubkey : pubkeys) {
    26             ret.emplace_back(std::make_unique<WPKHDescriptor>(std::move(pubkey)));
    27         }
    

    But, adding

     0diff --git a/src/test/fuzz/descriptor_parse.cpp b/src/test/fuzz/descriptor_parse.cpp
     1index eeba5f1369e..6b3084e23d1 100644
     2--- a/src/test/fuzz/descriptor_parse.cpp
     3+++ b/src/test/fuzz/descriptor_parse.cpp
     4@@ -61,6 +61,10 @@ static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_prov
     5     const bool is_nontop_or_nonsolvable{!*is_solvable || !desc.GetOutputType()};
     6     const bool is_input_size_info_set{max_sat_maxsig && max_sat_nonmaxsig && max_elems};
     7     assert(is_input_size_info_set || is_nontop_or_nonsolvable);
     8+
     9+    auto max_key_expr = desc.GetMaxKeyExpr();
    10+    auto key_count = desc.GetKeyCount();
    11+    assert((max_key_expr == 0 && key_count == 0) || max_key_expr + 1 == key_count);
    12 }
    13 
    14 void initialize_descriptor_parse()
    

    to the fuzz tests doesn’t catch the above mutations either, which leads me to suspect that diff maybe not actually have an observable effect?

  31. achow101 commented at 11:17 pm on February 2, 2026: member

    which leads me to suspect that diff maybe not actually have an observable effect?

    Indeed. The three lines reverted were for incrementing key_exp_index after parsing the key for pkh(), combo(), and wpkh(). All of these are expressions in contexts which do not allow any expressions after these are parsed, so incrementing key_exp_index isn’t actually doing anything as it is never passed into another PubkeyProvider. These lines were in fact redundant in the original code.

  32. test: Test that key expression indexes match key count ce4c66eb7c
  33. miniscript: Use a reference to key_exp_index in KeyParser
    For key_exp_index to count correctly for miniscript expressions,
    KeyParser should hold a reference to it.
    b12281bd86
  34. descriptors: Increment key_exp_index in ParsePubkey(Inner)
    Simplifies the callsites for incrementing key_exp_index
    6fd780d4fb
  35. miniscript: Using Func and Expr when parsing keys, hashes, and locktimes
    Since pk(), pk_k(), pkh(), pk_h(), sha256(), ripemd160(), hash256(),
    hash160(), after(), and older() all are single argument expressions that
    are parsed immediately, we can use the Expr and Func parsing functions
    to determine what the arguments of these expressions are, rather than
    searching for the next closing parentheses.
    
    This fixes an issue when pk(), pk_k(), pkh(), and pk_h() include a
    musig() expression as Expr properly handles nested expressions.
    ec0f47b15c
  36. test: Test for musig() in various miniscript expressions 4b53cbd692
  37. achow101 force-pushed on Feb 2, 2026
  38. sipa commented at 0:24 am on February 3, 2026: member
    ACK 4b53cbd69220c1c786bb23a72c0b26a6f78a38f7

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-02-06 00:13 UTC

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