Skip to content

BIP54 "Consensus Cleanup" implementation#99

Merged
ajtowns merged 23 commits intobitcoin-inquisition:29.xfrom
darosior:2509_inquisition_consensus_cleanup
Feb 6, 2026
Merged

BIP54 "Consensus Cleanup" implementation#99
ajtowns merged 23 commits intobitcoin-inquisition:29.xfrom
darosior:2509_inquisition_consensus_cleanup

Conversation

@darosior
Copy link

@darosior darosior commented Oct 21, 2025

This implements the BIP54 consensus rules.

Each of the 4 mitigations comes with its own unit test in a new src/test/bip54_tests.cpp module. The unit tests can be used to generate JSON test vectors for the BIP (opt-in). For those unit tests that require mainnet blocks, the JSON test vectors were generated separately and included in this PR as data files.

Besides the unit test for each mitigation, this contains a fuzz harness for the sigop accounting logic as well as a functional test which goes over all mitigations in pseudo realistic situations (for instance, mimic a Timewarp attack and a Murch-Zawy attack to demonstrate the new timestamp restrictions would prevent that). The fuzz target for the sigop accounting logic was designed to make it possible to seed it from the BIP test vectors. This branch implements that.

The chains of headers for the timestamp restrictions test vectors were generated using this unit test (note the premined headers to allow modifications without re-generating all headers from scratch). The small chains of blocks for the coinbase restrictions test vectors were generated using this unit test.

@darosior darosior force-pushed the 2509_inquisition_consensus_cleanup branch from ee557fc to bc95a67 Compare October 21, 2025 12:49
@darosior darosior force-pushed the 2509_inquisition_consensus_cleanup branch from bc95a67 to 8d513a0 Compare October 21, 2025 13:23
@DrahtBot
Copy link
Collaborator

DrahtBot commented Oct 21, 2025

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #94 (Allow configuring target block time for a signet by benthecarman)

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.

Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just going over the introduced consensus code fix by fix for now.

@darosior darosior force-pushed the 2509_inquisition_consensus_cleanup branch 3 times, most recently from 98a9a5e to 224ef91 Compare December 4, 2025 19:19
@darosior
Copy link
Author

darosior commented Dec 4, 2025

Thanks for kicking CI @ajtowns. I fixed the missing annotations that DEBUG_LOCKORDER complained about.

@darosior darosior force-pushed the 2509_inquisition_consensus_cleanup branch from 224ef91 to 5e06850 Compare December 5, 2025 14:35
Copy link

@instagibbs instagibbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Focused primarily on BIP compliance, everything looks good to me

@ajtowns ajtowns added this to the 29.x milestone Dec 9, 2025
Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a first review on the 64-byte fix, answering opened comments.

@Sjors
Copy link

Sjors commented Dec 12, 2025

The consensus changes, with respect to their specification in BIP54, look good to me:

  • 84e1043 validation: make BIP54 sigops check consensus-critical
  • 87e9e65 consensus: prevent timewarp attacks with a 2h grace period
  • c6c1b18 consensus: prevent negative difficulty adjustment intervals
  • 00745a2 consensus: mandate coinbase transactions be timelocked to block height
  • 8ee48e4 consensus: make 64 bytes transactions invalid

I think this PR is well organised (starting at ---). Each consensus change is first prepared, done in a minimal commit, and then followed by new tests.

