miner: Enforce murch-zawy rule (BIP54)

This commit is contained in:
Fabian Jahr
2026-08-03 01:02:10 +02:00
parent 8a4bab8e97
commit e014e5bb61
2 changed files with 44 additions and 0 deletions

View File

@@ -64,6 +64,13 @@ int64_t GetMinimumTime(const CBlockIndex* pindexPrev, const int64_t difficulty_a
if (height % difficulty_adjustment_interval == 0) {
min_time = std::max<int64_t>(min_time, pindexPrev->GetBlockTime() - MAX_TIMEWARP);
}
// Account for the BIP54 Murch-Zawy rule on all networks: the last block of
// a difficulty adjustment period may not be earlier than its first block.
if (height % difficulty_adjustment_interval == difficulty_adjustment_interval - 1) {
const int first_height{height - static_cast<int>(difficulty_adjustment_interval) + 1};
const CBlockIndex* first_block{Assert(pindexPrev->GetAncestor(first_height))};
min_time = std::max<int64_t>(min_time, first_block->GetBlockTime());
}
return min_time;
}

View File

@@ -14,6 +14,7 @@ import copy
from decimal import Decimal
from test_framework.blocktools import (
create_block,
create_coinbase,
get_witness_script,
NORMAL_GBT_REQUEST_PARAMS,
@@ -252,6 +253,41 @@ class MiningTest(BitcoinTestFramework):
bad_block.solve()
node.submitheader(hexdata=CBlockHeader(bad_block).serialize().hex())
def test_murch_zawy_mintime(self):
self.log.info("Test that GetMinimumTime accounts for the Murch-Zawy rule (BIP54)")
node = self.nodes[0]
self.log.info("Mine the first block of a retarget period two hours in the future")
blockchain_info = node.getblockchaininfo()
n = DIFFICULTY_ADJUSTMENT_INTERVAL - blockchain_info['blocks'] % DIFFICULTY_ADJUSTMENT_INTERVAL - 1
t = blockchain_info['time']
for _ in range(n):
t += 600
node.setmocktime(t)
self.generate(self.wallet, 1, sync_fun=self.no_op)
node.setmocktime(t + MAX_FUTURE_BLOCK_TIME)
self.generate(self.wallet, 1, sync_fun=self.no_op)
first_block_time = node.getblock(node.getbestblockhash())['time']
assert_equal(first_block_time, t + MAX_FUTURE_BLOCK_TIME)
self.log.info("Mine to the end of the period with timestamps held back")
node.setmocktime(t)
self.generate(self.wallet, DIFFICULTY_ADJUSTMENT_INTERVAL - 2, sync_fun=self.no_op)
assert_greater_than(first_block_time, node.getblock(node.getbestblockhash())['time'])
self.log.info("The template for the last block of the period is adjusted to its first block's time")
tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)
assert_equal(tmpl['mintime'], first_block_time)
assert_equal(tmpl['curtime'], first_block_time)
block = create_block(tmpl=tmpl)
block.solve()
node.submitheader(hexdata=CBlockHeader(block).serialize().hex())
self.log.info("The node mines a valid block at the end of the period despite its early wall clock")
self.generate(self.wallet, 1, sync_fun=self.no_op)
assert_equal(node.getblock(node.getbestblockhash())['time'], first_block_time)
def test_pruning(self):
self.log.info("Test that submitblock stores previously pruned block")
prune_node = self.nodes[2]
@@ -528,6 +564,7 @@ class MiningTest(BitcoinTestFramework):
self.test_blockmintxfee_parameter()
self.test_block_max_weight()
self.test_timewarp()
self.test_murch_zawy_mintime()
self.test_pruning()
self.test_height_in_locktime()