Implement BIP 54 (Consensus Cleanup) without mainnet activation #35793

pull darosior wants to merge 20 commits into bitcoin:master from darosior:bip54 changing 37 files +6569 −114
  1. darosior commented at 2:18 PM on July 24, 2026: member

    This implements the Consensus Cleanup validation rules proposed in BIP 54. These rules are only enabled on regtest. Mainnet activation, if any, is to be considered separately.

    This patchset is based on the code that was previously reviewed (1, 2) and tested (for instance here) on Bitcoin Inquisition. The tests and documentation have since been improved, but the consensus-critical commits have been carried over with only minor differences (the only behavioural one being the addition of the stripped-size check to PreChecks).

    Roughly 95% of the added lines are tests or test data. The format, contents, and reproduction procedure for the BIP 54 test vectors are documented here. The bip54_tests unit test module exercises each mitigation extensively in isolation. The feature_bip54.py functional test verifies all of them end-to-end after and prior to activation. For the timestamp rules, it simulates timewarp and Murch–Zawy attacks, demonstrating the new timestamp rules prevent these exploits. A fuzz harness for the sigop accounting logic is also included, which can be seeded from the BIP test vectors (implemented here).

    See commit messages for details.

  2. DrahtBot commented at 2:18 PM on July 24, 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/35793.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Concept ACK dergoegge, fanquake, polespinasa, theStack, hsjoberg, fjahr, stickies-v

    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:

    • #35852 (scripted-diff: Use inline const(expr) over static constexpr in headers by maflcko)
    • #35657 (refactor: avoid redundant input lookup in CheckTxInputs by arejula27)
    • #35570 (refactor: Change some validation.cpp methods to return BlockValidationState by optout21)
    • #35569 (Encapsulation for CTransaction by purpleKarrot)
    • #35301 (Silent Payments: Implement bip352 (take 2) by Eunovo)
    • #34864 (coins: tighten cache entry state invariants by l0rinc)
    • #32729 (test,refactor: extract script template helpers and expand sigop coverage by l0rinc)
    • #32468 (rpc: generateblock to allow multiple outputs by polespinasa)
    • #29491 ([EXPERIMENTAL] Schnorr batch verification for blocks by fjahr)
    • #28690 (build: Introduce internal kernel library by sedited)

    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-->

  3. chainparams: add versionbits deployment for BIP 54 4a6b1c6ffb
  4. scripted-diff: rename BIP54-sigops constants to MAX_TX_BIP54_SIGOPS
    BIP54 counts sigops differently from existing sigops-based checks. Since
    we are overloading the sigops term, make clear the constant refers to
    BIP54-sigops, not other kinds of pre-existing sigops.
    
    Also update the functional test framework's constant that has
    "standardness" in its name, since we are about to make it consensus
    critical (func test scripted-diff courtesy of Anthony Towns).
    
    -BEGIN VERIFY SCRIPT-
    sed -i 's/MAX_TX_LEGACY_SIGOPS/MAX_TX_BIP54_SIGOPS/g' $(git grep -l MAX_TX_LEGACY_SIGOPS src/)
    sed -i 's/MAX_STD_LEGACY_SIGOPS/MAX_TX_BIP54_SIGOPS/g' $(git grep -l MAX_STD_LEGACY_SIGOPS)
    sed -i 's/signature operations in validating a transaction./signature operations in a single transaction, per BIP54./' test/functional/test_framework/script_util.py
    -END VERIFY SCRIPT-
    
    Co-Authored-by: Anthony Towns <aj@erisian.com.au>
    9fb74e75d0
  5. moveonly: move CheckSigopsBIP54 from policy to consensus
    Move the function that checks whether a transaction respects the BIP54 sigops rule to the
    consensus folder (along with the accompanying constant), as it will be made consensus-critical
    in the next commit. Can be reviewed with git's --color-moved option.
    9ccf729e75
  6. validation: make BIP54 sigops check consensus-critical
    When BIP54 is active, enforce that block transactions do not violate the BIP54 limit on the
    number of legacy sigops present in Scripts that get executed during block validation.
    39cca6c97e
  7. qa: add to utilities a version of SignSignature for Taproot inputs
    In Taproot the signature commits to the list of spent outputs.
    16d8f7c502
  8. darosior force-pushed on Jul 24, 2026
  9. qa: extensive unit tests for BIP54 legacy sigops limit
    Test the newly introduced limit with various combinations of inputs and outputs types,
    historical transactions, and exercise some implementation-specific edge cases. Record
    each test case and optionally write them to disk as JSON to generate the BIP test vectors.
    d75143d393
  10. fuzz: add a fuzz target for the BIP54 sigops check
    The fuzz target was specifically crafted to support seeding it with the BIP54 test vectors
    generated by the unit test in the previous commit.
    243e7a7623
  11. scripted-diff: rename testnet4 timewarp constant
    We are going to introduce the timewarp fix for mainnet with a greater grace period. Rename
    the MAX_TIMEWARP value for testnet to differentiate them.
    
    -BEGIN VERIFY SCRIPT-
    
    for f in $(git grep -l MAX_TIMEWARP); do sed -i "s/MAX_TIMEWARP/MAX_TIMEWARP_TESTNET4/g" "$f"; done
    
    -END VERIFY SCRIPT-
    43f1b9471b
  12. miner: update a timewarp comment to refer specifically to BIP 54 6a10f77ef5
  13. validation: prevent timewarp attacks with a 2h grace period d96b2c79dc
  14. validation: prevent negative difficulty adjustment intervals 8795625242
  15. qa: BIP54 test vectors for timewarp and Murch-Zawy
    Documentation about the test vectors, including about their structure and content, as well as
    reproduction instructions, is available here: https://github.com/bitcoin/bips/tree/master/bip-0054/test_vectors
    da0b9ecf05
  16. validation: enforce that coinbase transactions are timelocked to block height
    When BIP 54 is active, coinbase transactions must have their nLockTime field set to the block height
    minus 1 (since it encodes the last height at which the transaction is invalid), and their nSequence
    field may be anything but the maximum value (which indicates "final", bypassing timelock
    validation).
    87b003f19d
  17. qa: BIP54 test vectors for restrictions on coinbase transactions
    Documentation about the test vectors' structure and content, as well as instructions for generating
    them is available at https://github.com/bitcoin/bips/tree/master/bip-0054/test_vectors .
    534d4766d1
  18. Avoid creating <= 64-byte transactions in most functional tests. e5c1b4aae9
  19. [test] Separate 64B and 63B tx size tests 32bfb7b6e1
  20. validation: make 64-byte transactions invalid
    64-byte transactions are also now treated as a consensus failure in
    PreChecks, like BIP54-sigops check failures. Note this only changes the
    RPC error, and not the disconnection behaviour in P2P since
    266dd0e10d08c0bfde63205db15d6c210a021b90.
    b1ec5a6f4f
  21. qa: unit tests for BIP54 rule on 64-byte transactions (with JSON test vectors)
    This adds tests exercising the bounds of the checks on the invalid transaction size, for various
    types of transactions (legacy, Segwit, bytes in input/output to get to 64 bytes) as well as
    sanity checking against some known historical violations.
    
    Thanks to Chris Stewart for digging up the historical violations to this rule.
    0719bf5164
  22. qa: end-to-end test all BIP54 mitigations
    The previously introduced unit tests extensively test the specific implementation of each
    mitigation. This functional test complements them by end-to-end testing all mitigations.
    For the added timestamp constraints, it mimicks how they would get exploited (by implementing pseudo
    timewarp and Murch-Zawy attacks) and demonstrates those exploits are not possible anymore after
    BIP54 activates.
    0acde965fa
  23. doc: add a BIP 54 entry to bips.md 9630491bf2
  24. DrahtBot added the label CI failed on Jul 24, 2026
  25. DrahtBot commented at 2:43 PM on July 24, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task macOS native: https://github.com/bitcoin/bitcoin/actions/runs/30100472888/job/89504946717</sub> <sub>LLM reason (✨ experimental): CI failed because bip54_tests aborted on an assertion failure in CExtKey::SetSeed (seed.size() was outside the expected 16–64 byte range) during bip54_legacy_sigops.</sub>

    <details><summary>Hints</summary>

    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.

    </details>

  26. DrahtBot removed the label CI failed on Jul 24, 2026
  27. Christewart commented at 5:49 PM on July 24, 2026: contributor

    Congrats! Excited to see this work make it to this point 🎉

  28. in src/consensus/consensus.h:44 in 9630491bf2
      40 |   */
      41 | -static constexpr int64_t MAX_TIMEWARP = 600;
      42 | +static constexpr int64_t MAX_TIMEWARP_TESTNET4 = 600;
      43 | +
      44 | +/** The maximum number of potentially executed legacy signature operations in a single tx */
      45 | +static constexpr unsigned int MAX_TX_BIP54_SIGOPS{2'500};
    


    ariard commented at 2:38 AM on July 29, 2026:

    there could be a definition of what is understood as a "legacy signature ops" even if it's just echoing the bip doc, it's consensus/consensus.h where one can reasonably expect to find the definition of a "legacy signature" in opposition to signatures included in the witness for SegWit spends.


    darosior commented at 8:22 PM on July 31, 2026:

    Ok, i can replace with "non-Segwit signature operations". I've also checked all other instances of "legacy" in the diff here, and it's only used in tests, where the term is already present and i think fine to use.

  29. in src/consensus/tx_verify.cpp:176 in 9630491bf2
     172 | +
     173 | +        // Unlike the existing block wide sigop limit which counts sigops present in the block
     174 | +        // itself (including the scriptPubKey which is not executed until spending later), BIP54
     175 | +        // counts sigops in the block where they are potentially executed (only).
     176 | +        // This means sigops in the spent scriptPubKey count toward the limit.
     177 | +        // `fAccurate` means correctly accounting sigops for CHECKMULTISIGs(VERIFY) with 16 pubkeys
    


    ariard commented at 2:49 AM on July 29, 2026:

    i was reading again the BIP part on the new CheckSigops limit and the computation price for each type of legacy checksig operations, i.e CHECKSIG counts as 1 signature operation, CHECKMULTISIG as 1 to 16 signature operation and (empty) CHECKMULTISIG as 20 signatures operation.

    the asymmetry between OP_1 CHECKMULTISIG and (empty) CHECKMULTISIG sounds striking as the vbyte cost of a single CHECKMULTISIG is going to be 1 vbyte and the fee cost of a OP_1 CHECKMULTISIG, so in any collaborative txn among multi parties, one can force the tx to reach the new 2500 limit at a vbyte cost inferior rather than putting a OP_1 CHECKMULTISIG coming with a higher absolute vbyte cost, whatever the feerate.

    curious if there is any rational somewhere in all the design notes, that's explaining that choice for the asymmetry in the legacy signature accounting for CHECKMULTISIG


    darosior commented at 9:24 PM on August 4, 2026:

    Absolutely, the rationale is to reuse BIP 16 accounting, instead of introducing another opcode counting method in consensus rules.


    ariard commented at 2:25 AM on August 8, 2026:

    @darosior So I'm saying this because in fact before the fix for this class of vulnerabilities: https://bitcoincore.org/en/2025/10/24/disclose-cve-2025-46598/ one could have abuse the new 2500 sigops rule, at a lower marginal feerate cost.

    Before #33050 a tx could have been executed twice, the first with standardness rule, the second with consensus check. An attacker could have done a marginal attack, where the tx would have had a huge OP_SHA256 element to hash and the minimal number of accounted for 20 OP_CHECKMULTISIG.

    Therefore the transaction would have failed a first time on the standard check and then a second time on the consensus check with a reduced liquidity cost for the attacker (due to the lower feerate surface). and there was easy way to optimize the attack.

    This one of the attack I did mention there even if never went to communicate it properly. I might have still have the python test vectors I played with at the time. Somehow the 20 OP_CHECKMULTISIG is a compact trick to render a tx invalid with the minimal of tx surface.


    darosior commented at 6:38 PM on August 10, 2026:

    I benchmarked the worst case processing time for various types of standard transactions back when i disclosed CVE-2025-46598. As far as i know the worst case for legacy transaction does not involve OP_SHA256. It does involve OP_CHECKMULTISIG, but not with the minimal amount of sigop accounted for (since you need it to actually do compute).

    I am not sure what you are trying to get at. Could your point be related to this previous one you raised on the list?

  30. ariard commented at 2:50 AM on July 29, 2026: contributor

    Let's start at least the review for the fix of the long validation block time.

  31. dergoegge commented at 2:25 PM on July 29, 2026: member

    Concept ACK

  32. fanquake commented at 10:20 AM on July 30, 2026: member

    Concept ACK

  33. polespinasa commented at 10:50 AM on July 30, 2026: member

    Concept ACK

  34. theStack commented at 12:41 PM on July 30, 2026: contributor

    Concept ACK

  35. hsjoberg commented at 12:50 PM on July 30, 2026: contributor

    Concept ACK

  36. fjahr commented at 7:31 PM on August 1, 2026: contributor

    Concept ACK

  37. in src/node/miner.cpp:67 in 9630491bf2
      65 | +    // activation safer since BIP94 sets a tighter bound.
      66 |      if (height % difficulty_adjustment_interval == 0) {
      67 | -        min_time = std::max<int64_t>(min_time, pindexPrev->GetBlockTime() - MAX_TIMEWARP);
      68 | +        min_time = std::max<int64_t>(min_time, pindexPrev->GetBlockTime() - MAX_TIMEWARP_TESTNET4);
      69 |      }
      70 |      return min_time;
    


    fjahr commented at 8:36 AM on August 3, 2026:

    The miner currently does not enforce the murch-zawy rule but I think it should. Suggested change along with a test: https://github.com/fjahr/bitcoin/commit/edbc841eb546b1c1c41cc6ed16021ddd7adb7b29


    darosior commented at 2:28 PM on August 3, 2026:

    I considered it for #31376 and decided against because it's strictly theoretical. But your patch looks good. Could you PR it to master? This one is already large, and i think that commit can be merged on its own.


    fjahr commented at 1:36 PM on August 11, 2026:

    Done now here: #35949

  38. in src/kernel/chainparams.cpp:619 in 4a6b1c6ffb
     614 | +        consensus.vDeployments[Consensus::DEPLOYMENT_CONSENSUSCLEANUP].bit = 3;
     615 | +        consensus.vDeployments[Consensus::DEPLOYMENT_CONSENSUSCLEANUP].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
     616 | +        consensus.vDeployments[Consensus::DEPLOYMENT_CONSENSUSCLEANUP].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
     617 | +        consensus.vDeployments[Consensus::DEPLOYMENT_CONSENSUSCLEANUP].min_activation_height = 0; // No activation delay
     618 | +        consensus.vDeployments[Consensus::DEPLOYMENT_CONSENSUSCLEANUP].threshold = 108; // 75%
     619 | +        consensus.vDeployments[Consensus::DEPLOYMENT_CONSENSUSCLEANUP].period = 144;
    


    l0rinc commented at 9:52 PM on August 6, 2026:

    4a6b1c6 chainparams: add versionbits deployment for BIP 54:

    I understand that the BIP mentions 2016/2015 because it describes mainnet's adjustment period. Since this PR applies the deployment only to regtest by default, could we adjust the BIP text, or add a note here to clarify that these values are derived from the network's adjustment period rather than fixed constants?


    darosior commented at 7:01 PM on August 10, 2026:

    4a6b1c6 chainparams: add versionbits deployment for BIP 54:

    I think your comment applies to the difficulty adjustment period values used by the timewarp and Murch-Zawy mitigations, not that commit (which sets a largely inconsequential dummy BIP 9 deployment for regtest), correct?

    So i went and looked for a way to maybe clarify this for the code implementing those checks, but it's already pretty clear these values are derived from the network's adjustment period. Timewarp:

        // If the block is the first of a difficulty adjustment interval, check its timestamp against the previous
        // one to prevent timewarp attacks (see BIP 54).
        const int dai{static_cast<int>(consensusParams.DifficultyAdjustmentInterval())};
        const bool is_first_block{nHeight % dai == 0};
        if (is_first_block) {
    

    Murch-Zawy:

        // Fix for the Murch-Zawy attack. See https://delvingbitcoin.org/t/zawy-s-alternating-timestamp-attack/1062 .
        // TL;DR: the duration of a retarget period must not be negative or the difficulty adjustment limit may be exploited to unduly
        // reduce difficulty similarly to the timewarp vulnerability. Along with the timewarp fix, this effectively makes retarget periods
        // monotonic (modulo the timewarp fix grace period).
        const bool is_last_block{nHeight % dai == dai - 1};
        if (is_last_block && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_CONSENSUSCLEANUP)) {
            int first_height{nHeight - dai + 1};
    

    Happy to consider any improvement to the comments there.


    l0rinc commented at 9:59 PM on August 12, 2026:

    Yes, added the code comment to that line simply to demonstrate that regtest has a different difficulty adjustment period than what's documented in the BIP itself:

    Given a block at height N:

    • if N % 2016 is equal to 0, the timestamp of the block must be set to a value higher than or equal to the value of the timestamp of block at height N-1 minus 7200 (T<sub>N</sub> &ge; T<sub>N−1</sub> − 7200);
    • if N % 2016 is equal to 2015, the timestamp of the block must be set to a value higher than or equal to the value of the timestamp of the block at height N-2015 (T<sub>N</sub> &ge; T<sub>N−2015</sub>).

    The code looks fine, was just wondering if we could update the BIP to make 2016 and 2015 variables, and in this PR draw attention to the fact that - strictly speaking - we're diverging from the BIP.

  39. stickies-v commented at 5:16 PM on August 7, 2026: contributor

    Concept ACK

  40. in test/functional/feature_bip54.py:121 in 0acde965fa
     116 | +    def submit_block_many_sigops(self, node):
     117 | +        """Create and submit a block that violates the BIP54 sigops limit."""
     118 | +        txs = [tx.serialize().hex() for tx in self.create_txs_many_sigops()]
     119 | +        self.mine_and_submit(node, txs)
     120 | +
     121 | +    def submit_block_many_sigops_split(self, node):
    


    ariard commented at 1:47 AM on August 8, 2026:

    there could be a test for the case where the tx has 2501 sigops and as such is invalid per BIP54 new rule. to demonstrate the code is not doing < 2500 => good ; 2500 => not good ; > 2500 => good.

    there might be a test for it elsewhere for the exact test vector. good to have it in the python framework.


    darosior commented at 7:35 PM on August 10, 2026:

    Bound checking for sigops accounting is tested extensively in unit tests (d75143d39354d723c6a67a290dd03359841f2901). I think it makes more sense for the integration tests to check higher-level boundaries, like for instance activation periods, than duplicate those tests.

  41. in src/test/fuzz/bip54.cpp:36 in 243e7a7623
      31 | +            tx.vin.emplace_back(dummy_txid, i);
      32 | +            Unserialize(reader, tx.vin.back().scriptSig);
      33 | +
      34 | +            // Either the scriptPubKey (bare) or the redeemScript (P2SH).
      35 | +            CScript spent_script;
      36 | +            Unserialize(reader, spent_script);
    


    ariard commented at 1:57 AM on August 8, 2026:

    i would check the test before if it's covered though getting tortuous and valid control flow redeemScript with 10-depth OP_IF then the OP_CHECKSIG, then 10-depth OP_ENDIF, or with OP_ELSE to exerciser the consensus sigops script parser. not guarantee the fuzzer randomness might never yield a structured script for nested control flow.

  42. in src/validation.cpp:897 in 39cca6c97e
     893 | @@ -894,7 +894,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
     894 |      }
     895 |  
     896 |      // The mempool holds txs for the next block, so pass height+1 to CheckTxInputs
     897 | -    if (!Consensus::CheckTxInputs(tx, state, m_view, m_active_chainstate.m_chain.Height() + 1, ws.m_base_fees)) {
     898 | +    if (!Consensus::CheckTxInputs(tx, state, m_view, m_active_chainstate.m_chain.Height() + 1, ws.m_base_fees, /*enforce_bip54=*/true)) {
    


    ariard commented at 2:03 AM on August 8, 2026:

    should this check be reflected in getblocktemplate given it's already caring about "sigoplimit", "weightlimit" etc. not infringing tx should be selected in the template and yield back in the template.

    now of course, this check is enforced at tx mempool processing, so it should never appear in the template already, though having a sanitization check in getblocktemplate might good.

    and there is the more transient aspect, than until BIP54 is activated a block is legit to have an infringing tx, even if it has been deployed as policy, though once it's activated, it should not be okay anymore (so don't go to produce an invalid block for the boundary block).


    ariard commented at 2:05 AM on August 8, 2026:

    in fact it's a dumb nuisance attack vector for the few transition blocks during activation if the miners are running "lax" mempools. a tx might be okay at mempool reception, is stuck in the mempools, and even after few blocks is selected in a template rendering it invalid.


    darosior commented at 3:43 PM on August 11, 2026:

    Yes, it's already an assumption we make that no invalid transaction make it to our mempool. Note how this commit changes two things in this regard:

    • The BIP 54 sigops check can't be bypassed anymore with -acceptnonstdtxs.
    • The mempool sanity checks now assert that it does not contain any transaction that violates the BIP 54 sigops rule.
  43. ariard commented at 2:06 AM on August 8, 2026: contributor

    Did a 1st read of the fix for long block validation time, I'll continue after the vacation.

    Faut pas deconner c'est le mois d'aout.

  44. DrahtBot added the label Needs rebase on Aug 14, 2026
  45. DrahtBot commented at 5:50 PM on August 14, 2026: contributor

    <!--cf906140f33d8803c4a75a2196329ecb-->

    🐙 This pull request conflicts with the target branch and needs rebase.


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-08-21 05:51 UTC

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