refactor: optimize: avoid allocations in script & policy verification #33645

pull Raimo33 wants to merge 4 commits into bitcoin:master from Raimo33:optimize-tx-policy-verification changing 3 files +21 โˆ’21
  1. Raimo33 commented at 6:43 pm on October 17, 2025: contributor

    Currently, some policy and script related methods are inefficiently allocating/reallocating containers where it is completely unnecessary.

    This PR aims at optimizing policy verifications by reducing redundant heap allocations without losing performance even in worst case scenarios, effectively reducing the overall memory footprint.

  2. DrahtBot added the label Refactoring on Oct 17, 2025
  3. DrahtBot commented at 6:43 pm on October 17, 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/33645.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Approach NACK l0rinc

    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:

    • #33757 (refactor: make script Solver’s often-unused solutions parameter optional by l0rinc)
    • #32729 (test,refactor: extract script template helpers & widen sigop count coverage by l0rinc)
    • #29060 (Policy: Report debug message why inputs are non standard by ismaelsadeeq)

    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.

  4. in src/policy/policy.cpp:228 in 5d4b008728 outdated
    227+    for (const CTxIn& txin : tx.vin) {
    228+        const CTxOut& prev = mapInputs.AccessCoin(txin.prevout).out;
    229 
    230-        std::vector<std::vector<unsigned char> > vSolutions;
    231+        vSolutions.clear();
    232         TxoutType whichType = Solver(prev.scriptPubKey, vSolutions);
    


    cedwies commented at 10:16 pm on October 21, 2025:
    Just double checking: vSolutions.clear(); is intentional despite Solver(...) already clearing vSolutions? For me, I think it’s good to keep.

    Raimo33 commented at 7:26 am on October 22, 2025:
    yes, it is deliberate to prevent future programming errors.
  5. cedwies commented at 11:03 pm on October 21, 2025: none
    • reusing vector capacity across loop iterations is makes sense
    • no functional or policy logic changes introduced
    • Code refactoring (range-based loops, variable renames) improves readability

    Here is my bench on MacOS M2 MAX:

    Before:

    ns/op op/s err% total benchmark
    134,805.18 7,418.11 0.7% 3.30 AssembleBlock
    338.44 2,954,694.93 0.5% 3.29 CCoinsCaching

    After:

    ns/op op/s err% total benchmark
    134,100.45 7,457.10 0.1% 3.29 AssembleBlock
    286.00 3,496,467.59 0.3% 3.29 CCoinsCaching

    Not so sure about the Assemble Block bench though. Doesn’t it measure block assembly after admission, therefore not timing the policy checks you changed?

  6. Raimo33 commented at 7:25 am on October 22, 2025: contributor

    Not so sure about the Assemble Block bench though. Doesn’t it measure block assembly after admission, therefore not timing the policy checks you changed?

    Thanks for the review & benchmarks. I’m positive about the fact that the AssembleBlock bench calls some of the impacted functions. I’ve not delved into details, and I think it’s irrelevant considering performance is the same.

  7. l0rinc commented at 12:22 pm on October 22, 2025: contributor

    I don’t really see any speedup for AssembleBlock

    Change: gcc=-0.71%, clang=-0.82%


    CCoinsCaching does seem to be improved a bit

    Change: gcc=+18.87%, clang=+11.79%


     0 for compiler in gcc clang; do \
     1  if [ "$compiler" = "gcc" ]; then CC=gcc; CXX=g++; COMP_VER=$(gcc -dumpfullversion); \
     2  else CC=clang; CXX=clang++; COMP_VER=$(clang -dumpversion); fi && \
     3  echo "> Compiler: $compiler $COMP_VER" && \
     4  for commit in 64a7c7cbb975cb3c3f25a3f784779f32cd95ebaa aa4c5b81ce686a160a883394f00a38604d81ccdd 7ef44fe6876dcf69f043df82a944e36a8a787b16 5d4b008728e13e923cd9d9315620b486c92b225b; do \
     5    git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && git log -1 --pretty='%h %s' && \
     6    rm -rf build && \
     7    cmake -B build -DBUILD_BENCH=ON -DENABLE_IPC=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=$CXX >/dev/null 2>&1 && \
     8    cmake --build build -j$(nproc) >/dev/null 2>&1 && \
     9    for i in 1 2; do \
    10      build/bin/bench_bitcoin -filter='AssembleBlock|CCoinsCaching' -min-time=5000; \
    11    done; \
    12  done; \
    13done
    

    Compiler: gcc 15.0.1

    64a7c7cbb9 Merge bitcoin/bitcoin#33558: ci: Use native platform for win-cross task

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    332,700.15 3,005.71 0.1% 2,892,252.71 1,193,396.57 2.424 262,758.41 0.4% 5.31 AssembleBlock
    613.60 1,629,733.71 0.1% 5,790.00 2,204.34 2.627 819.00 0.1% 5.50 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    333,387.37 2,999.51 0.0% 2,889,152.01 1,195,756.63 2.416 262,647.34 0.4% 5.32 AssembleBlock
    620.71 1,611,064.09 0.1% 5,790.00 2,229.94 2.596 819.00 0.2% 5.51 CCoinsCaching

    aa4c5b81ce refactor: use range-based iteration

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    336,394.98 2,972.70 0.0% 2,892,400.57 1,206,262.36 2.398 262,630.80 0.4% 5.31 AssembleBlock
    525.13 1,904,279.72 0.1% 5,229.00 1,886.12 2.772 802.00 0.1% 5.50 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    336,470.91 2,972.03 0.1% 2,887,727.08 1,206,756.91 2.393 262,840.00 0.4% 5.21 AssembleBlock
    605.07 1,652,703.31 0.0% 5,759.00 2,173.77 2.649 816.00 0.1% 5.48 CCoinsCaching

    7ef44fe687 refactor: rename script stack

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    332,237.42 3,009.90 0.0% 2,891,082.62 1,191,956.55 2.425 262,820.66 0.4% 5.31 AssembleBlock
    525.33 1,903,566.06 0.1% 5,229.00 1,887.29 2.771 802.00 0.1% 5.50 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    330,947.55 3,021.63 0.0% 2,888,455.08 1,187,248.72 2.433 262,606.41 0.4% 5.30 AssembleBlock
    526.32 1,899,969.76 0.0% 5,229.00 1,890.73 2.766 802.00 0.1% 5.50 CCoinsCaching

    5d4b008728 optimize: reuse containers across iterations

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    334,038.19 2,993.67 0.0% 2,894,146.80 1,198,114.94 2.416 262,962.33 0.5% 5.31 AssembleBlock
    562.38 1,778,156.15 0.1% 5,412.00 2,019.23 2.680 736.00 0.0% 5.49 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    336,807.71 2,969.05 0.0% 2,894,460.95 1,208,318.07 2.395 262,725.50 0.4% 5.31 AssembleBlock
    482.11 2,074,216.12 0.0% 4,882.00 1,731.89 2.819 722.00 0.0% 5.50 CCoinsCaching

    Compiler: clang 22.0.0

    64a7c7cbb9 Merge bitcoin/bitcoin#33558: ci: Use native platform for win-cross task

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    316,532.30 3,159.24 0.0% 2,799,107.72 1,135,483.03 2.465 252,868.15 0.4% 5.29 AssembleBlock
    482.62 2,072,038.85 0.2% 4,715.00 1,733.69 2.720 688.00 0.2% 5.51 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    319,172.99 3,133.10 0.1% 2,804,493.20 1,145,219.92 2.449 252,971.35 0.4% 5.29 AssembleBlock
    484.08 2,065,772.51 0.1% 4,715.00 1,738.81 2.712 688.00 0.1% 5.50 CCoinsCaching

    aa4c5b81ce refactor: use range-based iteration

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    319,298.02 3,131.87 0.1% 2,806,937.58 1,145,187.88 2.451 253,098.73 0.4% 5.29 AssembleBlock
    491.35 2,035,216.81 0.1% 4,686.00 1,765.17 2.655 687.00 0.2% 5.51 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    319,413.24 3,130.74 0.1% 2,798,169.36 1,145,659.16 2.442 252,777.51 0.4% 5.29 AssembleBlock
    496.25 2,015,105.84 0.0% 4,686.00 1,782.54 2.629 687.00 0.2% 5.50 CCoinsCaching

    7ef44fe687 refactor: rename script stack

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    319,993.93 3,125.06 0.1% 2,801,660.61 1,147,280.13 2.442 252,912.30 0.4% 5.30 AssembleBlock
    489.84 2,041,475.07 0.1% 4,686.00 1,759.79 2.663 687.00 0.2% 5.49 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    317,083.74 3,153.74 0.1% 2,802,373.03 1,136,973.75 2.465 252,921.05 0.4% 5.29 AssembleBlock
    490.88 2,037,151.54 0.1% 4,686.00 1,763.06 2.658 687.00 0.2% 5.50 CCoinsCaching

    5d4b008728 optimize: reuse containers across iterations

    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    321,831.90 3,107.21 0.1% 2,804,969.78 1,154,021.10 2.431 252,995.03 0.4% 5.30 AssembleBlock
    432.01 2,314,757.74 0.0% 4,301.00 1,551.96 2.771 608.00 0.0% 5.41 CCoinsCaching
    ns/op op/s err% ins/op cyc/op IPC bra/op miss% total benchmark
    319,109.25 3,133.72 0.1% 2,797,937.98 1,144,492.87 2.445 252,714.07 0.4% 5.30 AssembleBlock
    432.73 2,310,885.42 0.3% 4,301.00 1,554.58 2.767 608.00 0.0% 5.42 CCoinsCaching

    But the code seems more complicated than before - is there a way to retain the speedup without complicating the code (i.e. minimizing the diff)?

  8. cedwies commented at 12:40 pm on October 22, 2025: none

    Not so sure about the Assemble Block bench though. Doesn’t it measure block assembly after admission, therefore not timing the policy checks you changed?

    Thanks for the review & benchmarks. I’m positive about the fact that the AssembleBlock bench calls some of the impacted functions. I’ve not delved into details, and I think it’s irrelevant considering performance is the same.

    It does call code on the policy path, but only during setup, not during the timed region. In static void AssembleBlock(benchmark::Bench& bench) in src/bench/block_assemble.cpp

    0    bench.run([&] {
    1        PrepareBlock(test_setup->m_node, options);
    2    });
    

    Don’t want to be nitpicking here, but just want to point out that if a bench is presented as evidence of a performance claim (faster/slower/same), it should measure the code which was changed. Fine if we use it as a sanity-check, just wanted to mention that AssembleBlock is (AFAIK) expected to stay the same, since no timed code changes. On the other hand CCoinsCaching actually times AreInputsStandard(...)

  9. Raimo33 commented at 12:47 pm on October 22, 2025: contributor
    @l0rinc @cedwies the bench is not presented as evidence. it is presented as a transparent way of saying performance hasn’t degraded. the PR body clearly states that it remained the same. I included it for the sake of transparency.
  10. cedwies commented at 1:23 pm on October 22, 2025: none

    But the code seems more complicated than before - is there a way to retain the speedup without complicating the code (i.e. minimizing the diff)?

    It is more complicated. I would:

    Keep:

    • change from index-based to range-based loops.
    • In AreInputsStandard(...): Move vSolutions outside the for loop (this brings the performance gain in CCoinsCaching)
    • In IsWitnessStandard(...): Keep the variable name change from stack to script_stack

    Discard to minimize diff (these changes don’t give a (relevant) performance gain):

    • In AreInputsStandard(...): Don’t move the stack outside the for loop
    • In IsWitnessStandard(...): Don’t move stack and witnessprogram outside the for loop
    • In SpendsNonAnchorWitnessProg(...): Don’t move stack outside the for loop.

    Optional: Add a comment/hint to clarify that moving vSolutions outside the loop is intentional?

    This way we minimize the diff, avoiding making the code more complicated than necessary, while taking advantage of the performance gain. Happy to hear your thoughts.

  11. Raimo33 commented at 2:12 pm on October 22, 2025: contributor
    • In AreInputsStandard(…): Don’t move the stack outside the for loop

    Agreed: it is not in the hot path (or the most likely path).

    • In IsWitnessStandard(…): Don’t move stack and witnessprogram outside the for loop

    Agreed for stack: it is only needed for P2SH which is unlikely Disagree for witnessprogram: it is used in the hot path (P2WPKH is the most likely)

    • In SpendsNonAnchorWitnessProg(...): Don’t move stack outside the for loop.

    Agreed: it is not in the hot path (or the most likely path).

  12. l0rinc commented at 2:12 pm on October 22, 2025: contributor

    Since std::vector only allocates on first use

    Actually it can reallocate on every size exhaustion, i.e. every resize. I don’t like the new reuse, that can actually be slower than before because now the code paths depend on each other. Wouldn’t reserving the vectors help here instead?

  13. Raimo33 force-pushed on Oct 22, 2025
  14. Raimo33 commented at 3:00 pm on October 22, 2025: contributor
    I’ve removed some of the less impacting changes, considering @cedwies suggestions. It now shows a 9% improvement on CCoinsCache on my end (up from a 7.7% previously)
  15. Raimo33 commented at 3:04 pm on October 22, 2025: contributor

    now the code paths depend on each other

    That’s not the case, at least not with the latest push:

    • vSolutions is used regardless of the code path. It’s used before branching.
    • witnessprogram is used late, after a couple of branches, but this can’t possibly impact performance since std::vector starts allocating only after first use.

    Wouldn’t reserving the vectors help here instead?

    This was my initial intuition, but it actually goes against your previous point. .reserve() would mean allocating regardless of the code path. It would mean allocating even when unnecessary.

  16. cedwies commented at 3:39 pm on October 22, 2025: none

    Actually it can reallocate on every size exhaustion, i.e. every resize. I don’t like the new reuse, that can actually be slower than before because now the code paths depend on each other. Wouldn’t reserving the vectors help here instead?

    You are referring to iteration-to-iteration coupling? Yes, the iterations depend on each other, however in practice we still allocate less than before, so there should not be a slowdown. Are you suggesting something like:

    0witnessprogram.reserve(40); // segwit witness program length <= 40 bytes
    

    ? I would not use reserve() for vSolutions because it would be mostly guesswork with little payoff and hoisting alone already gets the win in performance. Or am I missing your point @l0rinc ?

  17. l0rinc commented at 12:40 pm on October 23, 2025: contributor

    I like the new version better, you have indeed identified an inconsistency that we could fix in a way that would make the source could also more readable.

    However, I think the current version still makes the code less readable than it was before - now the loop iterations depend on each other by reusing the same vector. However, vSolutions that you’re reusing isn’t actually needed at all (maybe that’s what the compiler detected after your change). The cleaner alternative (that I also bumped into previously) is to make the vector optional:

      0diff --git a/src/addresstype.cpp b/src/addresstype.cpp
      1index 67e643943d..979983b705 100644
      2--- a/src/addresstype.cpp
      3+++ b/src/addresstype.cpp
      4@@ -49,7 +49,7 @@ WitnessV0ScriptHash::WitnessV0ScriptHash(const CScript& in)
      5 bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet)
      6 {
      7     std::vector<valtype> vSolutions;
      8-    TxoutType whichType = Solver(scriptPubKey, vSolutions);
      9+    TxoutType whichType = Solver(scriptPubKey, &vSolutions);
     10 
     11     switch (whichType) {
     12     case TxoutType::PUBKEY: {
     13diff --git a/src/common/bloom.cpp b/src/common/bloom.cpp
     14index efb4178cab..c4133221c0 100644
     15--- a/src/common/bloom.cpp
     16+++ b/src/common/bloom.cpp
     17@@ -123,8 +123,7 @@ bool CBloomFilter::IsRelevantAndUpdate(const CTransaction& tx)
     18                     insert(COutPoint(hash, i));
     19                 else if ((nFlags & BLOOM_UPDATE_MASK) == BLOOM_UPDATE_P2PUBKEY_ONLY)
     20                 {
     21-                    std::vector<std::vector<unsigned char> > vSolutions;
     22-                    TxoutType type = Solver(txout.scriptPubKey, vSolutions);
     23+                    TxoutType type = Solver(txout.scriptPubKey);
     24                     if (type == TxoutType::PUBKEY || type == TxoutType::MULTISIG) {
     25                         insert(COutPoint(hash, i));
     26                     }
     27diff --git a/src/core_write.cpp b/src/core_write.cpp
     28index 14836f5148..4aea803997 100644
     29--- a/src/core_write.cpp
     30+++ b/src/core_write.cpp
     31@@ -159,8 +159,7 @@ void ScriptToUniv(const CScript& script, UniValue& out, bool include_hex, bool i
     32         out.pushKV("hex", HexStr(script));
     33     }
     34 
     35-    std::vector<std::vector<unsigned char>> solns;
     36-    const TxoutType type{Solver(script, solns)};
     37+    const TxoutType type{Solver(script)};
     38 
     39     if (include_address && ExtractDestination(script, address) && type != TxoutType::PUBKEY) {
     40         out.pushKV("address", EncodeDestination(address));
     41diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp
     42index 454f3f9021..8f1c0e9b8d 100644
     43--- a/src/policy/policy.cpp
     44+++ b/src/policy/policy.cpp
     45@@ -79,7 +79,7 @@ std::vector<uint32_t> GetDust(const CTransaction& tx, CFeeRate dust_relay_rate)
     46 bool IsStandard(const CScript& scriptPubKey, TxoutType& whichType)
     47 {
     48     std::vector<std::vector<unsigned char> > vSolutions;
     49-    whichType = Solver(scriptPubKey, vSolutions);
     50+    whichType = Solver(scriptPubKey, &vSolutions);
     51 
     52     if (whichType == TxoutType::NONSTANDARD) {
     53         return false;
     54@@ -220,12 +220,10 @@ bool AreInputsStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
     55         return false;
     56     }
     57 
     58-    std::vector<std::vector<unsigned char> > vSolutions;
     59     for (const CTxIn& txin : tx.vin) {
     60         const CTxOut& prev = mapInputs.AccessCoin(txin.prevout).out;
     61 
     62-        vSolutions.clear();
     63-        TxoutType whichType = Solver(prev.scriptPubKey, vSolutions);
     64+        TxoutType whichType = Solver(prev.scriptPubKey);
     65         if (whichType == TxoutType::NONSTANDARD || whichType == TxoutType::WITNESS_UNKNOWN) {
     66             // WITNESS_UNKNOWN failures are typically also caught with a policy
     67             // flag in the script interpreter, but it can be helpful to catch
     68diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
     69index 1a8edc594f..4d1a90bf47 100644
     70--- a/src/rpc/rawtransaction.cpp
     71+++ b/src/rpc/rawtransaction.cpp
     72@@ -534,7 +534,7 @@ static RPCHelpMan decodescript()
     73     ScriptToUniv(script, /*out=*/r, /*include_hex=*/false, /*include_address=*/true);
     74 
     75     std::vector<std::vector<unsigned char>> solutions_data;
     76-    const TxoutType which_type{Solver(script, solutions_data)};
     77+    const TxoutType which_type{Solver(script, &solutions_data)};
     78 
     79     const bool can_wrap{[&] {
     80         switch (which_type) {
     81diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
     82index bd819d365a..5ba2d43576 100644
     83--- a/src/script/descriptor.cpp
     84+++ b/src/script/descriptor.cpp
     85@@ -2569,7 +2569,7 @@ std::unique_ptr<DescriptorImpl> InferScript(const CScript& script, ParseScriptCo
     86     }
     87 
     88     std::vector<std::vector<unsigned char>> data;
     89-    TxoutType txntype = Solver(script, data);
     90+    TxoutType txntype = Solver(script, &data);
     91 
     92     if (txntype == TxoutType::PUBKEY && (ctx == ParseScriptContext::TOP || ctx == ParseScriptContext::P2SH || ctx == ParseScriptContext::P2WSH)) {
     93         CPubKey pubkey(data[0]);
     94diff --git a/src/script/sign.cpp b/src/script/sign.cpp
     95index 33cbc38be4..980ddc9abd 100644
     96--- a/src/script/sign.cpp
     97+++ b/src/script/sign.cpp
     98@@ -406,7 +406,7 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator
     99     std::vector<unsigned char> sig;
    100 
    101     std::vector<valtype> vSolutions;
    102-    whichTypeRet = Solver(scriptPubKey, vSolutions);
    103+    whichTypeRet = Solver(scriptPubKey, &vSolutions);
    104 
    105     switch (whichTypeRet) {
    106     case TxoutType::NONSTANDARD:
    107@@ -625,7 +625,7 @@ SignatureData DataFromTransaction(const CMutableTransaction& tx, unsigned int nI
    108 
    109     // Get scripts
    110     std::vector<std::vector<unsigned char>> solutions;
    111-    TxoutType script_type = Solver(txout.scriptPubKey, solutions);
    112+    TxoutType script_type = Solver(txout.scriptPubKey, &solutions);
    113     SigVersion sigversion = SigVersion::BASE;
    114     CScript next_script = txout.scriptPubKey;
    115 
    116@@ -636,7 +636,7 @@ SignatureData DataFromTransaction(const CMutableTransaction& tx, unsigned int nI
    117         next_script = std::move(redeem_script);
    118 
    119         // Get redeemScript type
    120-        script_type = Solver(next_script, solutions);
    121+        script_type = Solver(next_script, &solutions);
    122         stack.script.pop_back();
    123     }
    124     if (script_type == TxoutType::WITNESS_V0_SCRIPTHASH && !stack.witness.empty() && !stack.witness.back().empty()) {
    125@@ -646,7 +646,7 @@ SignatureData DataFromTransaction(const CMutableTransaction& tx, unsigned int nI
    126         next_script = std::move(witness_script);
    127 
    128         // Get witnessScript type
    129-        script_type = Solver(next_script, solutions);
    130+        script_type = Solver(next_script, &solutions);
    131         stack.witness.pop_back();
    132         stack.script = std::move(stack.witness);
    133         stack.witness.clear();
    134@@ -751,7 +751,7 @@ bool IsSegWitOutput(const SigningProvider& provider, const CScript& script)
    135     if (script.IsWitnessProgram(version, program)) return true;
    136     if (script.IsPayToScriptHash()) {
    137         std::vector<valtype> solutions;
    138-        auto whichtype = Solver(script, solutions);
    139+        auto whichtype = Solver(script, &solutions);
    140         if (whichtype == TxoutType::SCRIPTHASH) {
    141             auto h160 = uint160(solutions[0]);
    142             CScript subscript;
    143diff --git a/src/script/solver.cpp b/src/script/solver.cpp
    144index 783baf0708..2e45c7d305 100644
    145--- a/src/script/solver.cpp
    146+++ b/src/script/solver.cpp
    147@@ -138,16 +138,15 @@ std::optional<std::pair<int, std::vector<std::span<const unsigned char>>>> Match
    148     return std::pair{*threshold, std::move(keyspans)};
    149 }
    150 
    151-TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned char>>& vSolutionsRet)
    152+TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned char>>* vSolutionsRet)
    153 {
    154-    vSolutionsRet.clear();
    155+    if (vSolutionsRet) vSolutionsRet->clear();
    156 
    157     // Shortcut for pay-to-script-hash, which are more constrained than the other types:
    158     // it is always OP_HASH160 20 [20 byte hash] OP_EQUAL
    159-    if (scriptPubKey.IsPayToScriptHash())
    160-    {
    161+    if (scriptPubKey.IsPayToScriptHash()) {
    162         std::vector<unsigned char> hashBytes(scriptPubKey.begin()+2, scriptPubKey.begin()+22);
    163-        vSolutionsRet.push_back(hashBytes);
    164+        if (vSolutionsRet) vSolutionsRet->push_back(hashBytes);
    165         return TxoutType::SCRIPTHASH;
    166     }
    167 
    168@@ -155,23 +154,25 @@ TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned c
    169     std::vector<unsigned char> witnessprogram;
    170     if (scriptPubKey.IsWitnessProgram(witnessversion, witnessprogram)) {
    171         if (witnessversion == 0 && witnessprogram.size() == WITNESS_V0_KEYHASH_SIZE) {
    172-            vSolutionsRet.push_back(std::move(witnessprogram));
    173+            if (vSolutionsRet) vSolutionsRet->push_back(std::move(witnessprogram));
    174             return TxoutType::WITNESS_V0_KEYHASH;
    175         }
    176         if (witnessversion == 0 && witnessprogram.size() == WITNESS_V0_SCRIPTHASH_SIZE) {
    177-            vSolutionsRet.push_back(std::move(witnessprogram));
    178+            if (vSolutionsRet) vSolutionsRet->push_back(std::move(witnessprogram));
    179             return TxoutType::WITNESS_V0_SCRIPTHASH;
    180         }
    181         if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE) {
    182-            vSolutionsRet.push_back(std::move(witnessprogram));
    183+            if (vSolutionsRet) vSolutionsRet->push_back(std::move(witnessprogram));
    184             return TxoutType::WITNESS_V1_TAPROOT;
    185         }
    186         if (scriptPubKey.IsPayToAnchor()) {
    187             return TxoutType::ANCHOR;
    188         }
    189         if (witnessversion != 0) {
    190-            vSolutionsRet.push_back(std::vector<unsigned char>{(unsigned char)witnessversion});
    191-            vSolutionsRet.push_back(std::move(witnessprogram));
    192+            if (vSolutionsRet) {
    193+                vSolutionsRet->push_back(std::vector<unsigned char>{(unsigned char)witnessversion});
    194+                vSolutionsRet->push_back(std::move(witnessprogram));
    195+            }
    196             return TxoutType::WITNESS_UNKNOWN;
    197         }
    198         return TxoutType::NONSTANDARD;
    199@@ -188,25 +189,27 @@ TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned c
    200 
    201     std::vector<unsigned char> data;
    202     if (MatchPayToPubkey(scriptPubKey, data)) {
    203-        vSolutionsRet.push_back(std::move(data));
    204+        if (vSolutionsRet) vSolutionsRet->push_back(std::move(data));
    205         return TxoutType::PUBKEY;
    206     }
    207 
    208     if (MatchPayToPubkeyHash(scriptPubKey, data)) {
    209-        vSolutionsRet.push_back(std::move(data));
    210+        if (vSolutionsRet) vSolutionsRet->push_back(std::move(data));
    211         return TxoutType::PUBKEYHASH;
    212     }
    213 
    214     int required;
    215     std::vector<std::vector<unsigned char>> keys;
    216     if (MatchMultisig(scriptPubKey, required, keys)) {
    217-        vSolutionsRet.push_back({static_cast<unsigned char>(required)}); // safe as required is in range 1..20
    218-        vSolutionsRet.insert(vSolutionsRet.end(), keys.begin(), keys.end());
    219-        vSolutionsRet.push_back({static_cast<unsigned char>(keys.size())}); // safe as size is in range 1..20
    220+        if (vSolutionsRet) {
    221+            vSolutionsRet->push_back({static_cast<unsigned char>(required)}); // safe as required is in range 1..20
    222+            vSolutionsRet->insert(vSolutionsRet->end(), keys.begin(), keys.end());
    223+            vSolutionsRet->push_back({static_cast<unsigned char>(keys.size())}); // safe as size is in range 1..20
    224+        }
    225         return TxoutType::MULTISIG;
    226     }
    227 
    228-    vSolutionsRet.clear();
    229+    if (vSolutionsRet) vSolutionsRet->clear();
    230     return TxoutType::NONSTANDARD;
    231 }
    232 
    233diff --git a/src/script/solver.h b/src/script/solver.h
    234index d2b7fb8881..f708a00f82 100644
    235--- a/src/script/solver.h
    236+++ b/src/script/solver.h
    237@@ -52,7 +52,7 @@ constexpr bool IsPushdataOp(opcodetype opcode)
    238  * [@param](/bitcoin-bitcoin/contributor/param/)[out]  vSolutionsRet  Vector of parsed pubkeys and hashes
    239  * [@return](/bitcoin-bitcoin/contributor/return/)                     The script type. TxoutType::NONSTANDARD represents a failed solve.
    240  */
    241-TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned char>>& vSolutionsRet);
    242+TxoutType Solver(const CScript& scriptPubKey, std::vector<std::vector<unsigned char>>* vSolutionsRet = nullptr);
    243 
    244 /** Generate a P2PK script for the given pubkey. */
    245 CScript GetScriptForRawPubKey(const CPubKey& pubkey);
    246diff --git a/src/test/fuzz/key.cpp b/src/test/fuzz/key.cpp
    247index e4bedff8b5..5660310935 100644
    248--- a/src/test/fuzz/key.cpp
    249+++ b/src/test/fuzz/key.cpp
    250@@ -160,13 +160,13 @@ FUZZ_TARGET(key, .init = initialize_key)
    251         assert(which_type_tx_multisig == TxoutType::MULTISIG);
    252 
    253         std::vector<std::vector<unsigned char>> v_solutions_ret_tx_pubkey;
    254-        const TxoutType outtype_tx_pubkey = Solver(tx_pubkey_script, v_solutions_ret_tx_pubkey);
    255+        const TxoutType outtype_tx_pubkey = Solver(tx_pubkey_script, &v_solutions_ret_tx_pubkey);
    256         assert(outtype_tx_pubkey == TxoutType::PUBKEY);
    257         assert(v_solutions_ret_tx_pubkey.size() == 1);
    258         assert(v_solutions_ret_tx_pubkey[0].size() == 33);
    259 
    260         std::vector<std::vector<unsigned char>> v_solutions_ret_tx_multisig;
    261-        const TxoutType outtype_tx_multisig = Solver(tx_multisig_script, v_solutions_ret_tx_multisig);
    262+        const TxoutType outtype_tx_multisig = Solver(tx_multisig_script, &v_solutions_ret_tx_multisig);
    263         assert(outtype_tx_multisig == TxoutType::MULTISIG);
    264         assert(v_solutions_ret_tx_multisig.size() == 3);
    265         assert(v_solutions_ret_tx_multisig[0].size() == 1);
    266diff --git a/src/test/fuzz/script.cpp b/src/test/fuzz/script.cpp
    267index dfad0b7184..38608b48be 100644
    268--- a/src/test/fuzz/script.cpp
    269+++ b/src/test/fuzz/script.cpp
    270@@ -91,7 +91,8 @@ FUZZ_TARGET(script, .init = initialize_script)
    271     (void)RecursiveDynamicUsage(script);
    272 
    273     std::vector<std::vector<unsigned char>> solutions;
    274-    (void)Solver(script, solutions);
    275+    (void)Solver(script);
    276+    (void)Solver(script, &solutions);
    277 
    278     (void)script.HasValidOps();
    279     (void)script.IsPayToAnchor();
    280diff --git a/src/test/script_standard_tests.cpp b/src/test/script_standard_tests.cpp
    281index 9a63426e7d..49d7b164bc 100644
    282--- a/src/test/script_standard_tests.cpp
    283+++ b/src/test/script_standard_tests.cpp
    284@@ -42,14 +42,14 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    285     // TxoutType::PUBKEY
    286     s.clear();
    287     s << ToByteVector(pubkeys[0]) << OP_CHECKSIG;
    288-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::PUBKEY);
    289+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::PUBKEY);
    290     BOOST_CHECK_EQUAL(solutions.size(), 1U);
    291     BOOST_CHECK(solutions[0] == ToByteVector(pubkeys[0]));
    292 
    293     // TxoutType::PUBKEYHASH
    294     s.clear();
    295     s << OP_DUP << OP_HASH160 << ToByteVector(pubkeys[0].GetID()) << OP_EQUALVERIFY << OP_CHECKSIG;
    296-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::PUBKEYHASH);
    297+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::PUBKEYHASH);
    298     BOOST_CHECK_EQUAL(solutions.size(), 1U);
    299     BOOST_CHECK(solutions[0] == ToByteVector(pubkeys[0].GetID()));
    300 
    301@@ -57,7 +57,7 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    302     CScript redeemScript(s); // initialize with leftover P2PKH script
    303     s.clear();
    304     s << OP_HASH160 << ToByteVector(CScriptID(redeemScript)) << OP_EQUAL;
    305-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::SCRIPTHASH);
    306+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::SCRIPTHASH);
    307     BOOST_CHECK_EQUAL(solutions.size(), 1U);
    308     BOOST_CHECK(solutions[0] == ToByteVector(CScriptID(redeemScript)));
    309 
    310@@ -67,7 +67,7 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    311         ToByteVector(pubkeys[0]) <<
    312         ToByteVector(pubkeys[1]) <<
    313         OP_2 << OP_CHECKMULTISIG;
    314-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::MULTISIG);
    315+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::MULTISIG);
    316     BOOST_CHECK_EQUAL(solutions.size(), 4U);
    317     BOOST_CHECK(solutions[0] == std::vector<unsigned char>({1}));
    318     BOOST_CHECK(solutions[1] == ToByteVector(pubkeys[0]));
    319@@ -80,7 +80,7 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    320         ToByteVector(pubkeys[1]) <<
    321         ToByteVector(pubkeys[2]) <<
    322         OP_3 << OP_CHECKMULTISIG;
    323-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::MULTISIG);
    324+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::MULTISIG);
    325     BOOST_CHECK_EQUAL(solutions.size(), 5U);
    326     BOOST_CHECK(solutions[0] == std::vector<unsigned char>({2}));
    327     BOOST_CHECK(solutions[1] == ToByteVector(pubkeys[0]));
    328@@ -94,13 +94,13 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    329         std::vector<unsigned char>({0}) <<
    330         std::vector<unsigned char>({75}) <<
    331         std::vector<unsigned char>({255});
    332-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NULL_DATA);
    333+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NULL_DATA);
    334     BOOST_CHECK_EQUAL(solutions.size(), 0U);
    335 
    336     // TxoutType::WITNESS_V0_KEYHASH
    337     s.clear();
    338     s << OP_0 << ToByteVector(pubkeys[0].GetID());
    339-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_V0_KEYHASH);
    340+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_V0_KEYHASH);
    341     BOOST_CHECK_EQUAL(solutions.size(), 1U);
    342     BOOST_CHECK(solutions[0] == ToByteVector(pubkeys[0].GetID()));
    343 
    344@@ -111,21 +111,21 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    345 
    346     s.clear();
    347     s << OP_0 << ToByteVector(scriptHash);
    348-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_V0_SCRIPTHASH);
    349+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_V0_SCRIPTHASH);
    350     BOOST_CHECK_EQUAL(solutions.size(), 1U);
    351     BOOST_CHECK(solutions[0] == ToByteVector(scriptHash));
    352 
    353     // TxoutType::WITNESS_V1_TAPROOT
    354     s.clear();
    355     s << OP_1 << ToByteVector(uint256::ZERO);
    356-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_V1_TAPROOT);
    357+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_V1_TAPROOT);
    358     BOOST_CHECK_EQUAL(solutions.size(), 1U);
    359     BOOST_CHECK(solutions[0] == ToByteVector(uint256::ZERO));
    360 
    361     // TxoutType::WITNESS_UNKNOWN
    362     s.clear();
    363     s << OP_16 << ToByteVector(uint256::ONE);
    364-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_UNKNOWN);
    365+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_UNKNOWN);
    366     BOOST_CHECK_EQUAL(solutions.size(), 2U);
    367     BOOST_CHECK(solutions[0] == std::vector<unsigned char>{16});
    368     BOOST_CHECK(solutions[1] == ToByteVector(uint256::ONE));
    369@@ -133,7 +133,7 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    370     // TxoutType::ANCHOR
    371     s.clear();
    372     s << OP_1 << ANCHOR_BYTES;
    373-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::ANCHOR);
    374+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::ANCHOR);
    375     BOOST_CHECK(solutions.empty());
    376 
    377     // Sanity-check IsPayToAnchor
    378@@ -146,7 +146,7 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_success)
    379     // TxoutType::NONSTANDARD
    380     s.clear();
    381     s << OP_9 << OP_ADD << OP_11 << OP_EQUAL;
    382-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    383+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    384 }
    385 
    386 BOOST_AUTO_TEST_CASE(script_standard_Solver_failure)
    387@@ -160,67 +160,67 @@ BOOST_AUTO_TEST_CASE(script_standard_Solver_failure)
    388     // TxoutType::PUBKEY with incorrectly sized pubkey
    389     s.clear();
    390     s << std::vector<unsigned char>(30, 0x01) << OP_CHECKSIG;
    391-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    392+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    393 
    394     // TxoutType::PUBKEYHASH with incorrectly sized key hash
    395     s.clear();
    396     s << OP_DUP << OP_HASH160 << ToByteVector(pubkey) << OP_EQUALVERIFY << OP_CHECKSIG;
    397-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    398+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    399 
    400     // TxoutType::SCRIPTHASH with incorrectly sized script hash
    401     s.clear();
    402     s << OP_HASH160 << std::vector<unsigned char>(21, 0x01) << OP_EQUAL;
    403-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    404+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    405 
    406     // TxoutType::MULTISIG 0/2
    407     s.clear();
    408     s << OP_0 << ToByteVector(pubkey) << OP_1 << OP_CHECKMULTISIG;
    409-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    410+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    411 
    412     // TxoutType::MULTISIG 2/1
    413     s.clear();
    414     s << OP_2 << ToByteVector(pubkey) << OP_1 << OP_CHECKMULTISIG;
    415-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    416+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    417 
    418     // TxoutType::MULTISIG n = 2 with 1 pubkey
    419     s.clear();
    420     s << OP_1 << ToByteVector(pubkey) << OP_2 << OP_CHECKMULTISIG;
    421-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    422+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    423 
    424     // TxoutType::MULTISIG n = 1 with 0 pubkeys
    425     s.clear();
    426     s << OP_1 << OP_1 << OP_CHECKMULTISIG;
    427-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    428+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    429 
    430     // TxoutType::NULL_DATA with other opcodes
    431     s.clear();
    432     s << OP_RETURN << std::vector<unsigned char>({75}) << OP_ADD;
    433-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    434+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    435 
    436     // TxoutType::WITNESS_V0_{KEY,SCRIPT}HASH with incorrect program size (-> consensus-invalid, i.e. non-standard)
    437     s.clear();
    438     s << OP_0 << std::vector<unsigned char>(19, 0x01);
    439-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::NONSTANDARD);
    440+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::NONSTANDARD);
    441 
    442     // TxoutType::WITNESS_V1_TAPROOT with incorrect program size (-> undefined, but still policy-valid)
    443     s.clear();
    444     s << OP_1 << std::vector<unsigned char>(31, 0x01);
    445-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_UNKNOWN);
    446+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_UNKNOWN);
    447     s.clear();
    448     s << OP_1 << std::vector<unsigned char>(33, 0x01);
    449-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_UNKNOWN);
    450+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_UNKNOWN);
    451 
    452     // TxoutType::ANCHOR but wrong witness version
    453     s.clear();
    454     s << OP_2 << ANCHOR_BYTES;
    455     BOOST_CHECK(!s.IsPayToAnchor());
    456-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_UNKNOWN);
    457+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_UNKNOWN);
    458 
    459     // TxoutType::ANCHOR but wrong 2-byte data push
    460     s.clear();
    461     s << OP_1 << std::vector<unsigned char>{0xff, 0xff};
    462     BOOST_CHECK(!s.IsPayToAnchor());
    463-    BOOST_CHECK_EQUAL(Solver(s, solutions), TxoutType::WITNESS_UNKNOWN);
    464+    BOOST_CHECK_EQUAL(Solver(s, &solutions), TxoutType::WITNESS_UNKNOWN);
    465 }
    466 
    467 BOOST_AUTO_TEST_CASE(script_standard_ExtractDestination)
    468diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp
    469index fc4550bb7b..4e4dbd5f89 100644
    470--- a/src/test/script_tests.cpp
    471+++ b/src/test/script_tests.cpp
    472@@ -1157,13 +1157,6 @@ BOOST_AUTO_TEST_CASE(script_CHECKMULTISIG23)
    473     BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_INVALID_STACK_OPERATION, ScriptErrorString(err));
    474 }
    475 
    476-/** Return the TxoutType of a script without exposing Solver details. */
    477-static TxoutType GetTxoutType(const CScript& output_script)
    478-{
    479-    std::vector<std::vector<uint8_t>> unused;
    480-    return Solver(output_script, unused);
    481-}
    482-
    483 #define CHECK_SCRIPT_STATIC_SIZE(script, expected_size)                   \
    484     do {                                                                  \
    485         BOOST_CHECK_EQUAL((script).size(), (expected_size));              \
    486@@ -1193,14 +1186,14 @@ BOOST_AUTO_TEST_CASE(script_size_and_capacity_test)
    487     // Small OP_RETURN has direct allocation
    488     {
    489         const auto script{CScript() << OP_RETURN << std::vector<uint8_t>(10, 0xaa)};
    490-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::NULL_DATA);
    491+        BOOST_CHECK_EQUAL(Solver(script), TxoutType::NULL_DATA);
    492         CHECK_SCRIPT_STATIC_SIZE(script, 12);
    493     }
    494 
    495     // P2WPKH has direct allocation
    496     {
    497         const auto script{GetScriptForDestination(WitnessV0KeyHash{PKHash{dummy_pubkey}})};
    498-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::WITNESS_V0_KEYHASH);
    499+        BOOST_CHECK_EQUAL(Solver(script), TxoutType::WITNESS_V0_KEYHASH);
    500         CHECK_SCRIPT_STATIC_SIZE(script, 22);
    501     }
    502 
    503@@ -1214,7 +1207,7 @@ BOOST_AUTO_TEST_CASE(script_size_and_capacity_test)
    504     // P2PKH has direct allocation
    505     {
    506         const auto script{GetScriptForDestination(PKHash{dummy_pubkey})};
    507-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::PUBKEYHASH);
    508+        BOOST_CHECK_EQUAL(Solver(script), TxoutType::PUBKEYHASH);
    509         CHECK_SCRIPT_STATIC_SIZE(script, 25);
    510     }
    511 
    512@@ -1228,14 +1221,14 @@ BOOST_AUTO_TEST_CASE(script_size_and_capacity_test)
    513     // P2TR has direct allocation
    514     {
    515         const auto script{GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey{dummy_pubkey}})};
    516-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::WITNESS_V1_TAPROOT);
    517+        BOOST_CHECK_EQUAL(Solver(script), TxoutType::WITNESS_V1_TAPROOT);
    518         CHECK_SCRIPT_STATIC_SIZE(script, 34);
    519     }
    520 
    521     // Compressed P2PK has direct allocation
    522     {
    523         const auto script{GetScriptForRawPubKey(dummy_pubkey)};
    524-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::PUBKEY);
    525+        BOOST_CHECK_EQUAL(Solver(script), TxoutType::PUBKEY);
    526         CHECK_SCRIPT_STATIC_SIZE(script, 35);
    527     }
    528 
    529@@ -1246,14 +1239,14 @@ BOOST_AUTO_TEST_CASE(script_size_and_capacity_test)
    530         const CPubKey uncompressed_pubkey{uncompressed_key.GetPubKey()};
    531 
    532         const auto script{GetScriptForRawPubKey(uncompressed_pubkey)};
    533-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::PUBKEY);
    534+        BOOST_CHECK_EQUAL(Solver(script), TxoutType::PUBKEY);
    535         CHECK_SCRIPT_DYNAMIC_SIZE(script, 67, 67);
    536     }
    537 
    538     // Bare multisig needs extra allocation
    539     {
    540         const auto script{GetScriptForMultisig(1, std::vector{2, dummy_pubkey})};
    541-        BOOST_CHECK_EQUAL(GetTxoutType(script), TxoutType::MULTISIG);
    542+        BOOST_CHECK_EQUAL(Solver(script), TxoutType::MULTISIG);
    543         CHECK_SCRIPT_DYNAMIC_SIZE(script, 71, 103);
    544     }
    545 }
    546diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp
    547index b2a70057d5..47f42fb231 100644
    548--- a/src/test/transaction_tests.cpp
    549+++ b/src/test/transaction_tests.cpp
    550@@ -1125,7 +1125,6 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    551     CMutableTransaction tx_create{}, tx_spend{};
    552     tx_create.vout.emplace_back(0, CScript{});
    553     tx_spend.vin.emplace_back(Txid{}, 0);
    554-    std::vector<std::vector<uint8_t>> sol_dummy;
    555 
    556     // CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash,
    557     // WitnessV1Taproot, PayToAnchor, WitnessUnknown.
    558@@ -1135,14 +1134,14 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    559 
    560     // P2PK
    561     tx_create.vout[0].scriptPubKey = GetScriptForDestination(PubKeyDestination{pubkey});
    562-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::PUBKEY);
    563+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::PUBKEY);
    564     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    565     AddCoins(coins, CTransaction{tx_create}, 0, false);
    566     BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    567 
    568     // P2PKH
    569     tx_create.vout[0].scriptPubKey = GetScriptForDestination(PKHash{pubkey});
    570-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::PUBKEYHASH);
    571+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::PUBKEYHASH);
    572     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    573     AddCoins(coins, CTransaction{tx_create}, 0, false);
    574     BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    575@@ -1150,7 +1149,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    576     // P2SH
    577     auto redeem_script{CScript{} << OP_1 << OP_CHECKSIG};
    578     tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash{redeem_script});
    579-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
    580+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::SCRIPTHASH);
    581     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    582     tx_spend.vin[0].scriptSig = CScript{} << OP_0 << ToByteVector(redeem_script);
    583     AddCoins(coins, CTransaction{tx_create}, 0, false);
    584@@ -1160,7 +1159,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    585     // native P2WSH
    586     const auto witness_script{CScript{} << OP_12 << OP_HASH160 << OP_DUP << OP_EQUAL};
    587     tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV0ScriptHash{witness_script});
    588-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V0_SCRIPTHASH);
    589+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::WITNESS_V0_SCRIPTHASH);
    590     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    591     AddCoins(coins, CTransaction{tx_create}, 0, false);
    592     BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    593@@ -1168,7 +1167,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    594     // P2SH-wrapped P2WSH
    595     redeem_script = tx_create.vout[0].scriptPubKey;
    596     tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
    597-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
    598+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::SCRIPTHASH);
    599     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    600     tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
    601     AddCoins(coins, CTransaction{tx_create}, 0, false);
    602@@ -1178,7 +1177,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    603 
    604     // native P2WPKH
    605     tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV0KeyHash{pubkey});
    606-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V0_KEYHASH);
    607+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::WITNESS_V0_KEYHASH);
    608     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    609     AddCoins(coins, CTransaction{tx_create}, 0, false);
    610     BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    611@@ -1186,7 +1185,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    612     // P2SH-wrapped P2WPKH
    613     redeem_script = tx_create.vout[0].scriptPubKey;
    614     tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
    615-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
    616+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::SCRIPTHASH);
    617     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    618     tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
    619     AddCoins(coins, CTransaction{tx_create}, 0, false);
    620@@ -1196,7 +1195,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    621 
    622     // P2TR
    623     tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessV1Taproot{XOnlyPubKey{pubkey}});
    624-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_V1_TAPROOT);
    625+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::WITNESS_V1_TAPROOT);
    626     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    627     AddCoins(coins, CTransaction{tx_create}, 0, false);
    628     BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    629@@ -1204,7 +1203,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    630     // P2SH-wrapped P2TR (undefined, non-standard)
    631     redeem_script = tx_create.vout[0].scriptPubKey;
    632     tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
    633-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
    634+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::SCRIPTHASH);
    635     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    636     tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
    637     AddCoins(coins, CTransaction{tx_create}, 0, false);
    638@@ -1214,7 +1213,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    639 
    640     // P2A
    641     tx_create.vout[0].scriptPubKey = GetScriptForDestination(PayToAnchor{});
    642-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::ANCHOR);
    643+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::ANCHOR);
    644     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    645     AddCoins(coins, CTransaction{tx_create}, 0, false);
    646     BOOST_CHECK(!::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    647@@ -1222,7 +1221,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    648     // P2SH-wrapped P2A (undefined, non-standard)
    649     redeem_script = tx_create.vout[0].scriptPubKey;
    650     tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
    651-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
    652+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::SCRIPTHASH);
    653     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    654     tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
    655     AddCoins(coins, CTransaction{tx_create}, 0, false);
    656@@ -1231,7 +1230,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    657 
    658     // Undefined version 1 witness program
    659     tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessUnknown{1, {0x42, 0x42}});
    660-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_UNKNOWN);
    661+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::WITNESS_UNKNOWN);
    662     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    663     AddCoins(coins, CTransaction{tx_create}, 0, false);
    664     BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    665@@ -1239,7 +1238,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    666     // P2SH-wrapped undefined version 1 witness program
    667     redeem_script = tx_create.vout[0].scriptPubKey;
    668     tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
    669-    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
    670+    BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::SCRIPTHASH);
    671     tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    672     tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
    673     AddCoins(coins, CTransaction{tx_create}, 0, false);
    674@@ -1251,7 +1250,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    675     const auto program{ToByteVector(XOnlyPubKey{pubkey})};
    676     for (int i{2}; i <= 16; ++i) {
    677         tx_create.vout[0].scriptPubKey = GetScriptForDestination(WitnessUnknown{i, program});
    678-        BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::WITNESS_UNKNOWN);
    679+        BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::WITNESS_UNKNOWN);
    680         tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    681         AddCoins(coins, CTransaction{tx_create}, 0, false);
    682         BOOST_CHECK(::SpendsNonAnchorWitnessProg(CTransaction{tx_spend}, coins));
    683@@ -1259,7 +1258,7 @@ BOOST_AUTO_TEST_CASE(spends_witness_prog)
    684         // It's also detected within P2SH.
    685         redeem_script = tx_create.vout[0].scriptPubKey;
    686         tx_create.vout[0].scriptPubKey = GetScriptForDestination(ScriptHash(redeem_script));
    687-        BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey, sol_dummy), TxoutType::SCRIPTHASH);
    688+        BOOST_CHECK_EQUAL(Solver(tx_create.vout[0].scriptPubKey), TxoutType::SCRIPTHASH);
    689         tx_spend.vin[0].prevout.hash = tx_create.GetHash();
    690         tx_spend.vin[0].scriptSig = CScript{} << ToByteVector(redeem_script);
    691         AddCoins(coins, CTransaction{tx_create}, 0, false);
    692diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp
    693index a04e0903b9..0ddee1fa49 100644
    694--- a/src/wallet/rpc/addresses.cpp
    695+++ b/src/wallet/rpc/addresses.cpp
    696@@ -267,7 +267,7 @@ public:
    697     {
    698         // Always present: script type and redeemscript
    699         std::vector<std::vector<unsigned char>> solutions_data;
    700-        TxoutType which_type = Solver(subscript, solutions_data);
    701+        TxoutType which_type = Solver(subscript, &solutions_data);
    702         obj.pushKV("script", GetTxnOutputType(which_type));
    703         obj.pushKV("hex", HexStr(subscript));
    704 
    705diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
    706index a78f32f890..2f23861259 100644
    707--- a/src/wallet/scriptpubkeyman.cpp
    708+++ b/src/wallet/scriptpubkeyman.cpp
    709@@ -86,7 +86,7 @@ IsMineResult LegacyWalletIsMineInnerDONOTUSE(const LegacyDataSPKM& keystore, con
    710     IsMineResult ret = IsMineResult::NO;
    711 
    712     std::vector<valtype> vSolutions;
    713-    TxoutType whichType = Solver(scriptPubKey, vSolutions);
    714+    TxoutType whichType = Solver(scriptPubKey, &vSolutions);
    715 
    716     CKeyID keyID;
    717     switch (whichType) {
    718@@ -350,7 +350,7 @@ bool LegacyDataSPKM::LoadWatchOnly(const CScript &dest)
    719 static bool ExtractPubKey(const CScript &dest, CPubKey& pubKeyOut)
    720 {
    721     std::vector<std::vector<unsigned char>> solutions;
    722-    return Solver(dest, solutions) == TxoutType::PUBKEY &&
    723+    return Solver(dest, &solutions) == TxoutType::PUBKEY &&
    724         (pubKeyOut = CPubKey(solutions[0])).IsFullyValid();
    725 }
    726 
    727@@ -1346,7 +1346,7 @@ std::optional<PSBTError> DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTran
    728 
    729             // Taproot output pubkey
    730             std::vector<std::vector<unsigned char>> sols;
    731-            if (Solver(script, sols) == TxoutType::WITNESS_V1_TAPROOT) {
    732+            if (Solver(script, &sols) == TxoutType::WITNESS_V1_TAPROOT) {
    733                 sols[0].insert(sols[0].begin(), 0x02);
    734                 pubkeys.emplace_back(sols[0]);
    735                 sols[0][0] = 0x03;
    736diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
    737index 146fb49ea7..b36925381a 100644
    738--- a/src/wallet/spend.cpp
    739+++ b/src/wallet/spend.cpp
    740@@ -452,7 +452,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
    741 
    742         // Obtain script type
    743         std::vector<std::vector<uint8_t>> script_solutions;
    744-        TxoutType type = Solver(output.scriptPubKey, script_solutions);
    745+        TxoutType type = Solver(output.scriptPubKey, &script_solutions);
    746 
    747         // If the output is P2SH and solvable, we want to know if it is
    748         // a P2SH (legacy) or one of P2SH-P2WPKH, P2SH-P2WSH (P2SH-Segwit). We can determine
    749@@ -462,7 +462,7 @@ CoinsResult AvailableCoins(const CWallet& wallet,
    750         if (type == TxoutType::SCRIPTHASH && solvable) {
    751             CScript script;
    752             if (!provider->GetCScript(CScriptID(uint160(script_solutions[0])), script)) continue;
    753-            type = Solver(script, script_solutions);
    754+            type = Solver(script, &script_solutions);
    755             is_from_p2sh = true;
    756         }
    

    Concept ACK. if you decide to use my alternative, please split it into smaller, focused commits.

  18. Raimo33 commented at 12:54 pm on October 23, 2025: contributor

    You’re right, vSolutions is completely unnecessary in AreInputsStandard(). I’m not a fan of raw pointers but in this case it seems the best practical solution overall. smart pointers or optionals don’t fit quite well.

    I’ll use your commits with small tweaks and add you as co-author for now

  19. Raimo33 renamed this:
    refactor: optimize: reuse containers in transaction policy verification
    refactor: optimize: avoid allocations in script & policy verification
    on Oct 23, 2025
  20. Raimo33 force-pushed on Oct 23, 2025
  21. Raimo33 commented at 2:55 pm on October 23, 2025: contributor

    I’ve found other unnecessary related memory allocations. Instead of using @l0rinc ’s approach of modifying the already existing methods (adding branch complexity) I opted for defining new methods. While this increases redundancy between similar methods, it produces a smaller diff and cleaner separate paths.

    performance has further improved on my end. showing +15% for CCoinsCaching

  22. l0rinc commented at 3:03 pm on October 23, 2025: contributor
    I deliberately avoided duplication, not sure why you think that’s simpler
  23. Raimo33 commented at 3:10 pm on October 23, 2025: contributor

    I deliberately avoided duplication, not sure why you think that’s simpler

    • wether the diff is simpler is debatable.
    • I argue that the separation of concerns makes both Solver and GetScriptPubKeyType more readable
    • theoretically the duplication approach is faster, avoiding branches that check if vSolutions is null
  24. l0rinc commented at 7:55 pm on October 23, 2025: contributor
    Approach NACK, you’re needlessly modifying critical code and needlessly duplicating existing tests for them.
  25. Raimo33 marked this as a draft on Oct 24, 2025
  26. Raimo33 force-pushed on Oct 24, 2025
  27. Raimo33 force-pushed on Oct 24, 2025
  28. Raimo33 force-pushed on Oct 24, 2025
  29. refactor: use range-based iteration 3261ed6f48
  30. refactor: optimize: reuse container across multiple iterations 3d2ed792fa
  31. refactor: avoid allocations by assigning in place 79763634a6
  32. Raimo33 force-pushed on Oct 24, 2025
  33. Raimo33 commented at 12:59 pm on October 24, 2025: contributor

    I’ve noticed that the CCoinsCaching benchmark is a bit deceiving. What we are seeing in this PR is not a speedup in actual coin caching, but rather a speedup in AreInputsStandard()

     0static void CCoinsCaching(benchmark::Bench& bench)
     1{
     2    ECC_Context ecc_context{};
     3
     4    FillableSigningProvider keystore;
     5    CCoinsView coinsDummy;
     6    CCoinsViewCache coins(&coinsDummy);
     7    std::vector<CMutableTransaction> dummyTransactions =
     8        SetupDummyInputs(keystore, coins, {11 * COIN, 50 * COIN, 21 * COIN, 22 * COIN});
     9
    10    CMutableTransaction t1;
    11    t1.vin.resize(3);
    12    t1.vin[0].prevout.hash = dummyTransactions[0].GetHash();
    13    t1.vin[0].prevout.n = 1;
    14    t1.vin[0].scriptSig << std::vector<unsigned char>(65, 0);
    15    t1.vin[1].prevout.hash = dummyTransactions[1].GetHash();
    16    t1.vin[1].prevout.n = 0;
    17    t1.vin[1].scriptSig << std::vector<unsigned char>(65, 0) << std::vector<unsigned char>(33, 4);
    18    t1.vin[2].prevout.hash = dummyTransactions[1].GetHash();
    19    t1.vin[2].prevout.n = 1;
    20    t1.vin[2].scriptSig << std::vector<unsigned char>(65, 0) << std::vector<unsigned char>(33, 4);
    21    t1.vout.resize(2);
    22    t1.vout[0].nValue = 90 * COIN;
    23    t1.vout[0].scriptPubKey << OP_1;
    24
    25    // Benchmark.
    26    const CTransaction tx_1(t1);
    27    bench.run([&] {
    28        bool success{AreInputsStandard(tx_1, coins)};
    29        assert(success);
    30    });
    31}
    
  34. Raimo33 force-pushed on Oct 24, 2025
  35. Raimo33 force-pushed on Oct 24, 2025
  36. Raimo33 force-pushed on Oct 24, 2025
  37. Raimo33 force-pushed on Oct 24, 2025
  38. l0rinc commented at 3:14 pm on October 24, 2025: contributor
    Please only push after you’re actually done, everyone gets a notification for each push, and people will just simply unsubscribe from this thread otherwise… Also, please revert unrelated refactorings, the change is just getting more complicated after the simplification requests.
  39. cedwies commented at 3:14 pm on October 24, 2025: none

    I strongly suggest splitting this draft into smaller, more focused PRs (or separate drafts), each exploring one idea at a time.

    The current draft mixes several independent changes (policy loop optimizations, Solver/IsWitnessProgram API changes, and stylistic updates), which makes it harder to benchmark and review effectively.

    Iโ€™m happy to follow along and review, but it would be great to have clear, targeted benchmarks for future iterations (ones that measure the actual code (paths) being changed, so that we can make solid, data-driven decisions about which changes are worth merging.

    For example, regarding:

    “Disagree for witnessprogram: it is used in the hot path (P2WPKH is the most likely)”

    I think whether itโ€™s on the hot path or not is less important than having data to show a measurable gain. The benchmarks did not show a clear performance benefit. The performance benefit (AFAIK) came from hoisting vSolutions (even though we later found out vSolutions is not needed in that code). I prefer we do not simply hoist vectors (or similar) because we think/assume this improves performance, let’s confirm with targeted measurements first.

  40. refactor: replace `push()` with `emplace()` to avoid temporary object construction 996acdab33
  41. Raimo33 force-pushed on Oct 26, 2025
  42. Raimo33 commented at 1:57 pm on October 26, 2025: contributor

    As suggested, I’ve removed some commits to keep this PR simple. I’ll open a follow up PR with the more complex but related commits that change the API.

    As per the benchmarks, there currently is no benchmark that realistically measures the impacted methods, and I’m afraid I’ll not be able to code one. we’d need to measure AreInputsStandard(), IsWitnessStandard() and Solver() which depend on too many variables.

  43. Raimo33 marked this as ready for review on Oct 28, 2025
  44. in src/policy/policy.cpp:289 in 996acdab33
    285@@ -285,21 +286,21 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs)
    286         }
    287 
    288         int witnessversion = 0;
    289-        std::vector<unsigned char> witnessprogram;
    290+        witnessprogram.clear();
    


    ajtowns commented at 3:30 pm on October 28, 2025:
    Would be better to have IsWitnessProgram() populate a span rather than a vector if we’re trying to minimise allocations, as far as I can see.

    Raimo33 commented at 3:39 pm on October 28, 2025:
    here the vector is totally useless. it’s required by the IsWitnessProgram() API but not actually needed. I’ve made more significant changes on this private branch, for another PR: https://github.com/Raimo33/bitcoinknots/commits/optimize-tx-policy-verification-2/

    l0rinc commented at 4:54 pm on October 31, 2025:
    I don’t like the reuses, seems like a workaround instead of a fix, pushed an alternative to #33757, added @Raimo33 as a coauthor

    Raimo33 commented at 5:00 pm on October 31, 2025:
    what about the other commits? @l0rinc. there are some intresting zero-cost optimizations. maybe not worth for a PR alone, but could be included as small refactorings in bigger PRs.
  45. l0rinc referenced this in commit cbc3821fc8 on Oct 31, 2025
  46. l0rinc referenced this in commit 2726abd896 on Oct 31, 2025

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-11-02 12:13 UTC

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