From dd154b05689c60fad45df0df6d31cec12e09ab21 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 16 Aug 2024 09:07:16 +0200 Subject: [PATCH 1/4] consensus: lower regtest nPowTargetTimespan to 144 This currently has no effect due to fPowNoRetargeting, except for the getnetworkhashps when called with -1. It will when the next commit enforces the timewarp attack mitigation on regtest. --- src/kernel/chainparams.cpp | 2 +- test/functional/rpc_blockchain.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 4fd2d5e2d02..bf0415667ef 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -537,7 +537,7 @@ public: consensus.SegwitHeight = 0; // Always active unless overridden consensus.MinBIP9WarningHeight = 0; consensus.powLimit = uint256{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}; - consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks + consensus.nPowTargetTimespan = 24 * 60 * 60; // one day consensus.nPowTargetSpacing = 10 * 60; consensus.fPowAllowMinDifficultyBlocks = true; consensus.enforce_BIP94 = false; diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index e5aca7f138b..98147237b10 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -58,7 +58,7 @@ TIME_RANGE_STEP = 600 # ten-minute steps TIME_RANGE_MTP = TIME_GENESIS_BLOCK + (HEIGHT - 6) * TIME_RANGE_STEP TIME_RANGE_TIP = TIME_GENESIS_BLOCK + (HEIGHT - 1) * TIME_RANGE_STEP TIME_RANGE_END = TIME_GENESIS_BLOCK + HEIGHT * TIME_RANGE_STEP -DIFFICULTY_ADJUSTMENT_INTERVAL = 2016 +DIFFICULTY_ADJUSTMENT_INTERVAL = 144 class BlockchainTest(BitcoinTestFramework): From e85f386c4b157b7d1ac16aface9bd2c614e62b46 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 15 Aug 2024 17:13:33 +0200 Subject: [PATCH 2/4] consensus: enable BIP94 on regtest --- doc/release-notes-30647.md | 4 ++++ src/consensus/params.h | 4 ++++ src/kernel/chainparams.cpp | 2 +- src/validation.cpp | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 doc/release-notes-30647.md diff --git a/doc/release-notes-30647.md b/doc/release-notes-30647.md new file mode 100644 index 00000000000..ca91f0aaeb6 --- /dev/null +++ b/doc/release-notes-30647.md @@ -0,0 +1,4 @@ +Tests +----- + +- The BIP94 timewarp attack mitigation is now active on the `regtest` network diff --git a/src/consensus/params.h b/src/consensus/params.h index d970e41637a..eadfe2ba90a 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -108,6 +108,10 @@ struct Params { /** Proof of work parameters */ uint256 powLimit; bool fPowAllowMinDifficultyBlocks; + /** + * Enfore BIP94 timewarp attack mitigation. On testnet4 this also enforces + * the block storm mitigation. + */ bool enforce_BIP94; bool fPowNoRetargeting; int64_t nPowTargetSpacing; diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index bf0415667ef..e7cf56ecbc6 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -540,7 +540,7 @@ public: consensus.nPowTargetTimespan = 24 * 60 * 60; // one day consensus.nPowTargetSpacing = 10 * 60; consensus.fPowAllowMinDifficultyBlocks = true; - consensus.enforce_BIP94 = false; + consensus.enforce_BIP94 = true; consensus.fPowNoRetargeting = true; consensus.nRuleChangeActivationThreshold = 108; // 75% for testchains consensus.nMinerConfirmationWindow = 144; // Faster than normal for regtest (144 instead of 2016) diff --git a/src/validation.cpp b/src/validation.cpp index bf2b4b315e6..8c80e35c017 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4189,7 +4189,7 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio if (block.GetBlockTime() <= pindexPrev->GetMedianTimePast()) return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, "time-too-old", "block's timestamp is too early"); - // Testnet4 only: Check timestamp against prev for difficulty-adjustment + // Testnet4 and regtest only: Check timestamp against prev for difficulty-adjustment // blocks to prevent timewarp attacks (see https://github.com/bitcoin/bitcoin/pull/15482). if (consensusParams.enforce_BIP94) { // Check timestamp for the first block of each difficulty adjustment From e929054e12210353812f440c685a23329e7040f7 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 20 Aug 2024 18:49:59 +0200 Subject: [PATCH 3/4] Add timewarp attack mitigation test --- test/functional/mining_basic.py | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 6a364a48159..b183023b2f7 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -34,6 +34,9 @@ from test_framework.util import ( from test_framework.wallet import MiniWallet +DIFFICULTY_ADJUSTMENT_INTERVAL = 144 +MAX_FUTURE_BLOCK_TIME = 2 * 3600 +MAX_TIMEWARP = 600 VERSIONBITS_TOP_BITS = 0x20000000 VERSIONBITS_DEPLOYMENT_TESTDUMMY_BIT = 28 DEFAULT_BLOCK_MIN_TX_FEE = 1000 # default `-blockmintxfee` setting [sat/kvB] @@ -115,6 +118,46 @@ class MiningTest(BitcoinTestFramework): assert tx_below_min_feerate['txid'] not in block_template_txids assert tx_below_min_feerate['txid'] not in block_txids + def test_timewarp(self): + self.log.info("Test timewarp attack mitigation (BIP94)") + node = self.nodes[0] + + self.log.info("Mine until the last block of the retarget period") + blockchain_info = self.nodes[0].getblockchaininfo() + n = DIFFICULTY_ADJUSTMENT_INTERVAL - blockchain_info['blocks'] % DIFFICULTY_ADJUSTMENT_INTERVAL - 2 + t = blockchain_info['time'] + + for _ in range(n): + t += 600 + self.nodes[0].setmocktime(t) + self.generate(self.wallet, 1, sync_fun=self.no_op) + + self.log.info("Create block two hours in the future") + self.nodes[0].setmocktime(t + MAX_FUTURE_BLOCK_TIME) + self.generate(self.wallet, 1, sync_fun=self.no_op) + assert_equal(node.getblock(node.getbestblockhash())['time'], t + MAX_FUTURE_BLOCK_TIME) + + self.log.info("First block template of retarget period can't use wall clock time") + self.nodes[0].setmocktime(t) + assert_raises_rpc_error(-1, "time-timewarp-attack, block's timestamp is too early on diff adjustment block", + lambda: node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) + + # Create template with an acceptable timestamp and then modify it + self.nodes[0].setmocktime(t + MAX_FUTURE_BLOCK_TIME) + tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) + + block = CBlock() + block.nVersion = tmpl["version"] + block.hashPrevBlock = int(tmpl["previousblockhash"], 16) + block.nTime = t + block.nBits = int(tmpl["bits"], 16) + block.nNonce = 0 + block.vtx = [create_coinbase(height=int(tmpl["height"]))] + block.solve() + + self.nodes[0].setmocktime(t) + assert_raises_rpc_error(-25, 'time-timewarp-attack', lambda: node.submitheader(hexdata=CBlockHeader(block).serialize().hex())) + def run_test(self): node = self.nodes[0] self.wallet = MiniWallet(node) @@ -322,6 +365,7 @@ class MiningTest(BitcoinTestFramework): assert_equal(node.submitblock(hexdata=block.serialize().hex()), 'duplicate') # valid self.test_blockmintxfee_parameter() + self.test_timewarp() if __name__ == '__main__': From 59ff17e5af4e382cbe16f183767beef1bdcd9131 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 20 Aug 2024 10:45:04 +0200 Subject: [PATCH 4/4] miner: adjust clock to timewarp rule --- src/consensus/consensus.h | 7 +++++++ src/node/miner.cpp | 8 ++++++++ src/validation.cpp | 7 ------- test/functional/mining_basic.py | 17 +++++++++-------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/consensus/consensus.h b/src/consensus/consensus.h index 384f70bc104..cffe9cdafd7 100644 --- a/src/consensus/consensus.h +++ b/src/consensus/consensus.h @@ -27,4 +27,11 @@ static const size_t MIN_SERIALIZABLE_TRANSACTION_WEIGHT = WITNESS_SCALE_FACTOR * /** Interpret sequence numbers as relative lock-time constraints. */ static constexpr unsigned int LOCKTIME_VERIFY_SEQUENCE = (1 << 0); +/** + * Maximum number of seconds that the timestamp of the first + * block of a difficulty adjustment period is allowed to + * be earlier than the last block of the previous period (BIP94). + */ +static constexpr int64_t MAX_TIMEWARP = 600; + #endif // BITCOIN_CONSENSUS_CONSENSUS_H diff --git a/src/node/miner.cpp b/src/node/miner.cpp index fa2d979b86a..5c476e154f4 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -33,6 +33,14 @@ int64_t UpdateTime(CBlockHeader* pblock, const Consensus::Params& consensusParam int64_t nOldTime = pblock->nTime; int64_t nNewTime{std::max(pindexPrev->GetMedianTimePast() + 1, TicksSinceEpoch(NodeClock::now()))}; + if (consensusParams.enforce_BIP94) { + // Height of block to be mined. + const int height{pindexPrev->nHeight + 1}; + if (height % consensusParams.DifficultyAdjustmentInterval() == 0) { + nNewTime = std::max(nNewTime, pindexPrev->GetBlockTime() - MAX_TIMEWARP); + } + } + if (nOldTime < nNewTime) { pblock->nTime = nNewTime; } diff --git a/src/validation.cpp b/src/validation.cpp index 8c80e35c017..8f75b2e30a0 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -107,13 +107,6 @@ const std::vector CHECKLEVEL_DOC { * */ static constexpr int PRUNE_LOCK_BUFFER{10}; -/** - * Maximum number of seconds that the timestamp of the first - * block of a difficulty adjustment period is allowed to - * be earlier than the last block of the previous period (BIP94). - */ -static constexpr int64_t MAX_TIMEWARP = 600; - GlobalMutex g_best_block_mutex; std::condition_variable g_best_block_cv; uint256 g_best_block; diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index b183023b2f7..c0df120c65a 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -28,6 +28,7 @@ from test_framework.p2p import P2PDataStore from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than_or_equal, assert_raises_rpc_error, get_fee, ) @@ -139,24 +140,24 @@ class MiningTest(BitcoinTestFramework): self.log.info("First block template of retarget period can't use wall clock time") self.nodes[0].setmocktime(t) - assert_raises_rpc_error(-1, "time-timewarp-attack, block's timestamp is too early on diff adjustment block", - lambda: node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)) - - # Create template with an acceptable timestamp and then modify it - self.nodes[0].setmocktime(t + MAX_FUTURE_BLOCK_TIME) + # The template will have an adjusted timestamp, which we then modify tmpl = node.getblocktemplate(NORMAL_GBT_REQUEST_PARAMS) + assert_greater_than_or_equal(tmpl['curtime'], t + MAX_FUTURE_BLOCK_TIME - MAX_TIMEWARP) block = CBlock() block.nVersion = tmpl["version"] block.hashPrevBlock = int(tmpl["previousblockhash"], 16) - block.nTime = t + block.nTime = tmpl["curtime"] block.nBits = int(tmpl["bits"], 16) block.nNonce = 0 block.vtx = [create_coinbase(height=int(tmpl["height"]))] block.solve() + assert_template(node, block, None) - self.nodes[0].setmocktime(t) - assert_raises_rpc_error(-25, 'time-timewarp-attack', lambda: node.submitheader(hexdata=CBlockHeader(block).serialize().hex())) + bad_block = copy.deepcopy(block) + bad_block.nTime = t + bad_block.solve() + assert_raises_rpc_error(-25, 'time-timewarp-attack', lambda: node.submitheader(hexdata=CBlockHeader(bad_block).serialize().hex())) def run_test(self): node = self.nodes[0]