I'll study the test changes in more detail later.

Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See second comment on nSequence == SEQUENCE_FINAL`.

@ariard
Copy link

ariard commented Dec 20, 2025

Finished a first round of review of the code for the 4 consensus fixes:

  • “difficulty adjustment exploits”
  • “long block validation time”
  • “merkle tree malleability with 64-byte transaction”
  • “possibility of duplicate coinbase transactions”

Will review more the fixes at the conceptual level and the breadth / depth of their respective test coverage.

Copy link

@Sjors Sjors left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See inline suggestion for renaming the bip_active argument to check_bip54 and making it a bit more clear in the tests if they're checking standardness (pre activation) or consensus (post activation).

The other consensus commit changes for the 19d159e push (compared to my previous review) look fine.

return true; // Coinbases don't use vin normally
}

if (!Consensus::CheckSigopsBIP54(tx, mapInputs)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In d1d435f validation: make BIP54 sigops check consensus-critical: note to other reviewers: this doesn't prematurely drop the standardness check before activation.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does, however, make the BIP54 checks mandatory for the mempool, such that they cannot be disabled by acceptnonstdtxn, aiui. (Which is good: you don't want consensus-invalid txs in your mempool at the point the consensus change activates)

This encapsulates the soft fork configuration logic as set by the `-testactivationheight` (for
buried deployments) and `-vbparams` (for version bits deployments) options which for the moment
are regtest-only, in order to make them available on other networks as well in the next commit.

Can be reviewed using git's --color-moved option.
@darosior darosior force-pushed the 2509_inquisition_consensus_cleanup branch from 19d159e to f24256f Compare December 29, 2025 17:02
darosior and others added 14 commits February 4, 2026 13:51
The fuzz target was specifically crafted to support seeding it with the BIP54 test vectors
generated by the unit test in the previous commit.
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-
That is, enforce nLockTime be set to height-1 and nSequence not be set to final.
… 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.
It's not a standardness limit anymore, it was made consensus.

Thanks to Anthony Towns for the scripted diff script.

-BEGIN VERIFY SCRIPT-
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>
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.
@darosior darosior force-pushed the 2509_inquisition_consensus_cleanup branch from 0d298f5 to 55cd9a5 Compare February 4, 2026 19:18
@darosior
Copy link
Author

darosior commented Feb 4, 2026

That said, the cache doesn't seem necessary, just avoiding serializing the entire block as json seems to improve things ~60x for me

Would simply adding the block's coinbase's locktime to the output of verbosity-1 getblock be sufficient then?

diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index bbe401fcc6c..78062c74e6f 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -183,6 +183,7 @@ UniValue blockToJSON(BlockManager& blockman, const CBlock& block, const CBlockIn
     result.pushKV("strippedsize", (int)::GetSerializeSize(TX_NO_WITNESS(block)));
     result.pushKV("size", (int)::GetSerializeSize(TX_WITH_WITNESS(block)));
     result.pushKV("weight", (int)::GetBlockWeight(block));
+    result.pushKV("cblocktime", block.vtx.at(0)->nLockTime);
     UniValue txs(UniValue::VARR);
 
     switch (verbosity) {
@@ -724,6 +725,7 @@ static RPCHelpMan getblock()
                     {RPCResult::Type::NUM, "version", "The block version"},
                     {RPCResult::Type::STR_HEX, "versionHex", "The block version formatted in hexadecimal"},
                     {RPCResult::Type::STR_HEX, "merkleroot", "The merkle root"},
+                    {RPCResult::Type::NUM, "locktime", "The coinbase transaction's locktime value"},
                     {RPCResult::Type::ARR, "tx", "The transaction ids",
                         {{RPCResult::Type::STR_HEX, "", "The transaction id"}}},
                     {RPCResult::Type::NUM_TIME, "time",       "The block time expressed in " + UNIX_EPOCH_TIME},

I've tested this on mainnet with your snippet from earlier and it's reasonably performant.

@ariard
Copy link

ariard commented Feb 5, 2026

On the coinbase transaction and requesting the nLocktime to be set to the current block height minus one and the nSequence field must not be equal to 0xff_ff_ff_ff, I'm still thinking the second check is unecessary. The first check is ensuring that the transaction is always final (2nd check of IsFinal()). I’ve yet to verify with a test, but post-BIP54 the 3rd check in IsFinal() should never be reach by validating a coinbase transaction on its own.

#99 (comment)

https://groups.google.com/g/bitcoindev/c/6TTlDwP2OQg

Per se, unless I'm missing something, I don't think the value of the nSequence field needs to be set to a precise value and the whole field could be even “free" up field either for (a) for an extra nonce or (b) as a space where to fit a new commitment structure in the future (and that's particularly interesting as it would work even if there are no segwit txn in the block cf. BIP141). cc @darosior


// Make sure the coinbase transaction is timelocked to the block's height.
if (nHeight > 0 && DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_CONSENSUSCLEANUP)) {
Assert(!block.vtx.empty() && block.vtx[0] && !block.vtx[0]->vin.empty());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not consensus, but more relay. At CMPCTBLOCK reception, there must be at least one transaction.

    if (cmpctblock.header.IsNull() || (cmpctblock.shorttxids.empty() && cmpctblock.prefilledtxn.empty()))
        return READ_STATUS_INVALID;

Sanitizing that the received coinbase is respecting the new rules of consensus avoids to store cmpct block state for a block that will be overall consenus invalid (note the validation of the coinbase is already lax afaict).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be a tiny optimisation in the event that someone decided to waste an enormous amount of energy creating an invalid block. I don't think it's worth it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of compact block relay peers != block relay peers iirc, so I think there still some margin of shitting the coinbase tx and playing with it, but effectively I don’t think it’s something per se new coming with BIP54, and it’s rather ante date it.

@ajtowns
Copy link

ajtowns commented Feb 5, 2026

Would simply adding the block's coinbase's locktime to the output of verbosity-1 getblock be sufficient then?

Yeah, seems like -- it takes about 3 times as long as the dedicated RPC for me in a quick test, but that's good enough for how much simpler it is. I found "cblocktime" confusing (CBlock time?), maybe tx0_locktime? Also should report whether nseq is final or not. Could also just report "bip54_coinbase_readiness": true/false.

I messed around with the dedicated approach some more; it looks like if you add a dedicated ReadBlockCoinbaseOnly method to block storage, which just reads the header and coinbase tx, but not the rest of the block, then the dedicated rpc gets a massive speedup. My figures are:

  • just using the RPCs we have now: 2 retarget periods takes about 2 hours to scan
  • a couple of lines of code to add reporting it to getblock 1: 2 retarget periods takes about 10 minutes to scan
  • dedicated rpc: 2 retarget periods takes about 4 minutes to scan
  • dedicated rpc loading block headers and coinbase tx only: 2 retarget periods takes under 5 seconds to scan

I'll update I've updated my branch, but the getblock 1 approach seems like the sweet spot. (It's probably also faster if your node doesn't have quite as convoluted a setup as mine)

@Sjors
Copy link

Sjors commented Feb 5, 2026

I found "cblocktime" confusing (CBlock time?), maybe tx0_locktime?

Indeed, it reads like "c block time", cb_locktime should work.

Also should report whether nseq is final or not. Could also just report "bip54_coinbase_readiness": true/false.

Ideally we keep it generic enough so it can be PR'd to Bitcoin Core indepedent of BIP54.

Maybe add a coinbase field to getblock with (a guaranteed to be small subset of) fields similar to the CoinbaseTx struct from bitcoin#33819 : version, sequence and lock_time. Could add an is_final bool for convenience, given its relevance.


Update: bitcoin#34512

@ajtowns
Copy link

ajtowns commented Feb 5, 2026

ACK 55cd9a5 -- this looks good to me i think, and should be ready to merge

@darosior
Copy link
Author

darosior commented Feb 5, 2026

#99 (comment)

https://groups.google.com/g/bitcoindev/c/6TTlDwP2OQg

Per se, unless I'm missing something, I don't think the value of the nSequence field needs to be set

@ariard i have replied to you on the mailing list. TL;DR: it seems my explanation was lacking and you missed the purpose for making sure nSequence is never final.

to a precise value

Note also we don't constrain it to be a specific value. On the contrary it can be anything as long it's not the specific final value.

@ajtowns ajtowns merged commit ae8c5c3 into bitcoin-inquisition:29.x Feb 6, 2026
18 checks passed
@ariard
Copy link

ariard commented Feb 12, 2026

@ariard i have replied to you on the mailing list. TL;DR: it seems my explanation was lacking and you missed the purpose for making sure nSequence is never final.

@darosior I think I got your idea with the assumption that BIP54 can be buried in the future, and the check to be applied on all historical coinbase transactions, therefore ensuring any old coinbase cannot be used to generate a post-BIP54 action height identifier collision.

Replied, I’m still thinking you can make a simpler assumption by just relying on the PoW ordering mechanism itself. There can be only and only one block valid at a given chain height.

@ariard
Copy link

ariard commented Feb 12, 2026

Note also we don't constrain it to be a specific value. On the contrary it can be anything as long it's not the specific final value.

Yes, I see it doesn’t have to be set to a specific value. See the point on protecting the coinbase nSequence field. It’s of independent interest, that the check is kept or not.

@ajtowns
Copy link

ajtowns commented Feb 12, 2026

@darosior I think I got your idea with the assumption that BIP54 can be buried in the future, and the check to be applied on all historical coinbase transactions,

Historical coinbase transactions on mainnet, testnet3, testnet4 and signet all fail to comply with bip54 rules, so that approach isn't possible.

@ariard
Copy link

ariard commented Feb 15, 2026

Historical coinbase transactions on mainnet, testnet3, testnet4 and signet all fail to comply with bip54 rules, so that approach isn't possible.

Okay so up to Darosior what he meant here on the ml:

"For instance, if BIP 54 activation height ever gets buried in Bitcoin Core, the BIP 30 check could just be disabled past
this height instead of having to figure out if we are on a chain that contains the historical BIP 34 activation block hash [3].

So strictly a buried activation height, nothing like a retroactive validation from genesis like it was done for BIP30.

Point is the additional check in nsequence field is superflous imho.

@ajtowns
Copy link

ajtowns commented Feb 15, 2026

The issue is what happens if there is a reorg back to a point where neither BIP 34 or BIP 54 are active. At that point, BIP 30 checks must be enabled. With the nsequence check, those checks no longer need to be performed after BIP 54 is enabled because no coinbase that complies with its rules can have ever previously appeared in the blockchain. Also, this discussion should be on the mailing list, not here.

sedited added a commit to bitcoin/bitcoin that referenced this pull request Feb 19, 2026
e0463b4 rpc: add coinbase_tx field to getblock (Sjors Provoost)

Pull request description:

  This adds a `coinbase_tx` field to the `getblock` RPC result, starting at verbosity level 1. It contains only fields guaranteed to be small, i.e. not the outputs.

  Initial motivation for this was to more efficiently scan for BIP54 compliance. Without this change, it requires verbosity level 2 to get the coinbase, which makes such scan very slow. See bitcoin-inquisition#99 (comment).

  Adding these fields should be useful in general though and hardly makes the verbosity 1 result longer.

  ```
  bitcoin rpc help getblock

  getblock "blockhash" ( verbosity )

  If verbosity is 0, returns a string that is serialized, hex-encoded data for block 'hash'.
  If verbosity is 1, returns an Object with information about block <hash>.
  If verbosity is 2, returns an Object with information about block <hash> and information about each transaction.
  ...
  Result (for verbosity = 1):
  {                                 (json object)
    "hash" : "hex",                 (string) the block hash (same as provided)
    "confirmations" : n,            (numeric) The number of confirmations, or -1 if the block is not on the main chain
    "size" : n,                     (numeric) The block size
    "strippedsize" : n,             (numeric) The block size excluding witness data
    "weight" : n,                   (numeric) The block weight as defined in BIP 141
    "coinbase_tx" : {               (json object) Coinbase transaction metadata
      "version" : n,                (numeric) The coinbase transaction version
      "locktime" : n,               (numeric) The coinbase transaction's locktime (nLockTime)
      "sequence" : n,               (numeric) The coinbase input's sequence number (nSequence)
      "coinbase" : "hex",         (string) The coinbase input's script
      "witness" : "hex"             (string, optional) The coinbase input's first (and only) witness stack element, if present
    },
    "height" : n,                   (numeric) The block height or index
    "version" : n,                  (numeric) The block version
  ...
  ```

  ```
  bitcoin rpc getblock 000000000000000000013c986f9aebe800a78454c835ccd07ecae2650bfad3f6 1
  ```

  ```json
  {
    "hash": "000000000000000000013c986f9aebe800a78454c835ccd07ecae2650bfad3f6",
    "confirmations": 2,
    "height": 935113,
    "version": 561807360,
    "...": "...",
    "weight": 3993624,
    "coinbase_tx": {
      "version": 2,
      "locktime": 0,
      "sequence": 4294967295,
      "coinbase": "03c9440e04307c84692f466f756e6472792055534120506f6f6c202364726f70676f6c642ffabe6d6d9a8624235259d3680c972b0dd42fa3fe1c45c5e5ae5a96fe10c182bda17080e70100000000000000184b17d3f138020000000000",
      "witness": "0000000000000000000000000000000000000000000000000000000000000000"
    },
    "tx": [
      "70eb053340c7978c5aa1b34d75e1ba9f9d1879c09896317f306f30c243536b62",
      "5bcf8ed2900cb70721e808b8977898e47f2c9001fcee83c3ccd29e51c7775dcd",
      "3f1991771aef846d7bb379d2931cccc04e8421a630ec9f52d22449d028d2e7f4",
      "..."
    ]
  }
  ```

ACKs for top commit:
  sedited:
    Re-ACK e0463b4
  darosior:
    re-utACK e0463b4

Tree-SHA512: 1b3e7111e6a0edffde8619c49b3e9bca833c8e90e416defc66811bd56dd00d45b69a84c8fd9715508f4e6515f77ac4fb5c59868ab997ae111017c78c05b74ba3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants